diff --git a/TODO.md b/TODO.md index 2449f17..cd8b947 100644 --- a/TODO.md +++ b/TODO.md @@ -1,20 +1,16 @@ # Acute Tasks - [x] Implement Material Import for Maxim Data -- Move preview GN trees to the asset library. +- [x] Implement Robust DataFlowKind for list-like / spectral-like composite types +- [ ] Finish the "Low-Hanging Fruit" Nodes +- [ ] Move preview GN trees to the asset library. # Nodes -**LEGEND**: -- [-] Exists but doesn't quite work good enough. -- [x] Done to working degree (the standard is "good enough for the demo"). - - See check marks underneath -- [?] Unsure whether we should do this. - ## Inputs - [x] Wave Constant - [x] Implement export of frequency / wavelength array/range. -- [-] Unit System +- [x] Unit System - [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row. - [ ] Constants / Scientific Constant @@ -26,7 +22,7 @@ - [ ] Pol: Poincare sphere viz as 3D GN. - [x] Constants / Blender Constant -- [-] Web / Tidy3D Web Importer +- [ ] Web / Tidy3D Web Importer - [ ] Change to output only a `FilePath`, which can be plugged into a Tidy3D File Importer. - [ ] Implement caching, such that the file will only download if the file doesn't already exist. - [ ] Have a visual indicator for the current download status, with a manual re-download button. @@ -79,7 +75,7 @@ - [x] Point Dipole Source - [ ] Use a viz mesh, not empty (empty doesn't play well with alpha hashing). -- [-] Plane Wave Source +- [ ] Plane Wave Source - [x] Implement an oriented vector input with 3D preview. - [ ] **IMPORTANT**: Fix the math so that an actually valid construction emerges!! - [ ] Uniform Current Source @@ -120,7 +116,6 @@ - [x] Rewrite to use unit systems properly. - [ ] Propertly map / implement Enum input sockets to the GN group. - [ ] Implement a panel system, either based on native GN panels, or description parsing, or something like that. - - [?] When GeoNodes themselves declare panels, implement a grid-like tab system to select which sockets should be exposed in the node at a given point in time. - [ ] Primitive Structures / Plane Structure - [x] Primitive Structures / Box Structure @@ -197,7 +192,7 @@ - [x] Structures / Arrays / Box - [x] Structures / Arrays / Sphere - [ ] Structures / Arrays / Cylinder -- [-] Structures / Arrays / Ring +- [x] Structures / Arrays / Ring - [ ] Structures / Arrays / Capsule - [ ] Structures / Arrays / Cone @@ -270,7 +265,7 @@ - [ ] FDTD Sim - [ ] Sim Domain - - [?] Toggleable option to push-sync the simulation time duration to the scene end time (how to handle FPS vs time-step? Should we adjust the FPS such that there is one time step per frame, while keeping the definition of "second" aligned to the Blender unit system?) + - [ ] Toggleable option to push-sync the simulation time duration to the scene end time (how to handle FPS vs time-step? Should we adjust the FPS such that there is one time step per frame, while keeping the definition of "second" aligned to the Blender unit system?) - [ ] Sim Grid - [ ] Sim Grid Axis @@ -376,7 +371,7 @@ ## Registration and Contracts - [ ] Refactor the node category code; it's ugly. - It's maybe not that easy. And it seems to work with surprising reliability. Leave it alone for now! -- [?] Would be nice with some kind of indicator somewhere to help set good socket descriptions when making geonodes. +- [ ] (?) Would be nice with some kind of indicator somewhere to help set good socket descriptions when making geonodes. ## Managed Objects - [ ] Implement ManagedEmpty @@ -401,7 +396,8 @@ - [ ] When presets are used, if a preset is selected and the user alters a preset setting, then dynamically switch the preset indicator back to "Custom" to indicate that there is no active preset ## Events -- [-] Mechanism for selecting a blender object managed by a particular node. +- [x] Mechanism for selecting a blender object managed by a particular node. + - [ ] Standard way of triggering the selection - [ ] Mechanism for ex. specially coloring a node that is currently participating in the preview. - [ ] Custom callbacks when deleting a node (in `free()`), to ex. delete all previews with the viewer node. @@ -414,7 +410,7 @@ ## Many Nodes - [ ] Implement "Steady-State" / "Time Domain" on all relevant Monitor nodes -- [?] Dynamic `bl_label` where appropriate (ex. "Library Medium" becoming "Au Medium") +- [ ] (?) Dynamic `bl_label` where appropriate (ex. "Library Medium" becoming "Au Medium") - [ ] Implement LazyValue, including LazyParamValue on a new class of constant-like input nodes that really just emit ex. sympy variables. - [ ] Medium Features - [ ] Accept spatial field. Else, spatial uniformity. @@ -438,11 +434,11 @@ ## Version Churn - [ ] Migrate to StrEnum sockets (py3.11). - [ ] Implement drag-and-drop node-from-file via bl4.1 file handler API. -- [-] Start thinking about ways around `__annotations__` hacking. -- [-] Prepare for for multi-input sockets (bl4.2) +- [ ] Start thinking about ways around `__annotations__` hacking. +- [ ] Prepare for for multi-input sockets (bl4.2) - PR has been merged: (for now, just use the "Combine" node and have seperate socket types for both). - The `Combine` node has its own benefits, including previewability of "only structures". Multi-input would mainly be a kind of shorthand in specific cases (like input to the `Combine` node?) -- [-] Prepare for volume geonodes (bl4.2; July 16, 2024) +- [ ] Prepare for volume geonodes (bl4.2; July 16, 2024) - Will allow for actual volume processing in GeoNodes. - We might still want/need the jax based stuff after; volume geonodes aren't finalized. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py index 3bbdf39..d322a7f 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py @@ -53,7 +53,17 @@ from .managed_obj_type import ManagedObjType #################### # - Data Flows #################### -from .data_flows import DataFlowKind +from .data_flows import ( + DataFlowKind, + DataCapabilities, + DataValue, + DataValueArray, + DataValueSpectrum, + LazyDataValue, + LazyDataValueRange, + LazyDataValueSpectrum, +) +from .data_flow_actions import DataFlowAction #################### # - Schemas @@ -85,5 +95,13 @@ __all__ = [ 'NODE_CAT_LABELS', 'ManagedObjType', 'DataFlowKind', + 'DataCapabilities', + 'DataValue', + 'DataValueArray', + 'DataValueSpectrum', + 'LazyDataValue', + 'LazyDataValueRange', + 'LazyDataValueSpectrum', + 'DataFlowAction', 'schemas', ] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py new file mode 100644 index 0000000..5ab0897 --- /dev/null +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flow_actions.py @@ -0,0 +1,14 @@ +import enum + + +class DataFlowAction(enum.StrEnum): + # Locking + EnableLock = 'enable_lock' + DisableLock = 'disable_lock' + + # Value + DataChanged = 'value_changed' + + # Previewing + ShowPreview = 'show_preview' + ShowPlot = 'show_plot' diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py index ebd7121..0eb21cc 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/data_flows.py @@ -1,20 +1,34 @@ +import dataclasses import enum +import functools +import typing as typ +from types import MappingProxyType -from ....utils.blender_type_enum import BlenderTypeEnum +# import colour ## TODO +import numpy as np +import sympy as sp +import sympy.physics.units as spu +import typing_extensions as typx + +from ....utils import extra_sympy_units as spux +from ....utils import sci_constants as constants +from .socket_types import SocketType -class DataFlowKind(BlenderTypeEnum): +class DataFlowKind(enum.StrEnum): """Defines a shape/kind of data that may flow through a node tree. Since a node socket may define one of each, we can support several related kinds of data flow through the same node-graph infrastructure. Attributes: - Value: A value usable without new data. + Value: A value without any unknown symbols. - Basic types aka. float, int, list, string, etc. . - Exotic (immutable-ish) types aka. numpy array, KDTree, etc. . - A usable constructed object, ex. a `tidy3d.Box`. - Expressions (`sp.Expr`) that don't have unknown variables. - Lazy sequences aka. generators, with all data bound. + SpectralValue: A value defined along a spectral range. + - {`np.array` LazyValue: An object which, when given new data, can make many values. - An `sp.Expr`, which might need `simplify`ing, `jax` JIT'ing, unit cancellations, variable substitutions, etc. before use. @@ -50,8 +64,215 @@ class DataFlowKind(BlenderTypeEnum): Implementation TBD - though, ostensibly, one would have a "parameter" node which both would only provide a LazyValue (aka. a symbolic variable), but would also be able to provide a LazyParamValue, which would be a particular value of some kind (probably via the `value` of some other node socket). """ - Value = enum.auto() - LazyValue = enum.auto() Capabilities = enum.auto() - LazyParamValue = enum.auto() + # Values + Value = enum.auto() + ValueArray = enum.auto() + ValueSpectrum = enum.auto() + + # Lazy + LazyValue = enum.auto() + LazyValueRange = enum.auto() + LazyValueSpectrum = enum.auto() + + +#################### +# - Data Structures: Capabilities +#################### +@dataclasses.dataclass(frozen=True, kw_only=True) +class DataCapabilities: + socket_type: SocketType + active_kind: DataFlowKind + + is_universal: bool = False + + def is_compatible_with(self, other: typ.Self) -> bool: + return ( + self.socket_type == other.socket_type + and self.active_kind == other.active_kind + ) or other.is_universal + + +#################### +# - Data Structures: Non-Lazy +#################### +DataValue: typ.TypeAlias = typ.Any + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class DataValueArray: + """A simple, flat array of values with an optionally-attached unit. + + Attributes: + values: A 1D array-like object of arbitrary numerical type. + unit: A `sympy` unit. + None if unitless. + """ + + values: typ.Sequence[DataValue] + unit: spu.Quantity | None + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class DataValueSpectrum: + """A numerical representation of a spectral distribution. + + Attributes: + wls: A 1D `numpy` float array of wavelength values. + wls_unit: The unit of wavelengths, as length dimension. + values: A 1D `numpy` float array of values corresponding to wavelength values. + values_unit: The unit of the value, as arbitrary dimension. + freqs_unit: The unit of the value, as arbitrary dimension. + """ + + # Wavelength + wls: np.array + wls_unit: spu.Quantity + + # Value + values: np.array + values_unit: spu.Quantity + + # Frequency + freqs_unit: spu.Quantity = spu.hertz + + @functools.cached_property + def freqs(self) -> np.array: + """The spectral frequencies, computed from the wavelengths. + + Frequencies are NOT reversed, so as to preserve the by-index mapping to `DataValueSpectrum.values`. + + Returns: + Frequencies, as a unitless `numpy` array. + Use `DataValueSpectrum.wls_unit` to interpret this return value. + """ + unitless_speed_of_light = spux.sympy_to_python( + spux.scale_to_unit( + constants.vac_speed_of_light, (self.wl_unit / self.freq_unit) + ) + ) + return unitless_speed_of_light / self.wls + + # TODO: Colour Library + # def as_colour_sd(self) -> colour.SpectralDistribution: + # """Returns the `colour` representation of this spectral distribution, ideal for plotting and colorimetric analysis.""" + # return colour.SpectralDistribution(data=self.values, domain=self.wls) + + +#################### +# - Data Structures: Lazy +#################### +@dataclasses.dataclass(frozen=True, kw_only=True) +class LazyDataValue: + callback: typ.Callable[[...], [DataValue]] + + def realize(self, *args: list[DataValue]) -> DataValue: + return self.callback(*args) + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class LazyDataValueRange: + symbols: set[sp.Symbol] + + start: sp.Basic + stop: sp.Basic + steps: int + scaling: typx.Literal['lin', 'geom', 'log'] = 'lin' + + has_unit: bool = False + + def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self: + if self.has_unit: + return LazyDataValueRange( + symbols=self.symbols, + has_unit=self.has_unit, + start=spu.convert_to(self.start, unit), + stop=spu.convert_to(self.stop, unit), + steps=self.steps, + scaling=self.scaling, + ) + + msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' + raise ValueError(msg) + + def rescale_bounds( + self, + bound_cb: typ.Callable[[sp.Expr], sp.Expr], + reverse: bool = False, + ) -> typ.Self: + """Call a function on both bounds (start and stop), creating a new `LazyDataValueRange`.""" + return LazyDataValueRange( + symbols=self.symbols, + has_unit=self.has_unit, + start=bound_cb(self.start if not reverse else self.stop), + stop=bound_cb(self.stop if not reverse else self.start), + steps=self.steps, + scaling=self.scaling, + ) + + def realize( + self, symbol_values: dict[sp.Symbol, DataValue] = MappingProxyType({}) + ) -> DataValueArray: + # Realize Symbols + if not self.has_unit: + start = spux.sympy_to_python(self.start.subs(symbol_values)) + stop = spux.sympy_to_python(self.stop.subs(symbol_values)) + else: + start = spux.sympy_to_python( + spux.scale_to_unit(self.start.subs(symbol_values), self.unit) + ) + stop = spux.sympy_to_python( + spux.scale_to_unit(self.stop.subs(symbol_values), self.unit) + ) + + # Return Linspace / Logspace + if self.scaling == 'lin': + return DataValueArray( + values=np.linspace(start, stop, self.steps), unit=self.unit + ) + if self.scaling == 'geom': + return DataValueArray(np.geomspace(start, stop, self.steps), self.unit) + if self.scaling == 'log': + return DataValueArray(np.logspace(start, stop, self.steps), self.unit) + + raise NotImplementedError + + +@dataclasses.dataclass(frozen=True, kw_only=True) +class LazyDataValueSpectrum: + wl_unit: spu.Quantity + value_unit: spu.Quantity + value_expr: sp.Expr + + symbols: tuple[sp.Symbol, ...] = () + freq_symbol: sp.Symbol = sp.Symbol('lamda') # noqa: RUF009 + + def rescale_to_unit(self, unit: spu.Quantity) -> typ.Self: + raise NotImplementedError + + @functools.cached_property + def as_func(self) -> typ.Callable[[DataValue, ...], DataValue]: + """Generates an optimized function for numerical evaluation of the spectral expression.""" + return sp.lambdify([self.freq_symbol, *self.symbols], self.value_expr) + + def realize( + self, wl_range: DataValueArray, symbol_values: tuple[DataValue, ...] + ) -> DataValueSpectrum: + r"""Realizes the parameterized spectral function as a numerical spectral distribution. + + Parameters: + wl_range: The lazy wavelength range to build the concrete spectral distribution with. + symbol_values: Numerical values for each symbol, in the same order as defined in `LazyDataValueSpectrum.symbols`. + The wavelength symbol ($\lambda$ by default) always goes first. + _This is used to call the spectral function using the output of `.as_func()`._ + + Returns: + The concrete, numerical spectral distribution. + """ + return DataValueSpectrum( + wls=wl_range.values, + wls_unit=self.wl_unit, + values=self.as_func(*list(symbol_values.values())), + values_unit=self.value_unit, + ) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py index da69b3a..d2a085b 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py @@ -37,9 +37,8 @@ class ManagedBLImage(ct.schemas.ManagedObj): return # ...AND Desired Image Name is Taken - else: - msg = f'Desired name {value} for BL image is taken' - raise ValueError(msg) + msg = f'Desired name {value} for BL image is taken' + raise ValueError(msg) # Object DOES Exist bl_image.name = value @@ -48,11 +47,8 @@ class ManagedBLImage(ct.schemas.ManagedObj): ## - `set_name` is allowed to change the name; nodes account for this. def free(self): - if not (bl_image := bpy.data.images.get(self.name)): - msg = "Can't free BL image that doesn't exist" - raise ValueError(msg) - - bpy.data.images.remove(bl_image) + if bl_image := bpy.data.images.get(self.name): + bpy.data.images.remove(bl_image) #################### # - Managed Object Management @@ -166,7 +162,12 @@ class ManagedBLImage(ct.schemas.ManagedObj): # Compute Plot Dimensions aspect_ratio = _width_inches / _height_inches - log.debug('Create MPL Axes (aspect=%d, width=%d, height=%d)', aspect_ratio, _width_inches, _height_inches) + log.debug( + 'Create MPL Axes (aspect=%d, width=%d, height=%d)', + aspect_ratio, + _width_inches, + _height_inches, + ) # Create MPL Figure, Axes, and Compute Figure Geometry fig, ax = plt.subplots( figsize=[_width_inches, _height_inches], diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py index 04d6288..4a6f81c 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -352,11 +352,11 @@ class MaxwellSimNode(bpy.types.Node): self, value: dict[str, ct.schemas.SocketDef], ) -> None: - log.info( - 'Setting Loose Input Sockets on "%s" to "%s"', - self.bl_label, - str(value), - ) + # Prune Loose Sockets + self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER + self.sync_sockets() + + # Install New Sockets if not value: self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER else: @@ -364,13 +364,17 @@ class MaxwellSimNode(bpy.types.Node): # Synchronize Sockets self.sync_sockets() - ## TODO: Perhaps re-init() all loose sockets anyway? @loose_output_sockets.setter def loose_output_sockets( self, value: dict[str, ct.schemas.SocketDef], ) -> None: + # Prune Loose Sockets + self.ser_loose_output_sockets = _DEFAULT_LOOSE_SOCKET_SER + self.sync_sockets() + + # Install New Sockets if not value: self.ser_loose_output_sockets = _DEFAULT_LOOSE_SOCKET_SER else: @@ -378,7 +382,6 @@ class MaxwellSimNode(bpy.types.Node): # Synchronize Sockets self.sync_sockets() - ## TODO: Perhaps re-init() all loose sockets anyway? #################### # - Socket Management @@ -567,17 +570,11 @@ class MaxwellSimNode(bpy.types.Node): msg = f'Property {prop_name} not defined on socket {self}' raise RuntimeError(msg) - self.trigger_action('value_changed', prop_name=prop_name) + self.trigger_action(ct.DataFlowAction.DataChanged, prop_name=prop_name) def trigger_action( self, - action: typx.Literal[ - 'enable_lock', - 'disable_lock', - 'value_changed', - 'show_preview', - 'show_plot', - ], + action: ct.DataFlowAction, socket_name: ct.SocketName | None = None, prop_name: ct.SocketName | None = None, ) -> None: @@ -586,15 +583,15 @@ class MaxwellSimNode(bpy.types.Node): Invalidates (recursively) the cache of any managed object or output socket method that implicitly depends on this input socket. """ - #log.debug( - # 'Action "%s" Triggered in "%s" (socket_name="%s", prop_name="%s")', - # action, - # self.name, - # socket_name, - # prop_name, - #) + # log.debug( + # 'Action "%s" Triggered in "%s" (socket_name="%s", prop_name="%s")', + # action, + # self.name, + # socket_name, + # prop_name, + # ) # Forwards Chains - if action == 'value_changed': + if action == ct.DataFlowAction.DataChanged: # Run User Callbacks ## Careful with these, they run BEFORE propagation... ## ...because later-chain methods may rely on the results of this. @@ -611,11 +608,11 @@ class MaxwellSimNode(bpy.types.Node): and socket_name in self.loose_input_sockets ) ): - #log.debug( - # 'Running Value-Change Callback "%s" in "%s")', - # method.__name__, - # self.name, - #) + # log.debug( + # 'Running Value-Change Callback "%s" in "%s")', + # method.__name__, + # self.name, + # ) method(self) # Propagate via Output Sockets @@ -623,21 +620,21 @@ class MaxwellSimNode(bpy.types.Node): bl_socket.trigger_action(action) # Backwards Chains - elif action == 'enable_lock': + elif action == ct.DataFlowAction.EnableLock: self.locked = True ## Propagate via Input Sockets for bl_socket in self.active_bl_sockets('input'): bl_socket.trigger_action(action) - elif action == 'disable_lock': + elif action == ct.DataFlowAction.DisableLock: self.locked = False ## Propagate via Input Sockets for bl_socket in self.active_bl_sockets('input'): bl_socket.trigger_action(action) - elif action == 'show_preview': + elif action == ct.DataFlowAction.ShowPreview: # Run User Callbacks ## "On Show Preview" callbacks are 'on_value_changed' callbacks... ## ...which simply hook into the 'preview_active' property. @@ -653,7 +650,7 @@ class MaxwellSimNode(bpy.types.Node): for bl_socket in self.active_bl_sockets('input'): bl_socket.trigger_action(action) - elif action == 'show_plot': + elif action == ct.DataFlowAction.ShowPlot: # Run User Callbacks ## These shouldn't change any data, BUT... ## ...because they can stop propagation, they should go first. @@ -717,7 +714,7 @@ class MaxwellSimNode(bpy.types.Node): bl_socket.is_linked and bl_socket.locked for bl_socket in self.inputs.values() ): - self.trigger_action('disable_lock') + self.trigger_action(ct.DataFlowAction.DisableLock) # Free Managed Objects for managed_obj in self.managed_objs.values(): diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py index eb8eecc..435b170 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py @@ -69,11 +69,12 @@ PropName: typ.TypeAlias = str def event_decorator( action_type: EventCallbackType, extra_data: EventCallbackData, - kind: ct.DataFlowKind = ct.DataFlowKind.Value, props: set[PropName] = frozenset(), managed_objs: set[ManagedObjName] = frozenset(), input_sockets: set[ct.SocketName] = frozenset(), + input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}), output_sockets: set[ct.SocketName] = frozenset(), + output_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}), all_loose_input_sockets: bool = False, all_loose_output_sockets: bool = False, unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}), @@ -87,11 +88,11 @@ def event_decorator( Set to `return_method.action_type` extra_data: 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. - kind: The `ct.DataFlowKind` used to compute all input and output socket data for methods with. - Only affects data passed to the decorated method; namely `input_sockets`, `output_sockets`, and their loose variants. props: Set of `props` to compute, 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_socket_kinds: The `ct.DataFlowKind` to compute per-input-socket. + If an input socket isn't specified, it defaults to `ct.DataFlowKind.Value`. output_sockets: Set of `output_sockets` to compute, then pass to the decorated method. all_loose_input_sockets: Whether to compute all loose input sockets and pass them to the decorated method. Used when the names of the loose input sockets are unknown, but all of their values are needed. @@ -154,7 +155,12 @@ def event_decorator( ## Compute Requested Input Sockets if input_sockets: _input_sockets = { - input_socket_name: node._compute_input(input_socket_name, kind) + input_socket_name: node._compute_input( + input_socket_name, + kind=input_socket_kinds.get( + input_socket_name, ct.DataFlowKind.Value + ), + ) for input_socket_name in input_sockets } @@ -163,19 +169,35 @@ def event_decorator( ## 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] - _input_sockets[input_socket_name] = spux.sympy_to_python( - spux.scale_to_unit( - _input_sockets[input_socket_name], - unit_system[node.inputs[input_socket_name].socket_type], - ) + kind = input_socket_kinds.get( + input_socket_name, ct.DataFlowKind.Value ) + if kind == ct.DataFlowKind.Value: + _input_sockets[input_socket_name] = spux.sympy_to_python( + spux.scale_to_unit( + _input_sockets[input_socket_name], + unit_system[node.inputs[input_socket_name].socket_type], + ) + ) + elif kind == ct.DataFlowKind.LazyValueRange: + _input_sockets[input_socket_name] = _input_sockets[ + 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, kind) + output_socket_name: node.compute_output( + output_socket_name, + kind=input_socket_kinds.get( + input_socket_name, ct.DataFlowKind.Value + ), + ) for output_socket_name in output_sockets } @@ -184,19 +206,32 @@ def event_decorator( ## 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] - _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], - ) + 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 ## Compute All Loose Input Sockets if all_loose_input_sockets: _loose_input_sockets = { - input_socket_name: node._compute_input(input_socket_name, kind) + input_socket_name: node._compute_input(input_socket_name, kind=node.inputs[input_socket_name].active_kind) for input_socket_name in node.loose_input_sockets } method_kw_args |= {'loose_input_sockets': _loose_input_sockets} @@ -204,7 +239,7 @@ def event_decorator( ## Compute All Loose Output Sockets if all_loose_output_sockets: _loose_output_sockets = { - output_socket_name: node.compute_output(output_socket_name, kind) + output_socket_name: node.compute_output(output_socket_name, kind=node.outputs[output_socket_name].active_kind) for output_socket_name in node.loose_output_sockets } method_kw_args |= {'loose_output_sockets': _loose_output_sockets} @@ -221,10 +256,10 @@ def event_decorator( # Set Decorated Attributes and Return ## Fix Introspection + Documentation - #decorated.__name__ = method.__name__ - #decorated.__module__ = method.__module__ - #decorated.__qualname__ = method.__qualname__ - #decorated.__doc__ = method.__doc__ + # decorated.__name__ = method.__name__ + # decorated.__module__ = method.__module__ + # decorated.__qualname__ = method.__qualname__ + # decorated.__doc__ = method.__doc__ ## Add Spice decorated.action_type = action_type diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py index a5501f4..68186f3 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py @@ -1,5 +1,6 @@ import typing as typ +import bpy import sympy as sp import sympy.physics.units as spu @@ -15,106 +16,92 @@ class WaveConstantNode(base.MaxwellSimNode): bl_label = 'Wave Constant' input_socket_sets: typ.ClassVar = { - # Single - 'Vacuum WL': { - 'WL': sockets.PhysicalLengthSocketDef( - default_value=500 * spu.nm, - default_unit=spu.nm, - ), - }, - 'Frequency': { - 'Freq': sockets.PhysicalFreqSocketDef( - default_value=500 * spux.THz, - default_unit=spux.THz, - ), - }, - # Listy - 'Vacuum WLs': { - 'WLs': sockets.PhysicalLengthSocketDef( - is_list=True, - ), - }, - 'Frequencies': { - 'Freqs': sockets.PhysicalFreqSocketDef( - is_list=True, - ), - }, + 'Wavelength': {}, + 'Frequency': {}, } + use_range: bpy.props.BoolProperty( + name='Range', + description='Whether to use the wavelength range', + default=False, + update=lambda self, context: self.sync_prop('use_range', context), + ) + + def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout): + col.prop(self, 'use_range', toggle=True) + #################### - # - Event Methods: Listy Output + # - Event Methods: Wavelength Output #################### @events.computes_output_socket( 'WL', - input_sockets={'WL'}, + all_loose_input_sockets=True, ) - def compute_vacwl_from_vacwl(self, input_sockets: dict) -> sp.Expr: - return input_sockets['WL'] + def compute_wl(self, loose_input_sockets: dict) -> sp.Expr: + if (wl := loose_input_sockets.get('WL')) is not None: + return wl + + freq = loose_input_sockets.get('Freq') + + if isinstance(freq, ct.LazyDataValueRange): + return freq.rescale_bounds( + lambda bound: constants.vac_speed_of_light / bound, reverse=True + ) + + return constants.vac_speed_of_light / freq @events.computes_output_socket( - 'WL', - input_sockets={'Freq'}, + 'Freq', + all_loose_input_sockets=True, ) - def compute_freq_from_vacwl(self, input_sockets: dict) -> sp.Expr: - return constants.vac_speed_of_light / input_sockets['Freq'] + def compute_freq(self, loose_input_sockets: dict) -> sp.Expr: + if (freq := loose_input_sockets.get('Freq')) is not None: + return freq - #################### - # - Event Methods: Listy Output - #################### - @events.computes_output_socket( - 'WLs', - input_sockets={'WLs', 'Freqs'}, - ) - def compute_vac_wls(self, input_sockets: dict) -> sp.Expr: - if (vac_wls := input_sockets['WLs']) is not None: - return vac_wls - if (freqs := input_sockets['Freqs']) is not None: - return [constants.vac_speed_of_light / freq for freq in freqs][::-1] + wl = loose_input_sockets.get('WL') - msg = 'Vac WL and Freq are both None' - raise RuntimeError(msg) + if isinstance(wl, ct.LazyDataValueRange): + return wl.rescale_bounds( + lambda bound: constants.vac_speed_of_light / bound, reverse=True + ) - @events.computes_output_socket( - 'Freqs', - input_sockets={'WLs', 'Freqs'}, - ) - def compute_freqs(self, input_sockets: dict) -> sp.Expr: - if (vac_wls := input_sockets['WLs']) is not None: - return [constants.vac_speed_of_light / vac_wl for vac_wl in vac_wls][::-1] - if (freqs := input_sockets['Freqs']) is not None: - return freqs - - msg = 'Vac WL and Freq are both None' - raise RuntimeError(msg) + return constants.vac_speed_of_light / wl #################### # - Event Methods #################### - @events.on_value_changed(prop_name='active_socket_set', props={'active_socket_set'}) - def on_active_socket_set_changed(self, props: dict): - # Singular: Normal Output Sockets - if props['active_socket_set'] in {'Vacuum WL', 'Frequency'}: - self.loose_output_sockets = {} - self.loose_output_sockets = { - 'Freq': sockets.PhysicalFreqSocketDef(), - 'WL': sockets.PhysicalLengthSocketDef(), + @events.on_value_changed( + prop_name={'active_socket_set', 'use_range'}, + props={'active_socket_set', 'use_range'}, + ) + def on_input_spec_change(self, props: dict): + if props['active_socket_set'] == 'Wavelength': + self.loose_input_sockets = { + 'WL': sockets.PhysicalLengthSocketDef( + is_array=props['use_range'], + default_value=500 * spu.nm, + default_unit=spu.nm, + ) } - - # Plural: Listy Output Sockets - elif props['active_socket_set'] in {'Vacuum WLs', 'Frequencies'}: - self.loose_output_sockets = {} - self.loose_output_sockets = { - 'Freqs': sockets.PhysicalFreqSocketDef(is_list=True), - 'WLs': sockets.PhysicalLengthSocketDef(is_list=True), - } - else: - msg = f"Active socket set invalid for wave constant: {props['active_socket_set']}" - raise RuntimeError(msg) + self.loose_input_sockets = { + 'Freq': sockets.PhysicalFreqSocketDef( + is_array=props['use_range'], + default_value=600 * spux.THz, + default_unit=spux.THz, + ) + } - @events.on_init() - def on_init(self): - self.on_active_socket_set_changed() + self.loose_output_sockets = { + 'WL': sockets.PhysicalLengthSocketDef(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() #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py index 88e651f..d517afc 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py @@ -34,7 +34,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode): input_socket_sets: typ.ClassVar = { 'Freq Domain': { 'Freqs': sockets.PhysicalFreqSocketDef( - is_list=True, + is_array=True, ), }, 'Time Domain': { diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py index ffdb6fb..2dafaf8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py @@ -34,7 +34,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode): input_socket_sets: typ.ClassVar = { 'Freq Domain': { 'Freqs': sockets.PhysicalFreqSocketDef( - is_list=True, + is_array=True, ), }, 'Time Domain': { @@ -74,10 +74,14 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode): 'Freqs', 'Direction', }, + input_socket_kinds={ + 'Freqs': ct.LazyDataValueRange, + }, unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D}, scale_input_sockets={ 'Center': 'Tidy3DUnits', 'Size': 'Tidy3DUnits', + 'Freqs': 'Tidy3DUnits', 'Samples/Space': 'Tidy3DUnits', 'Rec Start': 'Tidy3DUnits', 'Rec Stop': 'Tidy3DUnits', @@ -88,8 +92,6 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode): direction = '+' if input_sockets['Direction'] else '-' if props['active_socket_set'] == 'Freq Domain': - freqs = input_sockets['Freqs'] - log.info( 'Computing FluxMonitor (name="%s") with center="%s", size="%s"', props['sim_node_name'], @@ -101,9 +103,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode): size=input_sockets['Size'], name=props['sim_node_name'], interval_space=input_sockets['Samples/Space'], - freqs=[ - float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs - ], + freqs=input_sockets['Freqs'].realize().values, normal_dir=direction, ) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py index 9d73d16..7fa55b2 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py @@ -180,7 +180,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode): #################### def sync_lock_tree(self, context): if self.lock_tree: - self.trigger_action('enable_lock') + self.trigger_action(ct.DataFlowAction.EnableLock) self.locked = False for bl_socket in self.inputs: if bl_socket.name == 'FDTD Sim': @@ -188,7 +188,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode): bl_socket.locked = False else: - self.trigger_action('disable_lock') + self.trigger_action(ct.DataFlowAction.DisableLock) self.sync_prop('lock_tree', context) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py index 249f7af..af9bb11 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/viewer.py @@ -131,7 +131,7 @@ class ViewerNode(base.MaxwellSimNode): def on_changed_plot_preview(self, props): if self.inputs['Data'].is_linked and props['auto_plot']: log.info('Enabling 2D Plot from "%s"', self.name) - self.trigger_action('show_plot') + self.trigger_action(ct.DataFlowAction.ShowPlot) @events.on_value_changed( prop_name='auto_3d_preview', @@ -145,7 +145,7 @@ class ViewerNode(base.MaxwellSimNode): # Trigger Preview Action if self.inputs['Data'].is_linked and props['auto_3d_preview']: log.info('Enabling 3D Previews from "%s"', self.name) - self.trigger_action('show_preview') + self.trigger_action(ct.DataFlowAction.ShowPreview) @events.on_value_changed( socket_name='Data', diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/props/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/props/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/props/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/props/base.py new file mode 100644 index 0000000..e681c68 --- /dev/null +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/props/base.py @@ -0,0 +1,26 @@ +#import typing as typ +#import bpy +# +#from .. import contracts as ct +# +# +# +#class MaxwellSimProp(bpy.types.PropertyGroup): +# """A Blender property usable in nodes and sockets.""" +# name: str = "" +# data_flow_kind: ct.DataFlowKind +# +# value: dict[str, tuple[bpy.types.Property, dict]] | None = None +# +# def __init_subclass__(cls, **kwargs: typ.Any): +# log.debug('Initializing Prop: %s', cls.node_type) +# super().__init_subclass__(**kwargs) +# +# # Setup Value +# if cls.value: +# cls.__annotations__['raw_value'] = value +# +# +# @property +# def value(self): +# if self.data_flow_kind diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py index deae400..bc5c754 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -65,11 +65,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket): cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type] # Setup List - cls.__annotations__['is_list'] = bpy.props.BoolProperty( - name='Is List', - description='Whether or not a particular socket is a list type socket', - default=False, - update=lambda self, context: self.sync_is_list(context), + cls.__annotations__['active_kind'] = bpy.props.StringProperty( + name='Active Kind', + description='The active Data Flow Kind', + default=str(ct.DataFlowKind.Value), + update=lambda self, _: self.sync_active_kind(), ) # Configure Use of Units @@ -90,7 +90,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket): for unit_name, unit_value in socket_units['values'].items() ], default=socket_units['default'], - update=lambda self, context: self.sync_unit_change(), + update=lambda self, _: self.sync_unit_change(), ) # Previous Unit (for conversion) @@ -103,13 +103,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket): #################### def trigger_action( self, - action: typx.Literal[ - 'enable_lock', - 'disable_lock', - 'value_changed', - 'show_preview', - 'show_plot', - ], + action: ct.DataFlowAction, ) -> None: """Called whenever the socket's output value has changed. @@ -157,24 +151,30 @@ class MaxwellSimSocket(bpy.types.NodeSocket): #################### # - Action Chain: Event Handlers #################### - def sync_is_list(self, context: bpy.types.Context): - """Called when the "is_list_ property has been updated.""" - if self.is_list: - if self.use_units: - self.display_shape = 'SQUARE_DOT' - else: - self.display_shape = 'SQUARE' + def sync_active_kind(self): + """Called when the active data flow kind of the socket changes. - self.trigger_action('value_changed') + Alters the shape of the socket to match the active DataFlowKind, then triggers `ct.DataFlowAction.DataChanged` on the current socket. + """ + self.display_shape = { + ct.DataFlowKind.Value: ct.SOCKET_SHAPES[self.socket_type], + ct.DataFlowKind.ValueArray: 'SQUARE', + ct.DataFlowKind.ValueSpectrum: 'SQUARE', + ct.DataFlowKind.LazyValue: ct.SOCKET_SHAPES[self.socket_type], + ct.DataFlowKind.LazyValueRange: 'SQUARE', + ct.DataFlowKind.LazyValueSpectrum: 'SQUARE', + }[self.active_kind] + ('_DOT' if self.use_units else '') - def sync_prop(self, prop_name: str, context: bpy.types.Context): + self.trigger_action(ct.DataFlowAction.DataChanged) + + def sync_prop(self, prop_name: str, _: bpy.types.Context): """Called when a property has been updated.""" - if not hasattr(self, prop_name): + if hasattr(self, prop_name): + self.trigger_action(ct.DataFlowAction.DataChanged) + else: msg = f'Property {prop_name} not defined on socket {self}' raise RuntimeError(msg) - self.trigger_action('value_changed') - def sync_link_added(self, link) -> bool: """Called when a link has been added to this (input) socket. @@ -186,7 +186,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket): msg = "Tried to sync 'link add' on output socket" raise RuntimeError(msg) - self.trigger_action('value_changed') + self.trigger_action(ct.DataFlowAction.DataChanged) return True @@ -201,63 +201,78 @@ class MaxwellSimSocket(bpy.types.NodeSocket): msg = "Tried to sync 'link add' on output socket" raise RuntimeError(msg) - self.trigger_action('value_changed') + self.trigger_action(ct.DataFlowAction.DataChanged) return True #################### # - Data Chain #################### + # Capabilities @property - def value(self) -> typ.Any: + def capabilities(self) -> None: + return ct.DataCapabilities( + socket_type=self.socket_type, + active_kind=self.active_kind, + ) + + # Value + @property + def value(self) -> ct.DataValue: raise NotImplementedError @value.setter - def value(self, value: typ.Any) -> None: + def value(self, value: ct.DataValue) -> None: raise NotImplementedError + # ValueArray @property - def value_list(self) -> typ.Any: - return [self.value] - - @value_list.setter - def value_list(self, value: typ.Any) -> None: + def value_array(self) -> ct.DataValueArray: raise NotImplementedError - def value_as_unit_system( - self, unit_system: dict, dimensionless: bool = True - ) -> typ.Any: - ## TODO: Caching could speed this boi up quite a bit - - unit_system_unit = unit_system[self.socket_type] - return ( - spu.convert_to( - self.value, - unit_system_unit, - ) - / unit_system_unit - ) + @value_array.setter + def value_array(self, value: ct.DataValueArray) -> None: + raise NotImplementedError + # ValueSpectrum @property - def lazy_value(self) -> None: + def value_spectrum(self) -> ct.DataValueSpectrum: + raise NotImplementedError + + @value_spectrum.setter + def value_spectrum(self, value: ct.DataValueSpectrum) -> None: + raise NotImplementedError + + # LazyValue + @property + def lazy_value(self) -> ct.LazyDataValue: raise NotImplementedError @lazy_value.setter - def lazy_value(self, lazy_value: typ.Any) -> None: + def lazy_value(self, lazy_value: ct.LazyDataValue) -> None: raise NotImplementedError + # LazyValueRange @property - def lazy_value_list(self) -> typ.Any: - return [self.lazy_value] - - @lazy_value_list.setter - def lazy_value_list(self, value: typ.Any) -> None: + def lazy_value_range(self) -> ct.LazyDataValueRange: raise NotImplementedError + @lazy_value_range.setter + def lazy_value_range(self, value: tuple[ct.DataValue, ct.DataValue, int]) -> None: + raise NotImplementedError + + # LazyValueSpectrum @property - def capabilities(self) -> None: + def lazy_value_spectrum(self) -> ct.LazyDataValueSpectrum: raise NotImplementedError + @lazy_value_spectrum.setter + def lazy_value_spectrum(self, value: ct.LazyDataValueSpectrum) -> None: + raise NotImplementedError + + #################### + # - Data Chain Computation + #################### def _compute_data( self, kind: ct.DataFlowKind = ct.DataFlowKind.Value, @@ -266,18 +281,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket): **NOTE**: Low-level method. Use `compute_data` instead. """ - if kind == ct.DataFlowKind.Value: - if self.is_list: - return self.value_list - return self.value - if kind == ct.DataFlowKind.LazyValue: - if self.is_list: - return self.lazy_value_list - return self.lazy_value - if kind == ct.DataFlowKind.Capabilities: - return self.capabilities + return { + ct.DataFlowKind.Value: lambda: self.value, + ct.DataFlowKind.ValueArray: lambda: self.value_array, + ct.DataFlowKind.ValueSpectrum: lambda: self.value_spectrum, + ct.DataFlowKind.LazyValue: lambda: self.lazy_value, + ct.DataFlowKind.LazyValueRange: lambda: self.lazy_value_range, + ct.DataFlowKind.LazyValueSpectrum: lambda: self.lazy_value_spectrum, + }[kind]() - return None + msg = f'socket._compute_data was called with invalid kind "{kind}"' + raise RuntimeError(msg) def compute_data( self, @@ -291,22 +305,25 @@ class MaxwellSimSocket(bpy.types.NodeSocket): - If output socket, ask node for data. """ # Compute Output Socket - ## List-like sockets guarantee that a list of a thing is passed. if self.is_output: - res = self.node.compute_output(self.name, kind=kind) - if self.is_list and not isinstance(res, list): - return [res] - return res + return self.node.compute_output(self.name, kind=kind) # Compute Input Socket ## Unlinked: Retrieve Socket Value if not self.is_linked: return self._compute_data(kind) - ## Linked: Compute Output of Linked Sockets + ## Linked: Check Capabilities + for link in self.links: + if not link.from_socket.capabilities.is_compatible_with(self.capabilities): + msg = f'Output socket "{link.from_socket.bl_label}" is linked to input socket "{self.bl_label}" with incompatible capabilities (caps_out="{link.from_socket.capabilities}", caps_in="{self.capabilities}")' + raise ValueError(msg) + + ## ...and Compute Data on Linked Socket linked_values = [link.from_socket.compute_data(kind) for link in self.links] - ## Return Single Value / List of Values + # Return Single Value / List of Values + ## Preparation for multi-input sockets. if len(linked_values) == 1: return linked_values[0] return linked_values @@ -361,14 +378,16 @@ class MaxwellSimSocket(bpy.types.NodeSocket): Can be overridden if more specific logic is required. """ - prev_value = self.value / self.unit * self.prev_unit - ## After changing units, self.value is expressed in the wrong unit. - ## - Therefore, we removing the new unit, and re-add the prev unit. - ## - Using only self.value avoids implementation-specific details. + if self.active_kind == ct.DataFlowKind.Value: + self.value = self.value / self.unit * self.prev_unit - self.value = spu.convert_to( - prev_value, self.unit - ) ## Now, the unit conversion can be done correctly. + elif self.active_kind == ct.DataFlowKind.LazyValueRange: + lazy_value_range = self.lazy_value_range + self.lazy_value_range = ( + lazy_value_range.start / self.unit * self.prev_unit, + lazy_value_range.stop / self.unit * self.prev_unit, + lazy_value_range.steps, + ) self.prev_active_unit = self.active_unit @@ -458,12 +477,16 @@ class MaxwellSimSocket(bpy.types.NodeSocket): elif self.locked: row.enabled = False - # Value Column(s) + # Data Column(s) col = row.column(align=True) - if self.is_list: - self.draw_value_list(col) - else: - self.draw_value(col) + { + ct.DataFlowKind.Value: self.draw_value, + ct.DataFlowKind.ValueArray: self.draw_value_array, + ct.DataFlowKind.ValueSpectrum: self.draw_value_spectrum, + ct.DataFlowKind.LazyValue: self.draw_lazy_value, + ct.DataFlowKind.LazyValueRange: self.draw_lazy_value_range, + ct.DataFlowKind.LazyValueSpectrum: self.draw_lazy_value_spectrum, + }[self.active_kind](col) def draw_output( self, @@ -489,14 +512,23 @@ class MaxwellSimSocket(bpy.types.NodeSocket): """ row.label(text=text) + #################### + # - DataFlowKind draw() Methods + #################### def draw_value(self, col: bpy.types.UILayout) -> None: - """Called to draw the value column in unlinked input sockets. + pass - Can be overridden. - """ + def draw_value_array(self, col: bpy.types.UILayout) -> None: + pass - def draw_value_list(self, col: bpy.types.UILayout) -> None: - """Called to draw the value list column in unlinked input sockets. + def draw_value_spectrum(self, col: bpy.types.UILayout) -> None: + pass - Can be overridden. - """ + def draw_lazy_value(self, col: bpy.types.UILayout) -> None: + pass + + def draw_lazy_value_range(self, col: bpy.types.UILayout) -> None: + pass + + def draw_lazy_value_spectrum(self, col: bpy.types.UILayout) -> None: + pass diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any.py index 040ee13..6f9618a 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/any.py @@ -10,6 +10,14 @@ from .. import base class AnyBLSocket(base.MaxwellSimSocket): socket_type = ct.SocketType.Any bl_label = 'Any' + + @property + def capabilities(self): + return ct.DataCapabilities( + socket_type=self.socket_type, + active_kind=self.active_kind, + is_universal=True, + ) #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py index da4d87d..7893494 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/monitor.py @@ -18,7 +18,8 @@ class MaxwellMonitorSocketDef(pyd.BaseModel): is_list: bool = False def init(self, bl_socket: MaxwellMonitorBLSocket) -> None: - bl_socket.is_list = self.is_list + if self.is_list: + bl_socket.active_kind = ct.DataValueArray #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source.py index 57c8930..b830762 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/source.py @@ -18,7 +18,8 @@ class MaxwellSourceSocketDef(pyd.BaseModel): is_list: bool = False def init(self, bl_socket: MaxwellSourceBLSocket) -> None: - bl_socket.is_list = self.is_list + if self.is_list: + bl_socket.active_kind = ct.DataValueArray #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure.py index b3453d5..1fa707c 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/structure.py @@ -18,7 +18,8 @@ class MaxwellStructureSocketDef(pyd.BaseModel): is_list: bool = False def init(self, bl_socket: MaxwellStructureBLSocket) -> None: - bl_socket.is_list = self.is_list + if self.is_list: + bl_socket.active_kind = ct.DataValueArray #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq.py index 5d4f23a..f1c410c 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/freq.py @@ -1,13 +1,16 @@ import bpy -import numpy as np import pydantic as pyd +import sympy as sp import sympy.physics.units as spu from .....utils import extra_sympy_units as spux +from .....utils import logger from .....utils.pydantic_sympy import SympyExpr from ... import contracts as ct from .. import base +log = logger.get(__name__) + #################### # - Blender Socket @@ -55,7 +58,7 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket): def draw_value(self, col: bpy.types.UILayout) -> None: col.prop(self, 'raw_value', text='') - def draw_value_list(self, col: bpy.types.UILayout) -> None: + def draw_lazy_value_range(self, col: bpy.types.UILayout) -> None: col.prop(self, 'min_freq', text='Min') col.prop(self, 'max_freq', text='Max') col.prop(self, 'steps', text='Steps') @@ -69,32 +72,25 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket): @value.setter def value(self, value: SympyExpr) -> None: - self.raw_value = spu.convert_to(value, self.unit) / self.unit + self.raw_value = spux.sympy_to_python(spux.scale_to_unit(value, self.unit)) @property - def value_list(self) -> list[SympyExpr]: - return [ - el * self.unit - for el in np.linspace(self.min_freq, self.max_freq, self.steps) - ] + def lazy_value_range(self) -> ct.LazyDataValueRange: + return ct.LazyDataValueRange( + symbols=set(), + has_unit=True, + start=sp.S(self.min_freq) * self.unit, + stop=sp.S(self.max_freq) * self.unit, + steps=self.steps, + scaling='lin', + ) - @value_list.setter - def value_list(self, value: tuple[SympyExpr, SympyExpr, int]): - self.min_freq, self.max_freq, self.steps = [ - spu.convert_to(el, self.unit) / self.unit for el in value[:2] - ] + [value[2]] - - def sync_unit_change(self) -> None: - if self.is_list: - self.value_list = ( - spu.convert_to(self.min_freq * self.prev_unit, self.unit), - spu.convert_to(self.max_freq * self.prev_unit, self.unit), - self.steps, - ) - else: - self.value = self.value / self.unit * self.prev_unit - - self.prev_active_unit = self.active_unit + @lazy_value_range.setter + def lazy_value_range(self, value: tuple[sp.Expr, sp.Expr, int]) -> None: + log.debug('Lazy Value Range: %s', str(value)) + self.min_freq = spux.sympy_to_python(spux.scale_to_unit(value[0], self.unit)) + self.max_freq = spux.sympy_to_python(spux.scale_to_unit(value[1], self.unit)) + self.steps = value[2] #################### @@ -102,24 +98,22 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket): #################### class PhysicalFreqSocketDef(pyd.BaseModel): socket_type: ct.SocketType = ct.SocketType.PhysicalFreq + is_array: bool = False default_value: SympyExpr = 500 * spux.terahertz - default_unit: SympyExpr | None = None - is_list: bool = False + default_unit: SympyExpr = spux.terahertz min_freq: SympyExpr = 400.0 * spux.terahertz max_freq: SympyExpr = 600.0 * spux.terahertz steps: SympyExpr = 50 def init(self, bl_socket: PhysicalFreqBLSocket) -> None: + bl_socket.unit = self.default_unit + bl_socket.value = self.default_value - bl_socket.is_list = self.is_list - - if self.default_unit: - bl_socket.unit = self.default_unit - - if self.is_list: - bl_socket.value_list = (self.min_freq, self.max_freq, self.steps) + if self.is_array: + bl_socket.active_kind = ct.DataFlowKind.LazyValueRange + bl_socket.lazy_value_range = (self.min_freq, self.max_freq, self.steps) #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length.py index 2a7dae8..53fc832 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/physical/length.py @@ -1,10 +1,10 @@ import bpy -import numpy as np import pydantic as pyd +import sympy as sp import sympy.physics.units as spu -from .....utils import logger from .....utils import extra_sympy_units as spux +from .....utils import logger from .....utils.pydantic_sympy import SympyExpr from ... import contracts as ct from .. import base @@ -58,7 +58,7 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket): def draw_value(self, col: bpy.types.UILayout) -> None: col.prop(self, 'raw_value', text='') - def draw_value_list(self, col: bpy.types.UILayout) -> None: + def draw_lazy_value_range(self, col: bpy.types.UILayout) -> None: col.prop(self, 'min_len', text='Min') col.prop(self, 'max_len', text='Max') col.prop(self, 'steps', text='Steps') @@ -75,28 +75,21 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket): self.raw_value = spux.sympy_to_python(spux.scale_to_unit(value, self.unit)) @property - def value_list(self) -> list[SympyExpr]: - return [ - el * self.unit for el in np.linspace(self.min_len, self.max_len, self.steps) - ] + def lazy_value_range(self) -> ct.LazyDataValueRange: + return ct.LazyDataValueRange( + symbols=set(), + has_unit=True, + start=sp.S(self.min_len) * self.unit, + stop=sp.S(self.max_len) * self.unit, + steps=self.steps, + scaling='lin', + ) - @value_list.setter - def value_list(self, value: tuple[SympyExpr, SympyExpr, int]): - self.min_len, self.max_len, self.steps = [ - spu.convert_to(el, self.unit) / self.unit for el in value[:2] - ] + [value[2]] - - def sync_unit_change(self) -> None: - if self.is_list: - self.value_list = ( - spu.convert_to(self.min_len * self.prev_unit, self.unit), - spu.convert_to(self.max_len * self.prev_unit, self.unit), - self.steps, - ) - else: - self.value = self.value / self.unit * self.prev_unit - - self.prev_active_unit = self.active_unit + @lazy_value_range.setter + def lazy_value_range(self, value: tuple[sp.Expr, sp.Expr, int]) -> None: + self.min_len = spux.sympy_to_python(spux.scale_to_unit(value[0], self.unit)) + self.max_len = spux.sympy_to_python(spux.scale_to_unit(value[1], self.unit)) + self.steps = value[2] #################### @@ -104,24 +97,24 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket): #################### class PhysicalLengthSocketDef(pyd.BaseModel): socket_type: ct.SocketType = ct.SocketType.PhysicalLength + is_array: bool = False default_value: SympyExpr = 1 * spu.um default_unit: SympyExpr | None = None - is_list: bool = False min_len: SympyExpr = 400.0 * spu.nm - max_len: SympyExpr = 600.0 * spu.nm + max_len: SympyExpr = 700.0 * spu.nm steps: SympyExpr = 50 def init(self, bl_socket: PhysicalLengthBLSocket) -> None: - bl_socket.value = self.default_value - bl_socket.is_list = self.is_list - if self.default_unit: bl_socket.unit = self.default_unit - if self.is_list: - bl_socket.value_list = (self.min_len, self.max_len, self.steps) + bl_socket.value = self.default_value + if self.is_array: + bl_socket.active_kind = ct.DataFlowKind.LazyValueRange + bl_socket.lazy_value_range = (self.min_len, self.max_len, self.steps) + ####################