refactor: Streamlined graph-update semantics.

main
Sofus Albert Høgsbro Rose 2024-04-12 15:39:13 +02:00
parent dc76ab7688
commit 480679a3c0
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
29 changed files with 1905 additions and 866 deletions

12
TODO.md
View File

@ -1,12 +1,17 @@
# Acute Tasks # Acute Tasks
- [x] Implement Material Import for Maxim Data - [x] Implement Material Import for Maxim Data
- [x] Implement Robust DataFlowKind for list-like / spectral-like composite types - [x] Implement Robust DataFlowKind for list-like / spectral-like composite types
- [ ] Unify random node/socket caches.
- [ ] Finish the "Low-Hanging Fruit" Nodes - [ ] Finish the "Low-Hanging Fruit" Nodes
- [ ] Move preview GN trees to the asset library. - [ ] Move preview GN trees to the asset library.
# Nodes # Nodes
## Analysis
- [ ] Extract
- [ ] Viz
## Inputs ## Inputs
- [x] Wave Constant - [x] Wave Constant
- [x] Implement export of frequency / wavelength array/range. - [x] Implement export of frequency / wavelength array/range.
@ -161,12 +166,7 @@
- [ ] Sim Grid Axes / Uniform Sim Grid Axis - [ ] Sim Grid Axes / Uniform Sim Grid Axis
- [ ] Sim Grid Axes / Array Sim Grid Axis - [ ] Sim Grid Axes / Array Sim Grid Axis
## Converters ## Utilities
- [ ] Math
- [ ] Implement common operations w/secondary choice of socket type based on a custom internal data structure
- [ ] Implement angfreq/frequency/vacwl conversion.
- [ ] Implement spectral math on SDs
- [ ] Implement easy derivation of ex. transmission and reflection.
- [ ] Separate - [ ] Separate
- [x] Combine - [x] Combine
- [x] Implement concatenation of sim-critical socket types into their multi-type - [x] Implement concatenation of sim-critical socket types into their multi-type

View File

@ -23,6 +23,7 @@ dependencies = [
"charset-normalizer==2.0.10", "charset-normalizer==2.0.10",
"certifi==2021.10.8", "certifi==2021.10.8",
"jax[cpu]>=0.4.26", "jax[cpu]>=0.4.26",
"msgspec[toml]>=0.18.6",
] ]
readme = "README.md" readme = "README.md"
requires-python = "~= 3.11" requires-python = "~= 3.11"
@ -136,4 +137,4 @@ max-args = 6
[tool.ruff.format] [tool.ruff.format]
quote-style = "single" quote-style = "single"
indent-style = "tab" indent-style = "tab"
docstring-code-format = true docstring-code-format = false

View File

@ -63,6 +63,7 @@ ml-dtypes==0.4.0
# via jaxlib # via jaxlib
mpmath==1.3.0 mpmath==1.3.0
# via sympy # via sympy
msgspec==0.18.6
networkx==3.2 networkx==3.2
numpy==1.24.3 numpy==1.24.3
# via contourpy # via contourpy
@ -135,6 +136,8 @@ sympy==1.12
tidy3d==2.6.3 tidy3d==2.6.3
toml==0.10.2 toml==0.10.2
# via tidy3d # via tidy3d
tomli-w==1.0.0
# via msgspec
toolz==0.12.1 toolz==0.12.1
# via dask # via dask
# via partd # via partd

View File

@ -62,6 +62,7 @@ ml-dtypes==0.4.0
# via jaxlib # via jaxlib
mpmath==1.3.0 mpmath==1.3.0
# via sympy # via sympy
msgspec==0.18.6
networkx==3.2 networkx==3.2
numpy==1.24.3 numpy==1.24.3
# via contourpy # via contourpy
@ -133,6 +134,8 @@ sympy==1.12
tidy3d==2.6.3 tidy3d==2.6.3
toml==0.10.2 toml==0.10.2
# via tidy3d # via tidy3d
tomli-w==1.0.0
# via msgspec
toolz==0.12.1 toolz==0.12.1
# via dask # via dask
# via partd # via partd

View File

@ -0,0 +1,529 @@
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
import functools
import inspect
import typing as typ
import bpy
import msgspec
import sympy as sp
import sympy.physics.units as spu
from ...utils import extra_sympy_units as spux
from ...utils import logger
from . import contracts as ct
from . import managed_objs, sockets
log = logger.get(__name__)
InstanceID: typ.TypeAlias = str ## Stringified UUID4
class BLInstance(typ.Protocol):
"""An instance of a blender object, ex. nodes/sockets.
Attributes:
instance_id: Stringified UUID4 that uniquely identifies an instance, among all active instances on all active classes.
"""
instance_id: InstanceID
EncodableValue: typ.TypeAlias = typ.Any ## msgspec-compatible
PropGetMethod: typ.TypeAlias = typ.Callable[[BLInstance], EncodableValue]
PropSetMethod: typ.TypeAlias = typ.Callable[[BLInstance, EncodableValue], None]
####################
# - (De)Serialization
####################
EncodedComplex: typ.TypeAlias = tuple[float, float] | list[float, float]
EncodedSympy: typ.TypeAlias = str
EncodedManagedObj: typ.TypeAlias = tuple[str, str] | list[str, str]
EncodedPydanticModel: typ.TypeAlias = tuple[str, str] | list[str, str]
def _enc_hook(obj: typ.Any) -> EncodableValue:
"""Translates types not natively supported by `msgspec`, to an encodable form supported by `msgspec`.
Parameters:
obj: The object of arbitrary type to transform into an encodable value.
Returns:
A value encodable by `msgspec`.
Raises:
NotImplementedError: When the type transformation hasn't been implemented.
"""
if isinstance(obj, complex):
return (obj.real, obj.imag)
if isinstance(obj, sp.Basic | sp.MatrixBase | sp.Expr | spu.Quantity):
return sp.srepr(obj)
if isinstance(obj, managed_objs.ManagedObj):
return (obj.name, obj.__class__.__name__)
if isinstance(obj, ct.schemas.SocketDef):
return (obj.model_dump(), obj.__class__.__name__)
msg = f'Can\'t encode "{obj}" of type {type(obj)}'
raise NotImplementedError(msg)
def _dec_hook(_type: type, obj: EncodableValue) -> typ.Any:
"""Translates the `msgspec`-encoded form of an object back to its true form.
Parameters:
_type: The type to transform the `msgspec`-encoded object back into.
obj: The encoded object of to transform back into an encodable value.
Returns:
A value encodable by `msgspec`.
Raises:
NotImplementedError: When the type transformation hasn't been implemented.
"""
if _type is complex and isinstance(obj, EncodedComplex):
return complex(obj[0], obj[1])
if (
_type is sp.Basic
and isinstance(obj, EncodedSympy)
or _type is sp.Expr
and isinstance(obj, EncodedSympy)
or _type is sp.MatrixBase
and isinstance(obj, EncodedSympy)
or _type is spu.Quantity
and isinstance(obj, EncodedSympy)
):
return sp.sympify(obj).subs(spux.ALL_UNIT_SYMBOLS)
if (
_type is managed_objs.ManagedBLMesh
and isinstance(obj, EncodedManagedObj)
or _type is managed_objs.ManagedBLImage
and isinstance(obj, EncodedManagedObj)
or _type is managed_objs.ManagedBLModifier
and isinstance(obj, EncodedManagedObj)
):
return {
'ManagedBLMesh': managed_objs.ManagedBLMesh,
'ManagedBLImage': managed_objs.ManagedBLImage,
'ManagedBLModifier': managed_objs.ManagedBLModifier,
}[obj[1]](obj[0])
if _type is ct.schemas.SocketDef:
return getattr(sockets, obj[1])(**obj[0])
msg = f'Can\'t decode "{obj}" to type {type(obj)}'
raise NotImplementedError(msg)
ENCODER = msgspec.json.Encoder(enc_hook=_enc_hook, order='deterministic')
_DECODERS: dict[type, msgspec.json.Decoder] = {
complex: msgspec.json.Decoder(type=complex, dec_hook=_dec_hook),
sp.Basic: msgspec.json.Decoder(type=sp.Basic, dec_hook=_dec_hook),
sp.Expr: msgspec.json.Decoder(type=sp.Expr, dec_hook=_dec_hook),
sp.MatrixBase: msgspec.json.Decoder(type=sp.MatrixBase, dec_hook=_dec_hook),
spu.Quantity: msgspec.json.Decoder(type=spu.Quantity, dec_hook=_dec_hook),
managed_objs.ManagedBLMesh: msgspec.json.Decoder(
type=managed_objs.ManagedBLMesh,
dec_hook=_dec_hook,
),
managed_objs.ManagedBLImage: msgspec.json.Decoder(
type=managed_objs.ManagedBLImage,
dec_hook=_dec_hook,
),
managed_objs.ManagedBLModifier: msgspec.json.Decoder(
type=managed_objs.ManagedBLModifier,
dec_hook=_dec_hook,
),
# managed_objs.ManagedObj: msgspec.json.Decoder(
# type=managed_objs.ManagedObj, dec_hook=_dec_hook
# ), ## Doesn't work b/c unions are not explicit
ct.schemas.SocketDef: msgspec.json.Decoder(
type=ct.schemas.SocketDef,
dec_hook=_dec_hook,
),
}
_DECODER_FALLBACK: msgspec.json.Decoder = msgspec.json.Decoder(dec_hook=_dec_hook)
@functools.cache
def DECODER(_type: type) -> msgspec.json.Decoder: # noqa: N802
"""Retrieve a suitable `msgspec.json.Decoder` by-type.
Parameters:
_type: The type to retrieve a decoder for.
Returns:
A suitable decoder.
"""
if (decoder := _DECODERS.get(_type)) is not None:
return decoder
return _DECODER_FALLBACK
def decode_any(_type: type, obj: str) -> typ.Any:
naive_decode = DECODER(_type).decode(obj)
if _type == dict[str, ct.schemas.SocketDef]:
return {
socket_name: getattr(sockets, socket_def_list[1])(**socket_def_list[0])
for socket_name, socket_def_list in naive_decode.items()
}
log.critical(
'Naive Decode of "%s" to "%s" (%s)', str(obj), str(naive_decode), str(_type)
)
return naive_decode
####################
# - Cache: Non-Persistent
####################
CACHE_NOPERSIST: dict[InstanceID, dict[typ.Any, typ.Any]] = {}
def invalidate_nonpersist_instance_id(instance_id: InstanceID) -> None:
"""Invalidate any `instance_id` that might be utilizing cache space in `CACHE_NOPERSIST`.
Note:
This should be run by the `instance_id` owner in its `free()` method.
Parameters:
instance_id: The ID of the Blender object instance that's being freed.
"""
CACHE_NOPERSIST.pop(instance_id, None)
####################
# - Property Descriptor
####################
class CachedBLProperty:
"""A descriptor that caches a computed attribute of a Blender node/socket/... instance (`bl_instance`), with optional cache persistence.
Note:
**Accessing the internal `_*` attributes is likely an anti-pattern**.
`CachedBLProperty` does not own the data; it only provides a convenient interface of running user-provided getter/setters.
This also applies to the `bpy.types.Property` entry created by `CachedBLProperty`, which should not be accessed directly.
Attributes:
_getter_method: Method of `bl_instance` that computes the value.
_setter_method: Method of `bl_instance` that sets the value.
_persist: Whether to persist the value on a `bpy.types.Property` defined on `bl_instance`.
The name of this `bpy.types.Property` will be `cache__<prop_name>`.
_type: The type of the value, used by the persistent decoder.
"""
def __init__(self, getter_method: PropGetMethod, persist: bool):
"""Initialize the getter (and persistance) of the cached property.
Notes:
- When `persist` is true, the return annotation of the getter mathod will be used to guide deserialization.
Parameters:
getter_method: Method of `bl_instance` that computes the value.
persist: Whether to persist the value on a `bpy.types.Property` defined on `bl_instance`.
The name of this `bpy.types.Property` will be `cache__<prop_name>`.
"""
self._getter_method: PropGetMethod = getter_method
self._setter_method: PropSetMethod | None = None
# Persistance
self._persist: bool = persist
self._type: type | None = (
inspect.signature(getter_method).return_annotation if persist else None
)
# Check Non-Empty Type Annotation
## For now, just presume that all types can be encoded/decoded.
# Check Non-Empty Type Annotation
## For now, just presume that all types can be encoded/decoded.
if self._type is not None and self._type is inspect.Signature.empty:
msg = f'A CachedBLProperty was instantiated with "persist={persist}", but its getter method "{self._getter_method}" has no return type annotation'
raise TypeError(msg)
def __set_name__(self, owner: type[BLInstance], name: str) -> None:
"""Generates the property name from the name of the attribute that this descriptor is assigned to.
Notes:
- Run by Python when setting an instance of this class to an attribute.
Parameters:
owner: The class that contains an attribute assigned to an instance of this descriptor.
name: The name of the attribute that an instance of descriptor was assigned to.
"""
self.prop_name: str = name
self._bl_prop_name: str = f'blcache__{name}'
# Define Blender Property (w/Update Sync)
owner.set_prop(
self._bl_prop_name,
bpy.props.StringProperty,
name=f'DO NOT USE: Cache for {self.prop_name}',
default='',
no_update=True,
)
def __get__(
self, bl_instance: BLInstance | None, owner: type[BLInstance]
) -> typ.Any:
"""Retrieves the property from a cache, or computes it and fills the cache(s).
If `self._persist` is `True`, the persistent cache will be checked and filled after the non-persistent cache.
Notes:
- The persistent cache keeps the
- The persistent cache is fast and has good compatibility (courtesy `msgspec` encoding), but isn't nearly as fast as
Parameters:
bl_instance: The Blender object this prop
"""
if bl_instance is None:
return None
# Create Non-Persistent Cache Entry
## Prefer explicit cache management to 'defaultdict'
if CACHE_NOPERSIST.get(bl_instance.instance_id) is None:
CACHE_NOPERSIST[bl_instance.instance_id] = {}
cache_nopersist = CACHE_NOPERSIST[bl_instance.instance_id]
# Try Hit on Non-Persistent Cache
if (value := cache_nopersist.get(self._bl_prop_name)) is not None:
return value
# Try Hit on Persistent Cache
## Hit: Fill Non-Persistent Cache
if (
self._persist
and (encoded_value := getattr(bl_instance, self._bl_prop_name)) != ''
):
value = decode_any(self._type, encoded_value)
cache_nopersist[self._bl_prop_name] = value
return value
# Compute Value
## Fill Non-Persistent Cache
## Fill Persistent Cache (maybe)
value = self._getter_method(bl_instance)
cache_nopersist[self._bl_prop_name] = value
if self._persist:
setattr(
bl_instance, self._bl_prop_name, ENCODER.encode(value).decode('utf-8')
)
return value
def __set__(self, bl_instance: BLInstance, value: typ.Any) -> None:
"""Runs the user-provided setter, after invalidating the caches.
Notes:
- This invalidates all caches without re-filling them.
- The caches will be re-filled on the first `__get__` invocation, which may be slow due to having to run the getter method.
Parameters:
bl_instance: The Blender object this prop
"""
if self._setter_method is None:
msg = f'Tried to set "{value}" to "{self.prop_name}" on "{bl_instance.bl_label}", but a setter was not defined'
raise NotImplementedError(msg)
# Invalidate Caches
self._invalidate_cache(bl_instance)
# Set the Value
self._setter_method(bl_instance, value)
def setter(self, setter_method: PropSetMethod) -> typ.Self:
"""Decorator to add a setter to the cached property.
Returns:
The same descriptor, so that use of the same method name for defining a setter won't change the semantics of the attribute.
Examples:
Without the decor
```python
class Test(bpy.types.Node):
bl_label = 'Default'
...
def method(self) -> str: return self.bl_label
attr = CachedBLProperty(getter_method=method, persist=False)
@attr.setter
def attr(self, value: str) -> None:
self.bl_label = 'Altered'
```
"""
# Validate Setter Signature
setter_sig = inspect.signature(setter_method)
## Parameter Length
if (sig_len := len(setter_sig.parameters)) != 2: # noqa: PLR2004
msg = f'Setter method for "{self.prop_name}" should have 2 parameters, not "{sig_len}"'
raise TypeError(msg)
## Parameter Value Type
if (sig_ret_type := setter_sig.return_annotation) is not None:
msg = f'Setter method for "{self.prop_name}" return value type "{sig_ret_type}", but it should be "None" (omitting an annotation does not imply "None")'
raise TypeError(msg)
self._setter_method = setter_method
return self
def _invalidate_cache(self, bl_instance: BLInstance) -> None:
"""Invalidates all caches that might be storing the computed property value.
This is invoked by `__set__`.
Note:
Will not delete the `bpy.props.StringProperty`; instead, it will be set to ''.
Parameters:
bl_instance: The instance of the Blender object that contains this property.
Examples:
It is discouraged to run this directly, as any use-pattern that requires manually invalidating a property cache is **likely an anti-pattern**.
With that disclaimer, manual invocation looks like this:
```python
bl_instance.attr._invalidate_cache()
```
"""
# Invalidate Non-Persistent Cache
if CACHE_NOPERSIST.get(bl_instance.instance_id) is not None:
CACHE_NOPERSIST[bl_instance.instance_id].pop(self._bl_prop_name, None)
# Invalidate Persistent Cache
if self._persist and getattr(bl_instance, self._bl_prop_name) != '':
setattr(bl_instance, self._bl_prop_name, '')
## TODO: How do we invalidate the data that the computed cached property depends on?
####################
# - Property Decorators
####################
def cached_bl_property(persist: bool = ...):
"""Decorator creating a descriptor that caches a computed attribute of a Blender node/socket.
Many such `bl_instance`s rely on fast access to computed, cached properties, for example to ensure that `draw()` remains effectively non-blocking.
It is also sometimes desired that this cache persist on `bl_instance`, ex. in the case of loose sockets or cached web data.
Notes:
- Unfortunately, `functools.cached_property` doesn't work, and can't handle persistance.
- Use `cached_attribute` instead if merely persisting the value is desired.
Parameters:
persist: Whether or not to persist the cache value in the Blender object.
This should be used when the **source(s) of the computed value also persists with the Blender object**.
For example, this is especially helpful when caching information for use in `draw()` methods, so that reloading the file won't alter the cache.
Examples:
```python
class CustomNode(bpy.types.Node):
@bl_cache.cached(persist=True|False)
def computed_prop(self) -> ...: return ...
print(bl_instance.prop) ## Computes first time
print(bl_instance.prop) ## Cached (maybe persistently in a property, maybe not)
```
When
"""
def decorator(getter_method: typ.Callable[[BLInstance], None]) -> type:
return CachedBLProperty(getter_method=getter_method, persist=persist)
return decorator
####################
# - Attribute Descriptor
####################
class BLField:
"""A descriptor that allows persisting arbitrary types in Blender objects, with cached reads."""
def __init__(self, default_value: typ.Any, triggers_prop_update: bool = True):
"""Initializes and sets the attribute to a given default value.
Parameters:
default_value: The default value to use if the value is read before it's set.
trigger_prop_update: Whether to run `bl_instance.sync_prop(attr_name)` whenever value is set.
"""
log.debug(
'Initializing BLField (default_value=%s, triggers_prop_update=%s)',
str(default_value),
str(triggers_prop_update),
)
self._default_value: typ.Any = default_value
self._triggers_prop_update: bool = triggers_prop_update
def __set_name__(self, owner: type[BLInstance], name: str) -> None:
"""Sets up getters/setters for attribute access, and sets up a `CachedBLProperty` to internally utilize them.
Our getter/setter essentially reads/writes to a `bpy.props.StringProperty`, with
and use them as user-provided getter/setter to internally define a normal non-persistent `CachedBLProperty`.
As a result, we can reuse almost all of the logic in `CachedBLProperty`
Note:
Run by Python when setting an instance of this class to an attribute.
Parameters:
owner: The class that contains an attribute assigned to an instance of this descriptor.
name: The name of the attribute that an instance of descriptor was assigned to.
"""
# Compute Name and Type of Property
## Also compute the internal
attr_name = name
bl_attr_name = f'blattr__{name}'
if (AttrType := inspect.get_annotations(owner).get(name)) is None: # noqa: N806
msg = f'BLField "{self.prop_name}" must define a type annotation, but doesn\'t.'
raise TypeError(msg)
# Define Blender Property (w/Update Sync)
encoded_default_value = ENCODER.encode(self._default_value).decode('utf-8')
log.debug(
'%s set to StringProperty w/default "%s" and no_update="%s"',
bl_attr_name,
encoded_default_value,
str(not self._triggers_prop_update),
)
owner.set_prop(
bl_attr_name,
bpy.props.StringProperty,
name=f'Encoded Attribute for {attr_name}',
default=encoded_default_value,
no_update=not self._triggers_prop_update,
update_with_name=attr_name,
)
## Getter:
## 1. Initialize bpy.props.StringProperty to Default (if undefined).
## 2. Retrieve bpy.props.StringProperty string.
## 3. Decode using annotated type.
def getter(_self: BLInstance) -> AttrType:
return decode_any(AttrType, getattr(_self, bl_attr_name))
## Setter:
## 1. Initialize bpy.props.StringProperty to Default (if undefined).
## 3. Encode value (implicitly using the annotated type).
## 2. Set bpy.props.StringProperty string.
def setter(_self: BLInstance, value: AttrType) -> None:
encoded_value = ENCODER.encode(value).decode('utf-8')
log.debug(
'Writing BLField attr "%s" w/encoded value: %s',
bl_attr_name,
encoded_value,
)
setattr(_self, bl_attr_name, encoded_value)
# Initialize CachedBLProperty w/Getter and Setter
## This is the usual descriptor assignment procedure.
self._cached_bl_property = CachedBLProperty(getter_method=getter, persist=False)
self._cached_bl_property.__set_name__(owner, name)
self._cached_bl_property.setter(setter)
def __get__(
self, bl_instance: BLInstance | None, owner: type[BLInstance]
) -> typ.Any:
return self._cached_bl_property.__get__(bl_instance, owner)
def __set__(self, bl_instance: BLInstance, value: typ.Any) -> None:
self._cached_bl_property.__set__(bl_instance, value)

View File

@ -220,7 +220,7 @@ def _writable_bl_socket_value(
_bl_socket_value = value _bl_socket_value = value
# Compute Blender Socket Value # Compute Blender Socket Value
if isinstance(_bl_socket_value, sp.Basic): if isinstance(_bl_socket_value, sp.Basic | sp.MatrixBase):
bl_socket_value = spux.sympy_to_python(_bl_socket_value) bl_socket_value = spux.sympy_to_python(_bl_socket_value)
else: else:
bl_socket_value = _bl_socket_value bl_socket_value = _bl_socket_value

View File

@ -1,3 +1,4 @@
import enum
import pydantic as pyd import pydantic as pyd
import typing_extensions as pytypes_ext import typing_extensions as pytypes_ext

View File

@ -1,4 +1,7 @@
import enum import enum
import typing as typ
import typing_extensions as typx
class DataFlowAction(enum.StrEnum): class DataFlowAction(enum.StrEnum):
@ -7,8 +10,37 @@ class DataFlowAction(enum.StrEnum):
DisableLock = 'disable_lock' DisableLock = 'disable_lock'
# Value # Value
OutputRequested = 'output_requested'
DataChanged = 'value_changed' DataChanged = 'value_changed'
# Previewing # Previewing
ShowPreview = 'show_preview' ShowPreview = 'show_preview'
ShowPlot = 'show_plot' ShowPlot = 'show_plot'
def trigger_direction(action: typ.Self) -> typx.Literal['input', 'output']:
"""When a given action is triggered, all sockets/nodes/... in this direction should be recursively triggered.
Parameters:
action: The action for which to retrieve the trigger direction.
Returns:
The trigger direction, which can be used ex. in nodes to select `node.inputs` or `node.outputs`.
"""
return {
DataFlowAction.EnableLock: 'input',
DataFlowAction.DisableLock: 'input',
DataFlowAction.DataChanged: 'output',
DataFlowAction.OutputRequested: 'input',
DataFlowAction.ShowPreview: 'input',
DataFlowAction.ShowPlot: 'input',
}[action]
def stop_if_no_event_methods(action: typ.Self) -> bool:
return {
DataFlowAction.EnableLock: False,
DataFlowAction.DisableLock: False,
DataFlowAction.DataChanged: True,
DataFlowAction.OutputRequested: True,
DataFlowAction.ShowPreview: False,
DataFlowAction.ShowPlot: False,
}[action]

View File

@ -5,7 +5,6 @@ import typing as typ
from types import MappingProxyType from types import MappingProxyType
# import colour ## TODO # import colour ## TODO
import jax
import numpy as np import numpy as np
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
@ -77,6 +76,21 @@ class DataFlowKind(enum.StrEnum):
LazyValueRange = enum.auto() LazyValueRange = enum.auto()
LazyValueSpectrum = enum.auto() LazyValueSpectrum = enum.auto()
@classmethod
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system):
if kind == cls.Value:
return spux.sympy_to_python(
spux.scale_to_unit(
value,
unit_system[socket_type],
)
)
if kind == cls.LazyValueRange:
return value.rescale_to_unit(unit_system[socket_type])
msg = 'Tried to scale unknown kind'
raise ValueError(msg)
#################### ####################
# - Data Structures: Capabilities # - Data Structures: Capabilities

View File

@ -1,3 +1,5 @@
import typing as typ
# from .managed_bl_empty import ManagedBLEmpty # from .managed_bl_empty import ManagedBLEmpty
from .managed_bl_image import ManagedBLImage from .managed_bl_image import ManagedBLImage
@ -8,6 +10,8 @@ from .managed_bl_mesh import ManagedBLMesh
# from .managed_bl_volume import ManagedBLVolume # from .managed_bl_volume import ManagedBLVolume
from .managed_bl_modifier import ManagedBLModifier from .managed_bl_modifier import ManagedBLModifier
ManagedObj: typ.TypeAlias = ManagedBLImage | ManagedBLMesh | ManagedBLModifier
__all__ = [ __all__ = [
#'ManagedBLEmpty', #'ManagedBLEmpty',
'ManagedBLImage', 'ManagedBLImage',
@ -17,3 +21,5 @@ __all__ = [
#'ManagedBLVolume', #'ManagedBLVolume',
'ManagedBLModifier', 'ManagedBLModifier',
] ]
## REMEMBER: Add the appropriate entry to the bl_cache.DECODER

View File

@ -31,6 +31,13 @@ class ManagedBLMesh(ct.schemas.ManagedObj):
'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value 'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value
) )
if self._bl_object_name == value:
## TODO: This is a workaround.
## Really, we can't tell if a name is valid by searching objects.
## Since, after all, other managedobjs may have taken a name..
## ...but not yet made an object that has it.
return
if (bl_object := bpy.data.objects.get(value)) is None: if (bl_object := bpy.data.objects.get(value)) is None:
log.info( log.info(
'Desired BLMesh Name "%s" Not Taken', 'Desired BLMesh Name "%s" Not Taken',

View File

@ -130,6 +130,7 @@ def write_modifier_geonodes(
bl_modifier[iface_id] = float(bl_socket_value) bl_modifier[iface_id] = float(bl_socket_value)
modifier_altered = True modifier_altered = True
else: else:
## TODO: Whitelist what can be here. I'm done with the TypeErrors.
bl_modifier[iface_id] = bl_socket_value bl_modifier[iface_id] = bl_socket_value
modifier_altered = True modifier_altered = True

View File

@ -1,3 +1,4 @@
import contextlib
import typing as typ import typing as typ
import bpy import bpy
@ -15,47 +16,176 @@ MemAddr = int
class DeltaNodeLinkCache(typ.TypedDict): class DeltaNodeLinkCache(typ.TypedDict):
"""Describes change in the `NodeLink`s of a node tree.
Attributes:
added: Set of pointers to added node tree links.
removed: Set of pointers to removed node tree links.
"""
added: set[MemAddr] added: set[MemAddr]
removed: set[MemAddr] removed: set[MemAddr]
class NodeLinkCache: class NodeLinkCache:
"""A pointer-based cache of node links in a node tree.
Attributes:
_node_tree: Reference to the owning node tree.
link_ptrs_as_links:
link_ptrs: Pointers (as in integer memory adresses) to `NodeLink`s.
link_ptrs_as_links: Map from pointers to actual `NodeLink`s.
link_ptrs_from_sockets: Map from pointers to `NodeSocket`s, representing the source of each `NodeLink`.
link_ptrs_from_sockets: Map from pointers to `NodeSocket`s, representing the destination of each `NodeLink`.
"""
def __init__(self, node_tree: bpy.types.NodeTree): def __init__(self, node_tree: bpy.types.NodeTree):
# Initialize Parameters """Initialize the cache from a node tree.
Parameters:
node_tree: The Blender node tree whose `NodeLink`s will be cached.
"""
self._node_tree = node_tree self._node_tree = node_tree
self.link_ptrs_to_links = {}
self.link_ptrs = set() # Link PTR and PTR->REF
self.link_ptrs_from_sockets = {} self.link_ptrs: set[MemAddr] = set()
self.link_ptrs_to_sockets = {} self.link_ptrs_as_links: dict[MemAddr, bpy.types.NodeLink] = {}
# Socket PTR and PTR->REF
self.socket_ptrs: set[MemAddr] = set()
self.socket_ptrs_as_sockets: dict[MemAddr, bpy.types.NodeSocket] = {}
self.socket_ptr_refcount: dict[MemAddr, int] = {}
# Link PTR -> Socket PTR
self.link_ptrs_as_from_socket_ptrs: dict[MemAddr, MemAddr] = {}
self.link_ptrs_as_to_socket_ptrs: dict[MemAddr, MemAddr] = {}
# Fill Cache # Fill Cache
self.regenerate() self.regenerate()
def remove(self, link_ptrs: set[MemAddr]) -> None: def remove_link(self, link_ptr: MemAddr) -> None:
for link_ptr in link_ptrs: """Removes a link pointer from the cache, indicating that the link doesn't exist anymore.
Notes:
- **DOES NOT** remove PTR->REF dictionary entries
- Invoking this method directly causes the removed node links to not be reported as "removed" by `NodeLinkCache.regenerate()`.
- This **must** be done whenever a node link is deleted.
- Failure to do so may result in a segmentation fault at arbitrary future time.
Parameters:
link_ptrs: Pointers to remove from the cache.
"""
self.link_ptrs.remove(link_ptr) self.link_ptrs.remove(link_ptr)
self.link_ptrs_to_links.pop(link_ptr, None) self.link_ptrs_as_links.pop(link_ptr)
def remove_sockets_by_link_ptr(self, link_ptr: MemAddr) -> None:
"""Removes a single pointer's reference to its from/to sockets."""
from_socket_ptr = self.link_ptrs_as_from_socket_ptrs.pop(link_ptr, None)
to_socket_ptr = self.link_ptrs_as_to_socket_ptrs.pop(link_ptr, None)
for socket_ptr in [from_socket_ptr, to_socket_ptr]:
if socket_ptr is None:
continue
# Delete w/RefCount Respect
if self.socket_ptr_refcount[socket_ptr] == 1:
self.socket_ptrs.remove(socket_ptr)
self.socket_ptrs_as_sockets.pop(socket_ptr)
self.socket_ptr_refcount.pop(socket_ptr)
else:
self.socket_ptr_refcount[socket_ptr] -= 1
def regenerate(self) -> DeltaNodeLinkCache: def regenerate(self) -> DeltaNodeLinkCache:
current_link_ptrs_to_links = { """Regenerates the cache from the internally-linked node tree.
Notes:
- This is designed to run within the `update()` invocation of the node tree.
- This should be a very fast function, since it is called so much.
"""
# Compute All NodeLink Pointers
all_link_ptrs_as_links = {
link.as_pointer(): link for link in self._node_tree.links link.as_pointer(): link for link in self._node_tree.links
} }
current_link_ptrs = set(current_link_ptrs_to_links.keys()) all_link_ptrs = set(all_link_ptrs_as_links.keys())
# Compute Delta # Compute Added/Removed Links
added_link_ptrs = current_link_ptrs - self.link_ptrs added_link_ptrs = all_link_ptrs - self.link_ptrs
removed_link_ptrs = self.link_ptrs - current_link_ptrs removed_link_ptrs = self.link_ptrs - all_link_ptrs
# Update Caches Incrementally # Edge Case: 'from_socket' Reassignment
self.remove(removed_link_ptrs) ## (Reverse engineered) When all:
## - Created a new link between the same two nodes.
## - Matching 'to_socket'.
## - Non-matching 'from_socket' on the same node.
## -> THEN the link_ptr will not change, but the from_socket ptr should.
if len(added_link_ptrs) == 0 and len(removed_link_ptrs) == 0:
# Find the Link w/Reassigned 'from_socket' PTR
## A bit of a performance hit from the search, but it's an edge case.
_link_ptr_as_from_socket_ptrs = {
link_ptr: (
from_socket_ptr,
all_link_ptrs_as_links[link_ptr].from_socket.as_pointer(),
)
for link_ptr, from_socket_ptr in self.link_ptrs_as_from_socket_ptrs.items()
if all_link_ptrs_as_links[link_ptr].from_socket.as_pointer()
!= from_socket_ptr
}
# Completely Remove the Old Link (w/Reassigned 'from_socket')
## This effectively reclassifies the edge case as a normal 're-add'.
for link_ptr in _link_ptr_as_from_socket_ptrs:
log.info(
'Edge-Case - "from_socket" Reassigned in NodeLink w/o New NodeLink Pointer: %s',
link_ptr,
)
self.remove_link(link_ptr)
self.remove_sockets_by_link_ptr(link_ptr)
# Recompute Added/Removed Links
## The algorithm will now detect an "added link".
added_link_ptrs = all_link_ptrs - self.link_ptrs
removed_link_ptrs = self.link_ptrs - all_link_ptrs
# Shuffle Cache based on Change in Links
## Remove Entries for Removed Pointers
for removed_link_ptr in removed_link_ptrs:
self.remove_link(removed_link_ptr)
## User must manually call 'remove_socket_by_link_ptr' later.
## For now, leave dangling socket information by-link.
# Add New Link Pointers
self.link_ptrs |= added_link_ptrs self.link_ptrs |= added_link_ptrs
for link_ptr in added_link_ptrs: for link_ptr in added_link_ptrs:
link = current_link_ptrs_to_links[link_ptr] # Add Link PTR->REF
new_link = all_link_ptrs_as_links[link_ptr]
self.link_ptrs_as_links[link_ptr] = new_link
self.link_ptrs_to_links[link_ptr] = link # Retrieve Link Socket Information
self.link_ptrs_from_sockets[link_ptr] = link.from_socket from_socket = new_link.from_socket
self.link_ptrs_to_sockets[link_ptr] = link.to_socket from_socket_ptr = from_socket.as_pointer()
to_socket = new_link.to_socket
to_socket_ptr = to_socket.as_pointer()
# Add Socket PTR, PTR -> REF
for socket_ptr, bl_socket in zip( # noqa: B905
[from_socket_ptr, to_socket_ptr],
[from_socket, to_socket],
):
# Increment RefCount of Socket PTR
## This happens if another link also uses the same socket.
## 1. An output socket links to several inputs.
## 2. A multi-input socket links from several inputs.
if socket_ptr in self.socket_ptr_refcount:
self.socket_ptr_refcount[socket_ptr] += 1
else:
## RefCount == 0: Add PTR, PTR -> REF
self.socket_ptrs.add(socket_ptr)
self.socket_ptrs_as_sockets[socket_ptr] = bl_socket
self.socket_ptr_refcount[socket_ptr] = 1
# Add Link PTR -> Socket PTR
self.link_ptrs_as_from_socket_ptrs[link_ptr] = from_socket_ptr
self.link_ptrs_as_to_socket_ptrs[link_ptr] = to_socket_ptr
return {'added': added_link_ptrs, 'removed': removed_link_ptrs} return {'added': added_link_ptrs, 'removed': removed_link_ptrs}
@ -71,20 +201,42 @@ class MaxwellSimTree(bpy.types.NodeTree):
#################### ####################
# - Lock Methods # - Lock Methods
#################### ####################
def unlock_all(self): def unlock_all(self) -> None:
"""Unlock all nodes in the node tree, making them editable."""
log.info('Unlocking All Nodes in NodeTree "%s"', self.bl_label)
for node in self.nodes: for node in self.nodes:
node.locked = False node.locked = False
for bl_socket in [*node.inputs, *node.outputs]: for bl_socket in [*node.inputs, *node.outputs]:
bl_socket.locked = False bl_socket.locked = False
def unpreview_all(self): @contextlib.contextmanager
log.info('Disabling All 3D Previews') def repreview_all(self) -> None:
for node in self.nodes: all_nodes_with_preview_active = {
if node.preview_active: node.instance_id: node for node in self.nodes if node.preview_active
node.preview_active = False }
self.is_currently_repreviewing = True
self.newly_previewed_nodes = {}
for bl_object in preview_collection().objects.values(): try:
preview_collection().objects.unlink(bl_object) yield
finally:
for dangling_previewed_node in [
node
for node_instance_id, node in all_nodes_with_preview_active.items()
if node_instance_id not in self.newly_previewed_nodes
]:
# log.debug(
# 'Removing Dangling Preview of Node "{%s}"',
# str(dangling_previewed_node),
# )
dangling_previewed_node.preview_active = False
def report_show_preview(self, node: bpy.types.Node) -> None:
if (
hasattr(self, 'is_currently_repreviewing')
and self.is_currently_repreviewing
):
self.newly_previewed_nodes[node.instance_id] = node
#################### ####################
# - Init Methods # - Init Methods
@ -94,11 +246,10 @@ class MaxwellSimTree(bpy.types.NodeTree):
It's a bit of a "fake" function - in practicality, it's triggered on the first update() function. It's a bit of a "fake" function - in practicality, it's triggered on the first update() function.
""" """
## TODO: Consider tying this to an "on_load" handler if hasattr(self, 'node_link_cache'):
if hasattr(self, '_node_link_cache'): self.node_link_cache.regenerate()
self._node_link_cache.regenerate()
else: else:
self._node_link_cache = NodeLinkCache(self) self.node_link_cache = NodeLinkCache(self)
#################### ####################
# - Update Methods # - Update Methods
@ -106,23 +257,35 @@ class MaxwellSimTree(bpy.types.NodeTree):
def sync_node_removed(self, node: bpy.types.Node): def sync_node_removed(self, node: bpy.types.Node):
"""Run by `Node.free()` when a node is being removed. """Run by `Node.free()` when a node is being removed.
ONLY input socket links are removed from the NodeLink cache.
- `self.update()` handles link-removal from existing nodes.
- `self.update()` can't handle link-removal
Removes node input links from the internal cache (so we don't attempt to update non-existant sockets). Removes node input links from the internal cache (so we don't attempt to update non-existant sockets).
""" """
for bl_socket in node.inputs.values():
# Retrieve Socket Links (if any)
self._node_link_cache.remove(
{link.as_pointer() for link in bl_socket.links}
)
## ONLY Input Socket Links are Removed from the NodeLink Cache ## ONLY Input Socket Links are Removed from the NodeLink Cache
## - update() handles link-removal from still-existing node just fine. ## - update() handles link-removal from still-existing node just fine.
## - update() does NOT handle link-removal of non-existant nodes. ## - update() does NOT handle link-removal of non-existant nodes.
for bl_socket in list(node.inputs.values()) + list(node.outputs.values()):
# Compute About-To-Be-Freed Link Ptrs
link_ptrs = {link.as_pointer() for link in bl_socket.links}
def update(self): if link_ptrs:
"""Run by Blender when 'something changes' in the node tree. for link_ptr in link_ptrs:
self.node_link_cache.remove_link(link_ptr)
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
Updates an internal node link cache, then updates sockets that just lost/gained an input link. def update(self) -> None:
"""Monitors all changes to the node tree, potentially responding with appropriate callbacks.
Notes:
- Run by Blender when "anything" changes in the node tree.
- Responds to node link changes with callbacks, with the help of a performant node link cache.
""" """
if not hasattr(self, '_node_link_cache'): if not hasattr(self, 'ignore_update'):
self.ignore_update = False
if not hasattr(self, 'node_link_cache'):
self.on_load() self.on_load()
## We presume update() is run before the first link is altered. ## We presume update() is run before the first link is altered.
## - Else, the first link of the session will not update caches. ## - Else, the first link of the session will not update caches.
@ -130,51 +293,82 @@ class MaxwellSimTree(bpy.types.NodeTree):
## - Therefore, self.on_load() is also called as a load_post handler. ## - Therefore, self.on_load() is also called as a load_post handler.
return return
# Compute Changes to NodeLink Cache # Ignore Update
delta_links = self._node_link_cache.regenerate() ## Manually set to implement link corrections w/o recursion.
if self.ignore_update:
return
link_alterations = { # Compute Changes to Node Links
delta_links = self.node_link_cache.regenerate()
link_corrections = {
'to_remove': [], 'to_remove': [],
'to_add': [], 'to_add': [],
} }
for link_ptr in delta_links['removed']: for link_ptr in delta_links['removed']:
from_socket = self._node_link_cache.link_ptrs_from_sockets[link_ptr] # Retrieve Link PTR -> From/To Socket PTR
to_socket = self._node_link_cache.link_ptrs_to_sockets[link_ptr] ## We don't know if they exist yet.
from_socket_ptr = self.node_link_cache.link_ptrs_as_from_socket_ptrs[
link_ptr
]
to_socket_ptr = self.node_link_cache.link_ptrs_as_to_socket_ptrs[link_ptr]
# Update Socket Caches # Check Existance of From/To Socket
self._node_link_cache.link_ptrs_from_sockets.pop(link_ptr, None) ## `Node.free()` must report removed sockets, so this here works.
self._node_link_cache.link_ptrs_to_sockets.pop(link_ptr, None) ## If Both Exist: 'to_socket' may "non-consent" to the link removal.
if (
from_socket_ptr in self.node_link_cache.socket_ptrs
and to_socket_ptr in self.node_link_cache.socket_ptrs
):
# Retrieve 'from_socket'/'to_socket' REF
from_socket = self.node_link_cache.socket_ptrs_as_sockets[
from_socket_ptr
]
to_socket = self.node_link_cache.socket_ptrs_as_sockets[to_socket_ptr]
# Trigger Report Chain on Socket that Just Lost a Link # Ask 'to_socket' for Consent to Remove Link
## Aka. Forward-Refresh Caches Relying on Linkage ## The link has already been removed, but we can fix that.
if not (consent_removal := to_socket.sync_link_removed(from_socket)): ## If NO: Queue re-adding the link (safe since the sockets exist)
# Did Not Consent to Removal: Queue Add Link ## TODO: Crash if deleting removing linked loose sockets.
link_alterations['to_add'].append((from_socket, to_socket)) consent_removal = to_socket.sync_link_removed(from_socket)
if not consent_removal:
link_corrections['to_add'].append((from_socket, to_socket))
# Ensure Removal of Socket PTRs, PTRs->REFs
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
for link_ptr in delta_links['added']: for link_ptr in delta_links['added']:
link = self._node_link_cache.link_ptrs_to_links.get(link_ptr) # Retrieve Link Reference
if link is None: link = self.node_link_cache.link_ptrs_as_links[link_ptr]
continue
# Trigger Report Chain on Socket that Just Gained a Link # Ask 'to_socket' for Consent to Remove Link
## Aka. Forward-Refresh Caches Relying on Linkage ## The link has already been added, but we can fix that.
## If NO: Queue re-adding the link (safe since the sockets exist)
consent_added = link.to_socket.sync_link_added(link)
if not consent_added:
link_corrections['to_remove'].append(link)
if not (consent_added := link.to_socket.sync_link_added(link)): # Link Corrections
# Did Not Consent to Addition: Queue Remove Link ## ADD: Links that 'to_socket' don't want removed.
link_alterations['to_remove'].append(link) ## REMOVE: Links that 'to_socket' don't want added.
## NOTE: Both remove() and new() recursively triggers update().
# Execute Queued Operations for link in link_corrections['to_remove']:
## - Especially undoing undesirable link changes. self.ignore_update = True
## - This is important for locked graphs, whose links must not change. self.links.remove(link) ## Recursively triggers update()
for link in link_alterations['to_remove']: self.ignore_update = False
self.links.remove(link) for from_socket, to_socket in link_corrections['to_add']:
for from_socket, to_socket in link_alterations['to_add']: ## 'to_socket' and 'from_socket' are guaranteed to exist.
self.ignore_update = True
self.links.new(from_socket, to_socket) self.links.new(from_socket, to_socket)
self.ignore_update = False
# If Queued Operations: Regenerate Cache # Regenerate on Corrections
## - This prevents the next update() from picking up on alterations. ## Prevents next update() from trying to correct the corrections.
if link_alterations['to_remove'] or link_alterations['to_add']: ## We must remember to trigger '.remove_sockets_by_link_ptr'
self._node_link_cache.regenerate() if link_corrections['to_remove'] or link_corrections['to_add']:
delta_links = self.node_link_cache.regenerate()
for link_ptr in delta_links['removed']:
self.node_link_cache.remove_sockets_by_link_ptr(link_ptr)
#################### ####################

View File

@ -13,14 +13,15 @@ CACHE_SIM_DATA = {}
class ExtractDataNode(base.MaxwellSimNode): class ExtractDataNode(base.MaxwellSimNode):
"""Node for visualizing simulation data, by querying its monitors.""" """Node for extracting data from other objects."""
node_type = ct.NodeType.ExtractData node_type = ct.NodeType.ExtractData
bl_label = 'Extract Data' bl_label = 'Extract'
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
'Sim Data': {'Sim Data': sockets.MaxwellFDTDSimDataSocketDef()}, 'Sim Data': {'Sim Data': sockets.MaxwellFDTDSimDataSocketDef()},
'Field Data': {'Field Data': sockets.AnySocketDef()}, 'Field Data': {'Field Data': sockets.AnySocketDef()},
'Flux Data': {'Flux Data': sockets.AnySocketDef()},
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(), 'Data': sockets.AnySocketDef(),
@ -192,6 +193,20 @@ class ExtractDataNode(base.MaxwellSimNode):
elif not self.inputs['Field Data'].is_linked and self.cache__components: elif not self.inputs['Field Data'].is_linked and self.cache__components:
self.cache__components = '' self.cache__components = ''
####################
# - Flux Data
####################
def draw_props__flux_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
pass
def draw_info__flux_data(
self, _: bpy.types.Context, col: bpy.types.UILayout
) -> None:
pass
#################### ####################
# - Global # - Global
#################### ####################
@ -200,12 +215,16 @@ class ExtractDataNode(base.MaxwellSimNode):
self.draw_props__sim_data(context, col) self.draw_props__sim_data(context, col)
if self.active_socket_set == 'Field Data': if self.active_socket_set == 'Field Data':
self.draw_props__field_data(context, col) self.draw_props__field_data(context, col)
if self.active_socket_set == 'Flux Data':
self.draw_props__flux_data(context, col)
def draw_info(self, context: bpy.types.Context, col: bpy.types.UILayout) -> None: def draw_info(self, context: bpy.types.Context, col: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Sim Data': if self.active_socket_set == 'Sim Data':
self.draw_info__sim_data(context, col) self.draw_info__sim_data(context, col)
if self.active_socket_set == 'Field Data': if self.active_socket_set == 'Field Data':
self.draw_info__field_data(context, col) self.draw_info__field_data(context, col)
if self.active_socket_set == 'Flux Data':
self.draw_info__flux_data(context, col)
@events.computes_output_socket( @events.computes_output_socket(
'Data', 'Data',
@ -226,6 +245,10 @@ class ExtractDataNode(base.MaxwellSimNode):
field_data = self._compute_input('Field Data') field_data = self._compute_input('Field Data')
return getattr(field_data, props['field_data__component']) return getattr(field_data, props['field_data__component'])
elif self.active_socket_set == 'Flux Data': # noqa: RET505
flux_data = self._compute_input('Flux Data')
return getattr(flux_data, 'flux')
msg = f'Tried to get data from unknown output socket in "{self.bl_label}"' msg = f'Tried to get data from unknown output socket in "{self.bl_label}"'
raise RuntimeError(msg) raise RuntimeError(msg)

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
import enum import dataclasses
import inspect import inspect
import typing as typ import typing as typ
from types import MappingProxyType from types import MappingProxyType
from ....utils import extra_sympy_units as spux
from ....utils import logger from ....utils import logger
from .. import contracts as ct from .. import contracts as ct
from .base import MaxwellSimNode
log = logger.get(__name__) log = logger.get(__name__)
@ -14,50 +12,35 @@ UnitSystemID = str
UnitSystem = dict[ct.SocketType, typ.Any] UnitSystem = dict[ct.SocketType, typ.Any]
class EventCallbackType(enum.StrEnum):
"""Names of actions that support callbacks."""
computes_output_socket = enum.auto()
on_value_changed = enum.auto()
on_show_plot = enum.auto()
on_init = enum.auto()
#################### ####################
# - Event Callback Information # - Event Callback Information
#################### ####################
class EventCallbackData_ComputesOutputSocket(typ.TypedDict): # noqa: N801 @dataclasses.dataclass(kw_only=True, frozen=True)
"""Extra data used to select a method to compute output sockets.""" class InfoDataChanged:
run_on_init: bool
on_changed_sockets: set[ct.SocketName]
on_changed_props: set[str]
on_any_changed_loose_input: set[str]
@dataclasses.dataclass(kw_only=True, frozen=True)
class InfoOutputRequested:
output_socket_name: ct.SocketName output_socket_name: ct.SocketName
any_loose_output_socket: bool any_loose_output_socket: bool
kind: ct.DataFlowKind kind: ct.DataFlowKind
depon_props: set[str]
class EventCallbackData_OnValueChanged(typ.TypedDict): # noqa: N801 depon_input_sockets: set[ct.SocketName]
"""Extra data used to select a method to compute output sockets.""" depon_input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind]
depon_all_loose_input_sockets: bool
changed_sockets: set[ct.SocketName] depon_output_sockets: set[ct.SocketName]
changed_props: set[str] depon_output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind]
changed_loose_input: set[str] depon_all_loose_output_sockets: bool
class EventCallbackData_OnShowPlot(typ.TypedDict): # noqa: N801 EventCallbackInfo: typ.TypeAlias = InfoDataChanged | InfoOutputRequested
"""Extra data in the callback, used when showing a plot."""
stop_propagation: bool
class EventCallbackData_OnInit(typ.TypedDict): # noqa: D101, N801
pass
EventCallbackData: typ.TypeAlias = (
EventCallbackData_ComputesOutputSocket
| EventCallbackData_OnValueChanged
| EventCallbackData_OnShowPlot
| EventCallbackData_OnInit
)
#################### ####################
@ -68,16 +51,21 @@ PropName: typ.TypeAlias = str
def event_decorator( def event_decorator(
action_type: EventCallbackType, action_type: ct.DataFlowAction,
extra_data: EventCallbackData, callback_info: EventCallbackInfo | None,
props: set[PropName] = frozenset(), stop_propagation: bool = False,
# Request Data for Callback
managed_objs: set[ManagedObjName] = frozenset(), managed_objs: set[ManagedObjName] = frozenset(),
props: set[PropName] = frozenset(),
input_sockets: set[ct.SocketName] = frozenset(), input_sockets: set[ct.SocketName] = frozenset(),
input_sockets_optional: dict[ct.SocketName, bool] = MappingProxyType({}),
input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}), input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
output_sockets: set[ct.SocketName] = frozenset(), output_sockets: set[ct.SocketName] = frozenset(),
output_sockets_optional: dict[ct.SocketName, bool] = MappingProxyType({}),
output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}), output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
all_loose_input_sockets: bool = False, all_loose_input_sockets: bool = False,
all_loose_output_sockets: bool = False, all_loose_output_sockets: bool = False,
# Request Unit System Scaling
unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}), unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}),
scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}), scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}), scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
@ -87,9 +75,11 @@ def event_decorator(
Parameters: Parameters:
action_type: A name describing which event the decorator should respond to. action_type: A name describing which event the decorator should respond to.
Set to `return_method.action_type` Set to `return_method.action_type`
extra_data: A dictionary that provides the caller with additional per-`action_type` information. callback_info: A dictionary that provides the caller with additional per-`action_type` information.
This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback. This might include parameters to help select the most appropriate method(s) to respond to an event with, or actions to take after running the callback.
props: Set of `props` to compute, then pass to the decorated method. props: Set of `props` to compute, then pass to the decorated method.
stop_propagation: Whether or stop propagating the event through the graph after encountering this method.
Other methods defined on the same node will still run.
managed_objs: Set of `managed_objs` to retrieve, then pass to the decorated method. managed_objs: Set of `managed_objs` to retrieve, then pass to the decorated method.
input_sockets: Set of `input_sockets` to compute, then pass to the decorated method. input_sockets: Set of `input_sockets` to compute, then pass to the decorated method.
input_socket_kinds: The `ct.DataFlowKind` to compute per-input-socket. input_socket_kinds: The `ct.DataFlowKind` to compute per-input-socket.
@ -104,7 +94,7 @@ def event_decorator(
A decorator, which can be applied to a method of `MaxwellSimNode`. A decorator, which can be applied to a method of `MaxwellSimNode`.
When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on. When a `MaxwellSimNode` subclass initializes, such a decorated method will be picked up on.
When the `action_type` action passes through the node, then `extra_data` is used to determine When the `action_type` action passes through the node, then `callback_info` is used to determine
""" """
req_params = ( req_params = (
{'self'} {'self'}
@ -119,6 +109,8 @@ def event_decorator(
# TODO: Check that all Unit System IDs referenced are also defined in 'unit_systems'. # TODO: Check that all Unit System IDs referenced are also defined in 'unit_systems'.
## TODO: More ex. introspective checks and such, to make it really hard to write invalid methods. ## TODO: More ex. introspective checks and such, to make it really hard to write invalid methods.
# TODO: Check Function Annotation Validity
## - socket capabilities
def decorator(method: typ.Callable) -> typ.Callable: def decorator(method: typ.Callable) -> typ.Callable:
# Check Function Signature Validity # Check Function Signature Validity
@ -133,127 +125,126 @@ def event_decorator(
msg = f'Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}' msg = f'Decorated method {method.__name__} has superfluous arguments {func_sig - req_params}'
raise ValueError(msg) raise ValueError(msg)
# TODO: Check Function Annotation Validity def decorated(node):
## - socket capabilities
def decorated(node: MaxwellSimNode):
method_kw_args = {} ## Keyword Arguments for Decorated Method method_kw_args = {} ## Keyword Arguments for Decorated Method
# Compute Requested Props # Unit Systems
if props: method_kw_args |= {'unit_systems': unit_systems} if unit_systems else {}
_props = {prop_name: getattr(node, prop_name) for prop_name in props}
method_kw_args |= {'props': _props}
# Retrieve Requested Managed Objects # Properties
if managed_objs: method_kw_args |= (
_managed_objs = { {'props': {prop_name: getattr(node, prop_name) for prop_name in props}}
if props
else {}
)
# Managed Objects
method_kw_args |= (
{
'managed_objs': {
managed_obj_name: node.managed_objs[managed_obj_name] managed_obj_name: node.managed_objs[managed_obj_name]
for managed_obj_name in managed_objs for managed_obj_name in managed_objs
} }
method_kw_args |= {'managed_objs': _managed_objs} }
if managed_objs
else {}
)
# Requested Sockets # Sockets
## Compute Requested Input Sockets ## Input Sockets
if input_sockets: method_kw_args |= (
_input_sockets = { {
'input_sockets': {
input_socket_name: node._compute_input( input_socket_name: node._compute_input(
input_socket_name, input_socket_name,
kind=input_socket_kinds.get( kind=input_socket_kinds.get(
input_socket_name, ct.DataFlowKind.Value input_socket_name, ct.DataFlowKind.Value
), ),
unit_system=(
unit_system := unit_systems.get(
scale_input_sockets.get(input_socket_name)
)
),
optional=input_sockets_optional.get(
input_socket_name, False
),
) )
for input_socket_name in input_sockets for input_socket_name in input_sockets
} }
}
# Scale Specified Input Sockets to Unit System if input_sockets
## First, scale the input socket value to the given unit system else {}
## Then, convert the symbol-less sympy scalar to a python type.
for input_socket_name, unit_system_id in scale_input_sockets.items():
unit_system = unit_systems[unit_system_id]
kind = input_socket_kinds.get(
input_socket_name, ct.DataFlowKind.Value
) )
if kind == ct.DataFlowKind.Value: ## Output Sockets
_input_sockets[input_socket_name] = spux.sympy_to_python( method_kw_args |= (
spux.scale_to_unit( {
_input_sockets[input_socket_name], 'output_sockets': {
unit_system[node.inputs[input_socket_name].socket_type], output_socket_name: ct.DataFlowKind.scale_to_unit_system(
(
output_socket_kind := output_socket_kinds.get(
output_socket_name, ct.DataFlowKind.Value
) )
),
node.compute_output(
output_socket_name,
kind=output_socket_kind,
optional=output_sockets_optional.get(
output_socket_name, False
),
),
node.outputs[output_socket_name].socket_type,
unit_systems.get(
scale_output_sockets.get(output_socket_name)
),
) )
elif kind == ct.DataFlowKind.LazyValueRange: if scale_output_sockets.get(output_socket_name) is not None
_input_sockets[input_socket_name] = _input_sockets[ else node.compute_output(
input_socket_name
].rescale_to_unit(
unit_system[node.inputs[input_socket_name].socket_type]
)
method_kw_args |= {'input_sockets': _input_sockets}
## Compute Requested Output Sockets
if output_sockets:
_output_sockets = {
output_socket_name: node.compute_output(
output_socket_name, output_socket_name,
kind=output_socket_kinds.get( kind=output_socket_kinds.get(
output_socket_name, ct.DataFlowKind.Value output_socket_name, ct.DataFlowKind.Value
), ),
optional=output_sockets_optional.get(
output_socket_name, False
),
) )
for output_socket_name in output_sockets for output_socket_name in output_sockets
} }
}
# Scale Specified Output Sockets to Unit System if output_sockets
## First, scale the output socket value to the given unit system else {}
## Then, convert the symbol-less sympy scalar to a python type.
for output_socket_name, unit_system_id in scale_output_sockets.items():
unit_system = unit_systems[unit_system_id]
kind = input_socket_kinds.get(
input_socket_name, ct.DataFlowKind.Value
) )
if kind == ct.DataFlowKind.Value:
_output_sockets[output_socket_name] = spux.sympy_to_python(
spux.scale_to_unit(
_output_sockets[output_socket_name],
unit_system[
node.outputs[output_socket_name].socket_type
],
)
)
elif kind == ct.DataFlowKind.LazyValueRange:
_output_sockets[output_socket_name] = _output_sockets[
output_socket_name
].rescale_to_unit(
unit_system[node.outputs[output_socket_name].socket_type]
)
method_kw_args |= {'output_sockets': _output_sockets}
# Loose Sockets # Loose Sockets
## Compute All Loose Input Sockets ## Compute All Loose Input Sockets
if all_loose_input_sockets: method_kw_args |= (
_loose_input_sockets = { {
'loose_input_sockets': {
input_socket_name: node._compute_input( input_socket_name: node._compute_input(
input_socket_name, input_socket_name,
kind=node.inputs[input_socket_name].active_kind, kind=node.inputs[input_socket_name].active_kind,
) )
for input_socket_name in node.loose_input_sockets for input_socket_name in node.loose_input_sockets
} }
method_kw_args |= {'loose_input_sockets': _loose_input_sockets} }
if all_loose_input_sockets
else {}
)
## Compute All Loose Output Sockets ## Compute All Loose Output Sockets
if all_loose_output_sockets: method_kw_args |= (
_loose_output_sockets = { {
'loose_output_sockets': {
output_socket_name: node.compute_output( output_socket_name: node.compute_output(
output_socket_name, output_socket_name,
kind=node.outputs[output_socket_name].active_kind, kind=node.outputs[output_socket_name].active_kind,
) )
for output_socket_name in node.loose_output_sockets for output_socket_name in node.loose_output_sockets
} }
method_kw_args |= {'loose_output_sockets': _loose_output_sockets} }
if all_loose_output_sockets
# Unit Systems else {}
if unit_systems: )
method_kw_args |= {'unit_systems': unit_systems}
# Call Method # Call Method
return method( return method(
@ -270,7 +261,8 @@ def event_decorator(
## Add Spice ## Add Spice
decorated.action_type = action_type decorated.action_type = action_type
decorated.extra_data = extra_data decorated.callback_info = callback_info
decorated.stop_propagation = stop_propagation
return decorated return decorated
@ -280,19 +272,22 @@ def event_decorator(
#################### ####################
# - Simplified Event Callbacks # - Simplified Event Callbacks
#################### ####################
def computes_output_socket( def on_enable_lock(
output_socket_name: ct.SocketName | None,
any_loose_output_socket: bool = False,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
**kwargs, **kwargs,
): ):
return event_decorator( return event_decorator(
action_type='computes_output_socket', action_type=ct.DataFlowAction.EnableLock,
extra_data={ callback_info=None,
'output_socket_name': output_socket_name, **kwargs,
'any_loose_output_socket': any_loose_output_socket, )
'kind': kind,
},
def on_disable_lock(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.DisableLock,
callback_info=None,
**kwargs, **kwargs,
) )
@ -302,37 +297,67 @@ def on_value_changed(
socket_name: set[ct.SocketName] | ct.SocketName | None = None, socket_name: set[ct.SocketName] | ct.SocketName | None = None,
prop_name: set[str] | str | None = None, prop_name: set[str] | str | None = None,
any_loose_input_socket: bool = False, any_loose_input_socket: bool = False,
run_on_init: bool = False,
**kwargs, **kwargs,
): ):
return event_decorator( return event_decorator(
action_type=EventCallbackType.on_value_changed, action_type=ct.DataFlowAction.DataChanged,
extra_data={ callback_info=InfoDataChanged(
'changed_sockets': ( run_on_init=run_on_init,
on_changed_sockets=(
socket_name if isinstance(socket_name, set) else {socket_name} socket_name if isinstance(socket_name, set) else {socket_name}
), ),
'changed_props': (prop_name if isinstance(prop_name, set) else {prop_name}), on_changed_props=(prop_name if isinstance(prop_name, set) else {prop_name}),
'changed_loose_input': any_loose_input_socket, on_any_changed_loose_input=any_loose_input_socket,
}, ),
**kwargs,
)
## TODO: Change name to 'on_output_requested'
def computes_output_socket(
output_socket_name: ct.SocketName | None,
any_loose_output_socket: bool = False,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.OutputRequested,
callback_info=InfoOutputRequested(
output_socket_name=output_socket_name,
any_loose_output_socket=any_loose_output_socket,
kind=kind,
depon_props=kwargs.get('props', set()),
depon_input_sockets=kwargs.get('input_sockets', set()),
depon_input_socket_kinds=kwargs.get('input_socket_kinds', set()),
depon_output_sockets=kwargs.get('output_sockets', set()),
depon_output_socket_kinds=kwargs.get('output_socket_kinds', set()),
depon_all_loose_input_sockets=kwargs.get('all_loose_input_sockets', set()),
depon_all_loose_output_sockets=kwargs.get(
'all_loose_output_sockets', set()
),
),
**kwargs, ## stop_propagation has no effect.
)
def on_show_preview(
**kwargs,
):
return event_decorator(
action_type=ct.DataFlowAction.ShowPreview,
callback_info={},
**kwargs, **kwargs,
) )
def on_show_plot( def on_show_plot(
stop_propagation: bool = False, stop_propagation: bool = True,
**kwargs, **kwargs,
): ):
return event_decorator( return event_decorator(
action_type=EventCallbackType.on_show_plot, action_type=ct.DataFlowAction.ShowPlot,
extra_data={ callback_info={},
'stop_propagation': stop_propagation, stop_propagation=stop_propagation,
},
**kwargs,
)
def on_init(**kwargs):
return event_decorator(
action_type=EventCallbackType.on_init,
extra_data={},
**kwargs, **kwargs,
) )

View File

@ -57,6 +57,7 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
#################### ####################
# - Properties # - Properties
#################### ####################
## TODO: More automatic determination of which file type is in use :)
tidy3d_type: bpy.props.EnumProperty( tidy3d_type: bpy.props.EnumProperty(
name='Tidy3D Type', name='Tidy3D Type',
description='Type of Tidy3D object to load', description='Type of Tidy3D object to load',
@ -228,7 +229,6 @@ class Tidy3DFileImporterNode(base.MaxwellSimNode):
disp_fitter = CACHE[self.bl_label]['fitter'] disp_fitter = CACHE[self.bl_label]['fitter']
# Plot # Plot
log.debug(disp_fitter)
managed_objs['plot'].mpl_plot_to_image( managed_objs['plot'].mpl_plot_to_image(
lambda ax: disp_fitter.plot( lambda ax: disp_fitter.plot(
medium=model_medium, medium=model_medium,

View File

@ -5,11 +5,14 @@ import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import logger
from .....utils import sci_constants as constants from .....utils import sci_constants as constants
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base, events
log = logger.get(__name__)
class WaveConstantNode(base.MaxwellSimNode): class WaveConstantNode(base.MaxwellSimNode):
node_type = ct.NodeType.WaveConstant node_type = ct.NodeType.WaveConstant
@ -22,7 +25,7 @@ class WaveConstantNode(base.MaxwellSimNode):
use_range: bpy.props.BoolProperty( use_range: bpy.props.BoolProperty(
name='Range', name='Range',
description='Whether to use the wavelength range', description='Whether to use a wavelength/frequency range',
default=False, default=False,
update=lambda self, context: self.sync_prop('use_range', context), update=lambda self, context: self.sync_prop('use_range', context),
) )
@ -36,69 +39,87 @@ class WaveConstantNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'WL', 'WL',
kind=ct.DataFlowKind.Value, kind=ct.DataFlowKind.Value,
all_loose_input_sockets=True, # Data
input_sockets={'WL', 'Freq'},
input_sockets_optional={'WL': True, 'Freq': True},
) )
def compute_wl_value(self, loose_input_sockets: dict) -> sp.Expr: def compute_wl_value(self, input_sockets: dict) -> sp.Expr:
if (wl := loose_input_sockets.get('WL')) is not None: if input_sockets['WL'] is not None:
return wl return input_sockets['WL']
freq = loose_input_sockets.get('Freq') if input_sockets['WL'] is None and input_sockets['Freq'] is None:
return constants.vac_speed_of_light / freq msg = 'Both WL and Freq are None.'
raise RuntimeError(msg)
return constants.vac_speed_of_light / input_sockets['Freq']
@events.computes_output_socket( @events.computes_output_socket(
'Freq', 'Freq',
kind=ct.DataFlowKind.Value, kind=ct.DataFlowKind.Value,
all_loose_input_sockets=True, # Data
input_sockets={'WL', 'Freq'},
input_sockets_optional={'WL': True, 'Freq': True},
) )
def compute_freq_value(self, loose_input_sockets: dict) -> sp.Expr: def compute_freq_value(self, input_sockets: dict) -> sp.Expr:
if (freq := loose_input_sockets.get('Freq')) is not None: log.critical(input_sockets)
return freq if input_sockets['Freq'] is not None:
return input_sockets['Freq']
wl = loose_input_sockets.get('WL') if input_sockets['WL'] is None and input_sockets['Freq'] is None:
return constants.vac_speed_of_light / wl msg = 'Both WL and Freq are None.'
raise RuntimeError(msg)
return constants.vac_speed_of_light / input_sockets['WL']
@events.computes_output_socket( @events.computes_output_socket(
'WL', 'WL',
kind=ct.DataFlowKind.LazyValueRange, kind=ct.DataFlowKind.LazyValueRange,
all_loose_input_sockets=True, # Data
input_sockets={'WL', 'Freq'},
input_sockets_optional={'WL': True, 'Freq': True},
) )
def compute_wl_lazyvaluerange(self, loose_input_sockets: dict) -> sp.Expr: def compute_wl_range(self, input_sockets: dict) -> sp.Expr:
if (wl := loose_input_sockets.get('WL')) is not None: if input_sockets['WL'] is not None:
return wl return input_sockets['WL']
freq = loose_input_sockets.get('Freq') if input_sockets['WL'] is None and input_sockets['Freq'] is None:
msg = 'Both WL and Freq are None.'
raise RuntimeError(msg)
if isinstance(freq, ct.LazyDataValueRange): return input_sockets['Freq'].rescale_bounds(
return freq.rescale_bounds(
lambda bound: constants.vac_speed_of_light / bound, reverse=True lambda bound: constants.vac_speed_of_light / bound, reverse=True
) )
return constants.vac_speed_of_light / freq
@events.computes_output_socket( @events.computes_output_socket(
'Freq', 'Freq',
kind=ct.DataFlowKind.LazyValueRange, kind=ct.DataFlowKind.LazyValueRange,
all_loose_input_sockets=True, # Data
input_sockets={'WL', 'Freq'},
input_socket_kinds={
'WL': ct.DataFlowKind.LazyValueRange,
'Freq': ct.DataFlowKind.LazyValueRange,
},
input_sockets_optional={'WL': True, 'Freq': True},
) )
def compute_freq_lazyvaluerange(self, loose_input_sockets: dict) -> sp.Expr: def compute_freq_range(self, input_sockets: dict) -> sp.Expr:
if (freq := loose_input_sockets.get('Freq')) is not None: if input_sockets['Freq'] is not None:
return freq return input_sockets['Freq']
wl = loose_input_sockets.get('WL') if input_sockets['WL'] is None and input_sockets['Freq'] is None:
msg = 'Both WL and Freq are None.'
raise RuntimeError(msg)
if isinstance(wl, ct.LazyDataValueRange): return input_sockets['WL'].rescale_bounds(
return wl.rescale_bounds(
lambda bound: constants.vac_speed_of_light / bound, reverse=True lambda bound: constants.vac_speed_of_light / bound, reverse=True
) )
return constants.vac_speed_of_light / wl
#################### ####################
# - Event Methods # - Event Methods
#################### ####################
@events.on_value_changed( @events.on_value_changed(
prop_name={'active_socket_set', 'use_range'}, prop_name={'active_socket_set', 'use_range'},
props={'active_socket_set', 'use_range'}, props={'active_socket_set', 'use_range'},
run_on_init=True,
) )
def on_input_spec_change(self, props: dict): def on_input_spec_change(self, props: dict):
if props['active_socket_set'] == 'Wavelength': if props['active_socket_set'] == 'Wavelength':
@ -123,12 +144,6 @@ class WaveConstantNode(base.MaxwellSimNode):
'Freq': sockets.PhysicalFreqSocketDef(is_array=props['use_range']), 'Freq': sockets.PhysicalFreqSocketDef(is_array=props['use_range']),
} }
@events.on_init(
props={'active_socket_set', 'use_range'},
)
def on_init(self, props: dict):
self.on_input_spec_change()
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -47,6 +47,7 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
## TODO: REMOVE TEST ## TODO: REMOVE TEST
log.info('Loading SimulationData File') log.info('Loading SimulationData File')
import sys import sys
for module_name, module in sys.modules.copy().items(): for module_name, module in sys.modules.copy().items():
if module_name == '__mp_main__': if module_name == '__mp_main__':
print('Problematic Module Entry', module_name) print('Problematic Module Entry', module_name)
@ -77,7 +78,9 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
cloud_task, _sim_data_cache_path(cloud_task.task_id) cloud_task, _sim_data_cache_path(cloud_task.task_id)
) )
@events.on_value_changed(socket_name='Cloud Task', input_sockets={'Cloud Task'}) @events.on_value_changed(
socket_name='Cloud Task', run_on_init=True, input_sockets={'Cloud Task'}
)
def on_cloud_task_changed(self, input_sockets: dict): def on_cloud_task_changed(self, input_sockets: dict):
if ( if (
(cloud_task := input_sockets['Cloud Task']) is not None (cloud_task := input_sockets['Cloud Task']) is not None
@ -90,10 +93,6 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
else: else:
self.loose_output_sockets = {} self.loose_output_sockets = {}
@events.on_init()
def on_init(self):
self.on_cloud_task_changed()
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -99,9 +99,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=tuple(input_sockets['Samples/Space']), interval_space=tuple(input_sockets['Samples/Space']),
freqs=input_sockets['Freqs'].realize().values, freqs=input_sockets['Freqs'].realize().values,
#freqs=[
# float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
#],
) )
#################### ####################

View File

@ -45,8 +45,9 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
), ),
}, },
} }
output_sockets: typ.ClassVar = { output_socket_sets: typ.ClassVar = {
'Monitor': sockets.MaxwellMonitorSocketDef(), 'Freq Domain': {'Freq Monitor': sockets.MaxwellMonitorSocketDef()},
'Time Domain': {'Time Monitor': sockets.MaxwellMonitorSocketDef()},
} }
managed_obj_defs: typ.ClassVar = { managed_obj_defs: typ.ClassVar = {
@ -62,36 +63,31 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
# - Event Methods: Computation # - Event Methods: Computation
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Monitor', 'Freq Monitor',
props={'active_socket_set', 'sim_node_name'}, props={'sim_node_name'},
input_sockets={ input_sockets={
'Rec Start',
'Rec Stop',
'Center', 'Center',
'Size', 'Size',
'Samples/Space', 'Samples/Space',
'Samples/Time',
'Freqs', 'Freqs',
'Direction', 'Direction',
}, },
input_socket_kinds={ input_socket_kinds={
'Freqs': ct.LazyDataValueRange, 'Freqs': ct.DataFlowKind.LazyValueRange,
}, },
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D}, unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={ scale_input_sockets={
'Center': 'Tidy3DUnits', 'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits', 'Size': 'Tidy3DUnits',
'Freqs': 'Tidy3DUnits', 'Freqs': 'Tidy3DUnits',
'Samples/Space': 'Tidy3DUnits',
'Rec Start': 'Tidy3DUnits',
'Rec Stop': 'Tidy3DUnits',
'Samples/Time': 'Tidy3DUnits',
}, },
) )
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor: def compute_freq_monitor(
direction = '+' if input_sockets['Direction'] else '-' self,
input_sockets: dict,
if props['active_socket_set'] == 'Freq Domain': props: dict,
unit_systems: dict,
) -> td.FieldMonitor:
log.info( log.info(
'Computing FluxMonitor (name="%s") with center="%s", size="%s"', 'Computing FluxMonitor (name="%s") with center="%s", size="%s"',
props['sim_node_name'], props['sim_node_name'],
@ -102,20 +98,9 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
center=input_sockets['Center'], center=input_sockets['Center'],
size=input_sockets['Size'], size=input_sockets['Size'],
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=input_sockets['Samples/Space'], interval_space=(1,1,1),
freqs=input_sockets['Freqs'].realize().values, freqs=input_sockets['Freqs'].realize().values,
normal_dir=direction, normal_dir='+' if input_sockets['Direction'] else '-',
)
return td.FluxTimeMonitor(
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
start=input_sockets['Rec Start'],
stop=input_sockets['Rec Stop'],
interval=input_sockets['Samples/Time'],
interval_space=input_sockets['Samples/Space'],
normal_dir=direction,
) )
#################### ####################

View File

@ -70,12 +70,6 @@ class ViewerNode(base.MaxwellSimNode):
update=lambda self, context: self.sync_prop('auto_3d_preview', context), update=lambda self, context: self.sync_prop('auto_3d_preview', context),
) )
cache__data_socket_linked: bpy.props.BoolProperty(
name='Data Is Linked',
description='Whether the Data input was linked last time it was checked.',
default=True,
)
#################### ####################
# - UI # - UI
#################### ####################
@ -125,42 +119,27 @@ class ViewerNode(base.MaxwellSimNode):
# - Event Methods # - Event Methods
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name='Data',
prop_name='auto_plot', prop_name='auto_plot',
props={'auto_plot'}, props={'auto_plot'},
) )
def on_changed_plot_preview(self, props): def on_changed_plot_preview(self, props):
if self.inputs['Data'].is_linked and props['auto_plot']: if self.inputs['Data'].is_linked and props['auto_plot']:
# log.debug('Enabling 2D Plot from "%s"', self.name)
self.trigger_action(ct.DataFlowAction.ShowPlot) self.trigger_action(ct.DataFlowAction.ShowPlot)
@events.on_value_changed( @events.on_value_changed(
socket_name='Data',
prop_name='auto_3d_preview', prop_name='auto_3d_preview',
props={'auto_3d_preview'}, props={'auto_3d_preview'},
) )
def on_changed_3d_preview(self, props): def on_changed_3d_preview(self, props):
# Unpreview Everything
if props['auto_3d_preview']:
node_tree = self.id_data node_tree = self.id_data
node_tree.unpreview_all()
# Trigger Preview Action # Remove Non-Repreviewed Previews on Close
with node_tree.repreview_all():
if self.inputs['Data'].is_linked and props['auto_3d_preview']: if self.inputs['Data'].is_linked and props['auto_3d_preview']:
# log.debug('Enabling 3D Previews from "%s"', self.name)
self.trigger_action(ct.DataFlowAction.ShowPreview) self.trigger_action(ct.DataFlowAction.ShowPreview)
@events.on_value_changed(
socket_name='Data',
)
def on_changed_3d_data(self):
# Is Linked: Re-Preview
if self.inputs['Data'].is_linked:
self.on_changed_3d_preview()
self.on_changed_plot_preview()
# Just Linked / Just Unlinked: Preview/Unpreview All
if self.inputs['Data'].is_linked ^ self.cache__data_socket_linked:
self.cache__data_socket_linked = self.inputs['Data'].is_linked
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -63,6 +63,7 @@ class SimDomainNode(base.MaxwellSimNode):
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active', prop_name='preview_active',
run_on_init=True,
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
@ -94,10 +95,6 @@ class SimDomainNode(base.MaxwellSimNode):
if props['preview_active']: if props['preview_active']:
managed_objs['mesh'].show_preview() managed_objs['mesh'].show_preview()
@events.on_init()
def on_init(self):
self.on_input_changed()
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -71,14 +71,14 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
socket_name='GeoNodes', socket_name='GeoNodes',
prop_name='preview_active', prop_name='preview_active',
any_loose_input_socket=True, any_loose_input_socket=True,
run_on_init=True,
# Pass Data
props={'preview_active'}, props={'preview_active'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
input_sockets={'Center', 'GeoNodes'}, input_sockets={'Center', 'GeoNodes'},
all_loose_input_sockets=True, all_loose_input_sockets=True,
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={'Center': 'BlenderUnits'},
'Center': 'BlenderUnits'
}
) )
def on_input_changed( def on_input_changed(
self, self,

View File

@ -62,6 +62,7 @@ class BoxStructureNode(base.MaxwellSimNode):
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active', prop_name='preview_active',
run_on_init=True,
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
@ -93,10 +94,6 @@ class BoxStructureNode(base.MaxwellSimNode):
if props['preview_active']: if props['preview_active']:
managed_objs['mesh'].show_preview() managed_objs['mesh'].show_preview()
@events.on_init()
def on_init(self):
self.on_inputs_changed()
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -64,6 +64,7 @@ class SphereStructureNode(base.MaxwellSimNode):
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Radius'}, socket_name={'Center', 'Radius'},
prop_name='preview_active', prop_name='preview_active',
run_on_init=True,
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center', 'Radius'}, input_sockets={'Center', 'Radius'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
@ -96,10 +97,6 @@ class SphereStructureNode(base.MaxwellSimNode):
if props['preview_active']: if props['preview_active']:
managed_objs['mesh'].show_preview() managed_objs['mesh'].show_preview()
@events.on_init()
def on_init(self):
self.on_inputs_changed()
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -1,3 +1,5 @@
import typing as typ
import bpy import bpy
import sympy as sp import sympy as sp
@ -5,18 +7,15 @@ from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base, events
MAX_AMOUNT = 20
class CombineNode(base.MaxwellSimNode): class CombineNode(base.MaxwellSimNode):
node_type = ct.NodeType.Combine node_type = ct.NodeType.Combine
bl_label = 'Combine' bl_label = 'Combine'
# bl_icon = ...
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_socket_sets = { input_socket_sets: typ.ClassVar = {
'Maxwell Sources': {}, 'Maxwell Sources': {},
'Maxwell Structures': {}, 'Maxwell Structures': {},
'Maxwell Monitors': {}, 'Maxwell Monitors': {},
@ -69,7 +68,7 @@ class CombineNode(base.MaxwellSimNode):
description='Amount of Objects to Combine', description='Amount of Objects to Combine',
default=1, default=1,
min=1, min=1,
max=MAX_AMOUNT, # max=MAX_AMOUNT,
update=lambda self, context: self.sync_prop('amount', context), update=lambda self, context: self.sync_prop('amount', context),
) )
@ -118,6 +117,7 @@ class CombineNode(base.MaxwellSimNode):
@events.on_value_changed( @events.on_value_changed(
prop_name='active_socket_set', prop_name='active_socket_set',
props={'active_socket_set', 'amount'}, props={'active_socket_set', 'amount'},
run_on_init=True,
) )
def on_value_changed__active_socket_set(self, props): def on_value_changed__active_socket_set(self, props):
if props['active_socket_set'] == 'Maxwell Sources': if props['active_socket_set'] == 'Maxwell Sources':
@ -144,10 +144,6 @@ class CombineNode(base.MaxwellSimNode):
def on_value_changed__amount(self): def on_value_changed__amount(self):
self.on_value_changed__active_socket_set() self.on_value_changed__active_socket_set()
@events.on_init()
def on_init(self):
self.on_value_changed__active_socket_set()
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -116,7 +116,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
`trigger_action` method will be called. `trigger_action` method will be called.
""" """
# Forwards Chains # Forwards Chains
if action in {'value_changed'}: if action in {ct.DataFlowAction.DataChanged}:
## Input Socket ## Input Socket
if not self.is_output: if not self.is_output:
self.node.trigger_action(action, socket_name=self.name) self.node.trigger_action(action, socket_name=self.name)
@ -128,15 +128,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
# Backwards Chains # Backwards Chains
elif action in { elif action in {
'enable_lock', ct.DataFlowAction.EnableLock,
'disable_lock', ct.DataFlowAction.DisableLock,
'show_preview', ct.DataFlowAction.OutputRequested,
'show_plot', ct.DataFlowAction.DataChanged,
ct.DataFlowAction.ShowPreview,
ct.DataFlowAction.ShowPlot,
}: }:
if action == 'enable_lock': if action == ct.DataFlowAction.EnableLock:
self.locked = True self.locked = True
if action == 'disable_lock': if action == ct.DataFlowAction.DisableLock:
self.locked = False self.locked = False
## Output Socket ## Output Socket
@ -208,6 +210,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Returns a bool, whether or not the socket consents to the link change. Returns a bool, whether or not the socket consents to the link change.
""" """
## TODO: Crash if deleting removing linked loose sockets.
if self.locked: if self.locked:
return False return False
if self.is_output: if self.is_output:

View File

@ -105,7 +105,7 @@ class TidyCloudFolders:
cloud_folder.folder_id: cloud_folder for cloud_folder in cloud_folders cloud_folder.folder_id: cloud_folder for cloud_folder in cloud_folders
} }
cls.cache_folders = folders cls.cache_folders = folders
log.info("Retrieved Folders: %s", str(cls.cache_folders)) log.info('Retrieved Folders: %s', str(cls.cache_folders))
return folders return folders
@classmethod @classmethod
@ -243,7 +243,11 @@ class TidyCloudTasks:
## Task by-Folder Cache ## Task by-Folder Cache
cls.cache_folder_tasks[cloud_folder.folder_id] = set(cloud_tasks) cls.cache_folder_tasks[cloud_folder.folder_id] = set(cloud_tasks)
log.info('Retrieved Tasks (folder="%s"): %s)', cloud_folder.folder_id, str(set(cloud_tasks))) log.info(
'Retrieved Tasks (folder="%s"): %s)',
cloud_folder.folder_id,
str(set(cloud_tasks)),
)
return cloud_tasks return cloud_tasks
#################### ####################