feat: Robust DataFlowKind w/lazy structures.

This is essential for:
- Representing ranges as bounds
- Arbitrary symbolic/numeric representation of spectral distributions
- Parametric representation and JIT of critical-path procedures.

Unfortunately this broke a lot of nodes in small ways.
Next step is to finish the low-hanging fruit nodes + fix the ones we
have.
main
Sofus Albert Høgsbro Rose 2024-04-09 08:50:32 +02:00
parent 54dc46290a
commit a75f697acd
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
21 changed files with 665 additions and 340 deletions

34
TODO.md
View File

@ -1,20 +1,16 @@
# Acute Tasks # Acute Tasks
- [x] Implement Material Import for Maxim Data - [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 # 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 ## Inputs
- [x] Wave Constant - [x] Wave Constant
- [x] Implement export of frequency / wavelength array/range. - [x] Implement export of frequency / wavelength array/range.
- [-] Unit System - [x] Unit System
- [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row. - [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row.
- [ ] Constants / Scientific Constant - [ ] Constants / Scientific Constant
@ -26,7 +22,7 @@
- [ ] Pol: Poincare sphere viz as 3D GN. - [ ] Pol: Poincare sphere viz as 3D GN.
- [x] Constants / Blender Constant - [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. - [ ] 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. - [ ] 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. - [ ] Have a visual indicator for the current download status, with a manual re-download button.
@ -79,7 +75,7 @@
- [x] Point Dipole Source - [x] Point Dipole Source
- [ ] Use a viz mesh, not empty (empty doesn't play well with alpha hashing). - [ ] 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. - [x] Implement an oriented vector input with 3D preview.
- [ ] **IMPORTANT**: Fix the math so that an actually valid construction emerges!! - [ ] **IMPORTANT**: Fix the math so that an actually valid construction emerges!!
- [ ] Uniform Current Source - [ ] Uniform Current Source
@ -120,7 +116,6 @@
- [x] Rewrite to use unit systems properly. - [x] Rewrite to use unit systems properly.
- [ ] Propertly map / implement Enum input sockets to the GN group. - [ ] 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. - [ ] 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 - [ ] Primitive Structures / Plane Structure
- [x] Primitive Structures / Box Structure - [x] Primitive Structures / Box Structure
@ -197,7 +192,7 @@
- [x] Structures / Arrays / Box - [x] Structures / Arrays / Box
- [x] Structures / Arrays / Sphere - [x] Structures / Arrays / Sphere
- [ ] Structures / Arrays / Cylinder - [ ] Structures / Arrays / Cylinder
- [-] Structures / Arrays / Ring - [x] Structures / Arrays / Ring
- [ ] Structures / Arrays / Capsule - [ ] Structures / Arrays / Capsule
- [ ] Structures / Arrays / Cone - [ ] Structures / Arrays / Cone
@ -270,7 +265,7 @@
- [ ] FDTD Sim - [ ] FDTD Sim
- [ ] Sim Domain - [ ] 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
- [ ] Sim Grid Axis - [ ] Sim Grid Axis
@ -376,7 +371,7 @@
## Registration and Contracts ## Registration and Contracts
- [ ] Refactor the node category code; it's ugly. - [ ] 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! - 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 ## Managed Objects
- [ ] Implement ManagedEmpty - [ ] 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 - [ ] 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 ## 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. - [ ] 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. - [ ] Custom callbacks when deleting a node (in `free()`), to ex. delete all previews with the viewer node.
@ -414,7 +410,7 @@
## Many Nodes ## Many Nodes
- [ ] Implement "Steady-State" / "Time Domain" on all relevant Monitor 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. - [ ] Implement LazyValue, including LazyParamValue on a new class of constant-like input nodes that really just emit ex. sympy variables.
- [ ] Medium Features - [ ] Medium Features
- [ ] Accept spatial field. Else, spatial uniformity. - [ ] Accept spatial field. Else, spatial uniformity.
@ -438,11 +434,11 @@
## Version Churn ## Version Churn
- [ ] Migrate to StrEnum sockets (py3.11). - [ ] Migrate to StrEnum sockets (py3.11).
- [ ] Implement drag-and-drop node-from-file via bl4.1 file handler API. - [ ] Implement drag-and-drop node-from-file via bl4.1 file handler API.
- [-] Start thinking about ways around `__annotations__` hacking. - [ ] Start thinking about ways around `__annotations__` hacking.
- [-] Prepare for for multi-input sockets (bl4.2) - [ ] Prepare for for multi-input sockets (bl4.2)
- PR has been merged: <https://projects.blender.org/blender/blender/commit/14106150797a6ce35e006ffde18e78ea7ae67598> (for now, just use the "Combine" node and have seperate socket types for both). - PR has been merged: <https://projects.blender.org/blender/blender/commit/14106150797a6ce35e006ffde18e78ea7ae67598> (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?) - 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. - Will allow for actual volume processing in GeoNodes.
- We might still want/need the jax based stuff after; volume geonodes aren't finalized. - We might still want/need the jax based stuff after; volume geonodes aren't finalized.

View File

@ -53,7 +53,17 @@ from .managed_obj_type import ManagedObjType
#################### ####################
# - Data Flows # - 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 # - Schemas
@ -85,5 +95,13 @@ __all__ = [
'NODE_CAT_LABELS', 'NODE_CAT_LABELS',
'ManagedObjType', 'ManagedObjType',
'DataFlowKind', 'DataFlowKind',
'DataCapabilities',
'DataValue',
'DataValueArray',
'DataValueSpectrum',
'LazyDataValue',
'LazyDataValueRange',
'LazyDataValueSpectrum',
'DataFlowAction',
'schemas', 'schemas',
] ]

View File

@ -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'

View File

@ -1,20 +1,34 @@
import dataclasses
import enum 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. """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. 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: Attributes:
Value: A value usable without new data. Value: A value without any unknown symbols.
- Basic types aka. float, int, list, string, etc. . - Basic types aka. float, int, list, string, etc. .
- Exotic (immutable-ish) types aka. numpy array, KDTree, etc. . - Exotic (immutable-ish) types aka. numpy array, KDTree, etc. .
- A usable constructed object, ex. a `tidy3d.Box`. - A usable constructed object, ex. a `tidy3d.Box`.
- Expressions (`sp.Expr`) that don't have unknown variables. - Expressions (`sp.Expr`) that don't have unknown variables.
- Lazy sequences aka. generators, with all data bound. - 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. 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. - 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). 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() 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,
)

View File

@ -37,7 +37,6 @@ class ManagedBLImage(ct.schemas.ManagedObj):
return return
# ...AND Desired Image Name is Taken # ...AND Desired Image Name is Taken
else:
msg = f'Desired name {value} for BL image is taken' msg = f'Desired name {value} for BL image is taken'
raise ValueError(msg) raise ValueError(msg)
@ -48,10 +47,7 @@ class ManagedBLImage(ct.schemas.ManagedObj):
## - `set_name` is allowed to change the name; nodes account for this. ## - `set_name` is allowed to change the name; nodes account for this.
def free(self): def free(self):
if not (bl_image := bpy.data.images.get(self.name)): if 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) bpy.data.images.remove(bl_image)
#################### ####################
@ -166,7 +162,12 @@ class ManagedBLImage(ct.schemas.ManagedObj):
# Compute Plot Dimensions # Compute Plot Dimensions
aspect_ratio = _width_inches / _height_inches 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 # Create MPL Figure, Axes, and Compute Figure Geometry
fig, ax = plt.subplots( fig, ax = plt.subplots(
figsize=[_width_inches, _height_inches], figsize=[_width_inches, _height_inches],

View File

@ -352,11 +352,11 @@ class MaxwellSimNode(bpy.types.Node):
self, self,
value: dict[str, ct.schemas.SocketDef], value: dict[str, ct.schemas.SocketDef],
) -> None: ) -> None:
log.info( # Prune Loose Sockets
'Setting Loose Input Sockets on "%s" to "%s"', self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER
self.bl_label, self.sync_sockets()
str(value),
) # Install New Sockets
if not value: if not value:
self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER
else: else:
@ -364,13 +364,17 @@ class MaxwellSimNode(bpy.types.Node):
# Synchronize Sockets # Synchronize Sockets
self.sync_sockets() self.sync_sockets()
## TODO: Perhaps re-init() all loose sockets anyway?
@loose_output_sockets.setter @loose_output_sockets.setter
def loose_output_sockets( def loose_output_sockets(
self, self,
value: dict[str, ct.schemas.SocketDef], value: dict[str, ct.schemas.SocketDef],
) -> None: ) -> None:
# Prune Loose Sockets
self.ser_loose_output_sockets = _DEFAULT_LOOSE_SOCKET_SER
self.sync_sockets()
# Install New Sockets
if not value: if not value:
self.ser_loose_output_sockets = _DEFAULT_LOOSE_SOCKET_SER self.ser_loose_output_sockets = _DEFAULT_LOOSE_SOCKET_SER
else: else:
@ -378,7 +382,6 @@ class MaxwellSimNode(bpy.types.Node):
# Synchronize Sockets # Synchronize Sockets
self.sync_sockets() self.sync_sockets()
## TODO: Perhaps re-init() all loose sockets anyway?
#################### ####################
# - Socket Management # - Socket Management
@ -567,17 +570,11 @@ class MaxwellSimNode(bpy.types.Node):
msg = f'Property {prop_name} not defined on socket {self}' msg = f'Property {prop_name} not defined on socket {self}'
raise RuntimeError(msg) 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( def trigger_action(
self, self,
action: typx.Literal[ action: ct.DataFlowAction,
'enable_lock',
'disable_lock',
'value_changed',
'show_preview',
'show_plot',
],
socket_name: ct.SocketName | None = None, socket_name: ct.SocketName | None = None,
prop_name: ct.SocketName | None = None, prop_name: ct.SocketName | None = None,
) -> None: ) -> None:
@ -586,15 +583,15 @@ class MaxwellSimNode(bpy.types.Node):
Invalidates (recursively) the cache of any managed object or Invalidates (recursively) the cache of any managed object or
output socket method that implicitly depends on this input socket. output socket method that implicitly depends on this input socket.
""" """
#log.debug( # log.debug(
# 'Action "%s" Triggered in "%s" (socket_name="%s", prop_name="%s")', # 'Action "%s" Triggered in "%s" (socket_name="%s", prop_name="%s")',
# action, # action,
# self.name, # self.name,
# socket_name, # socket_name,
# prop_name, # prop_name,
#) # )
# Forwards Chains # Forwards Chains
if action == 'value_changed': if action == ct.DataFlowAction.DataChanged:
# Run User Callbacks # Run User Callbacks
## Careful with these, they run BEFORE propagation... ## Careful with these, they run BEFORE propagation...
## ...because later-chain methods may rely on the results of this. ## ...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 and socket_name in self.loose_input_sockets
) )
): ):
#log.debug( # log.debug(
# 'Running Value-Change Callback "%s" in "%s")', # 'Running Value-Change Callback "%s" in "%s")',
# method.__name__, # method.__name__,
# self.name, # self.name,
#) # )
method(self) method(self)
# Propagate via Output Sockets # Propagate via Output Sockets
@ -623,21 +620,21 @@ class MaxwellSimNode(bpy.types.Node):
bl_socket.trigger_action(action) bl_socket.trigger_action(action)
# Backwards Chains # Backwards Chains
elif action == 'enable_lock': elif action == ct.DataFlowAction.EnableLock:
self.locked = True self.locked = True
## Propagate via Input Sockets ## Propagate via Input Sockets
for bl_socket in self.active_bl_sockets('input'): for bl_socket in self.active_bl_sockets('input'):
bl_socket.trigger_action(action) bl_socket.trigger_action(action)
elif action == 'disable_lock': elif action == ct.DataFlowAction.DisableLock:
self.locked = False self.locked = False
## Propagate via Input Sockets ## Propagate via Input Sockets
for bl_socket in self.active_bl_sockets('input'): for bl_socket in self.active_bl_sockets('input'):
bl_socket.trigger_action(action) bl_socket.trigger_action(action)
elif action == 'show_preview': elif action == ct.DataFlowAction.ShowPreview:
# Run User Callbacks # Run User Callbacks
## "On Show Preview" callbacks are 'on_value_changed' callbacks... ## "On Show Preview" callbacks are 'on_value_changed' callbacks...
## ...which simply hook into the 'preview_active' property. ## ...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'): for bl_socket in self.active_bl_sockets('input'):
bl_socket.trigger_action(action) bl_socket.trigger_action(action)
elif action == 'show_plot': elif action == ct.DataFlowAction.ShowPlot:
# Run User Callbacks # Run User Callbacks
## These shouldn't change any data, BUT... ## These shouldn't change any data, BUT...
## ...because they can stop propagation, they should go first. ## ...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 bl_socket.is_linked and bl_socket.locked
for bl_socket in self.inputs.values() for bl_socket in self.inputs.values()
): ):
self.trigger_action('disable_lock') self.trigger_action(ct.DataFlowAction.DisableLock)
# Free Managed Objects # Free Managed Objects
for managed_obj in self.managed_objs.values(): for managed_obj in self.managed_objs.values():

View File

@ -69,11 +69,12 @@ PropName: typ.TypeAlias = str
def event_decorator( def event_decorator(
action_type: EventCallbackType, action_type: EventCallbackType,
extra_data: EventCallbackData, extra_data: EventCallbackData,
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
props: set[PropName] = frozenset(), props: set[PropName] = frozenset(),
managed_objs: set[ManagedObjName] = frozenset(), managed_objs: set[ManagedObjName] = frozenset(),
input_sockets: set[ct.SocketName] = frozenset(), input_sockets: set[ct.SocketName] = frozenset(),
input_socket_kinds: dict[ct.SocketName, ct.DataFlowKind] = MappingProxyType({}),
output_sockets: set[ct.SocketName] = frozenset(), output_sockets: set[ct.SocketName] = frozenset(),
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,
unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}), unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}),
@ -87,11 +88,11 @@ def event_decorator(
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. 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. 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. 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. 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.
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. 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. 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. 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 ## Compute Requested Input Sockets
if input_sockets: if input_sockets:
_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 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. ## Then, convert the symbol-less sympy scalar to a python type.
for input_socket_name, unit_system_id in scale_input_sockets.items(): for input_socket_name, unit_system_id in scale_input_sockets.items():
unit_system = unit_systems[unit_system_id] unit_system = unit_systems[unit_system_id]
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( _input_sockets[input_socket_name] = spux.sympy_to_python(
spux.scale_to_unit( spux.scale_to_unit(
_input_sockets[input_socket_name], _input_sockets[input_socket_name],
unit_system[node.inputs[input_socket_name].socket_type], 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} method_kw_args |= {'input_sockets': _input_sockets}
## Compute Requested Output Sockets ## Compute Requested Output Sockets
if output_sockets: if output_sockets:
_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 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. ## Then, convert the symbol-less sympy scalar to a python type.
for output_socket_name, unit_system_id in scale_output_sockets.items(): for output_socket_name, unit_system_id in scale_output_sockets.items():
unit_system = unit_systems[unit_system_id] 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( _output_sockets[output_socket_name] = spux.sympy_to_python(
spux.scale_to_unit( spux.scale_to_unit(
_output_sockets[output_socket_name], _output_sockets[output_socket_name],
unit_system[node.outputs[output_socket_name].socket_type], 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} 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: if all_loose_input_sockets:
_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 for input_socket_name in node.loose_input_sockets
} }
method_kw_args |= {'loose_input_sockets': _loose_input_sockets} method_kw_args |= {'loose_input_sockets': _loose_input_sockets}
@ -204,7 +239,7 @@ def event_decorator(
## Compute All Loose Output Sockets ## Compute All Loose Output Sockets
if all_loose_output_sockets: if all_loose_output_sockets:
_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 for output_socket_name in node.loose_output_sockets
} }
method_kw_args |= {'loose_output_sockets': _loose_output_sockets} method_kw_args |= {'loose_output_sockets': _loose_output_sockets}
@ -221,10 +256,10 @@ def event_decorator(
# Set Decorated Attributes and Return # Set Decorated Attributes and Return
## Fix Introspection + Documentation ## Fix Introspection + Documentation
#decorated.__name__ = method.__name__ # decorated.__name__ = method.__name__
#decorated.__module__ = method.__module__ # decorated.__module__ = method.__module__
#decorated.__qualname__ = method.__qualname__ # decorated.__qualname__ = method.__qualname__
#decorated.__doc__ = method.__doc__ # decorated.__doc__ = method.__doc__
## Add Spice ## Add Spice
decorated.action_type = action_type decorated.action_type = action_type

View File

@ -1,5 +1,6 @@
import typing as typ import typing as typ
import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
@ -15,106 +16,92 @@ class WaveConstantNode(base.MaxwellSimNode):
bl_label = 'Wave Constant' bl_label = 'Wave Constant'
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
# Single 'Wavelength': {},
'Vacuum WL': { 'Frequency': {},
'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,
),
},
} }
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( @events.computes_output_socket(
'WL', 'WL',
input_sockets={'WL'}, all_loose_input_sockets=True,
) )
def compute_vacwl_from_vacwl(self, input_sockets: dict) -> sp.Expr: def compute_wl(self, loose_input_sockets: dict) -> sp.Expr:
return input_sockets['WL'] 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( @events.computes_output_socket(
'WL', 'Freq',
input_sockets={'Freq'}, all_loose_input_sockets=True,
) )
def compute_freq_from_vacwl(self, input_sockets: dict) -> sp.Expr: def compute_freq(self, loose_input_sockets: dict) -> sp.Expr:
return constants.vac_speed_of_light / input_sockets['Freq'] if (freq := loose_input_sockets.get('Freq')) is not None:
return freq
#################### wl = loose_input_sockets.get('WL')
# - Event Methods: Listy Output
#################### if isinstance(wl, ct.LazyDataValueRange):
@events.computes_output_socket( return wl.rescale_bounds(
'WLs', lambda bound: constants.vac_speed_of_light / bound, reverse=True
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]
msg = 'Vac WL and Freq are both None' return constants.vac_speed_of_light / wl
raise RuntimeError(msg)
@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)
#################### ####################
# - Event Methods # - Event Methods
#################### ####################
@events.on_value_changed(prop_name='active_socket_set', props={'active_socket_set'}) @events.on_value_changed(
def on_active_socket_set_changed(self, props: dict): prop_name={'active_socket_set', 'use_range'},
# Singular: Normal Output Sockets props={'active_socket_set', 'use_range'},
if props['active_socket_set'] in {'Vacuum WL', 'Frequency'}: )
self.loose_output_sockets = {} def on_input_spec_change(self, props: dict):
self.loose_output_sockets = { if props['active_socket_set'] == 'Wavelength':
'Freq': sockets.PhysicalFreqSocketDef(), self.loose_input_sockets = {
'WL': sockets.PhysicalLengthSocketDef(), '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: else:
msg = f"Active socket set invalid for wave constant: {props['active_socket_set']}" self.loose_input_sockets = {
raise RuntimeError(msg) 'Freq': sockets.PhysicalFreqSocketDef(
is_array=props['use_range'],
default_value=600 * spux.THz,
default_unit=spux.THz,
)
}
@events.on_init() self.loose_output_sockets = {
def on_init(self): 'WL': sockets.PhysicalLengthSocketDef(is_array=props['use_range']),
self.on_active_socket_set_changed() '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()
#################### ####################

View File

@ -34,7 +34,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
'Freq Domain': { 'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef( 'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True, is_array=True,
), ),
}, },
'Time Domain': { 'Time Domain': {

View File

@ -34,7 +34,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
'Freq Domain': { 'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef( 'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True, is_array=True,
), ),
}, },
'Time Domain': { 'Time Domain': {
@ -74,10 +74,14 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
'Freqs', 'Freqs',
'Direction', 'Direction',
}, },
input_socket_kinds={
'Freqs': ct.LazyDataValueRange,
},
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',
'Samples/Space': 'Tidy3DUnits', 'Samples/Space': 'Tidy3DUnits',
'Rec Start': 'Tidy3DUnits', 'Rec Start': 'Tidy3DUnits',
'Rec Stop': 'Tidy3DUnits', 'Rec Stop': 'Tidy3DUnits',
@ -88,8 +92,6 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
direction = '+' if input_sockets['Direction'] else '-' direction = '+' if input_sockets['Direction'] else '-'
if props['active_socket_set'] == 'Freq Domain': if props['active_socket_set'] == 'Freq Domain':
freqs = input_sockets['Freqs']
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'],
@ -101,9 +103,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
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=input_sockets['Samples/Space'],
freqs=[ freqs=input_sockets['Freqs'].realize().values,
float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
],
normal_dir=direction, normal_dir=direction,
) )

View File

@ -180,7 +180,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
#################### ####################
def sync_lock_tree(self, context): def sync_lock_tree(self, context):
if self.lock_tree: if self.lock_tree:
self.trigger_action('enable_lock') self.trigger_action(ct.DataFlowAction.EnableLock)
self.locked = False self.locked = False
for bl_socket in self.inputs: for bl_socket in self.inputs:
if bl_socket.name == 'FDTD Sim': if bl_socket.name == 'FDTD Sim':
@ -188,7 +188,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
bl_socket.locked = False bl_socket.locked = False
else: else:
self.trigger_action('disable_lock') self.trigger_action(ct.DataFlowAction.DisableLock)
self.sync_prop('lock_tree', context) self.sync_prop('lock_tree', context)

View File

@ -131,7 +131,7 @@ class ViewerNode(base.MaxwellSimNode):
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.info('Enabling 2D Plot from "%s"', self.name) log.info('Enabling 2D Plot from "%s"', self.name)
self.trigger_action('show_plot') self.trigger_action(ct.DataFlowAction.ShowPlot)
@events.on_value_changed( @events.on_value_changed(
prop_name='auto_3d_preview', prop_name='auto_3d_preview',
@ -145,7 +145,7 @@ class ViewerNode(base.MaxwellSimNode):
# Trigger Preview Action # Trigger Preview Action
if self.inputs['Data'].is_linked and props['auto_3d_preview']: if self.inputs['Data'].is_linked and props['auto_3d_preview']:
log.info('Enabling 3D Previews from "%s"', self.name) log.info('Enabling 3D Previews from "%s"', self.name)
self.trigger_action('show_preview') self.trigger_action(ct.DataFlowAction.ShowPreview)
@events.on_value_changed( @events.on_value_changed(
socket_name='Data', socket_name='Data',

View File

@ -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

View File

@ -65,11 +65,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type] cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type]
# Setup List # Setup List
cls.__annotations__['is_list'] = bpy.props.BoolProperty( cls.__annotations__['active_kind'] = bpy.props.StringProperty(
name='Is List', name='Active Kind',
description='Whether or not a particular socket is a list type socket', description='The active Data Flow Kind',
default=False, default=str(ct.DataFlowKind.Value),
update=lambda self, context: self.sync_is_list(context), update=lambda self, _: self.sync_active_kind(),
) )
# Configure Use of Units # Configure Use of Units
@ -90,7 +90,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
for unit_name, unit_value in socket_units['values'].items() for unit_name, unit_value in socket_units['values'].items()
], ],
default=socket_units['default'], default=socket_units['default'],
update=lambda self, context: self.sync_unit_change(), update=lambda self, _: self.sync_unit_change(),
) )
# Previous Unit (for conversion) # Previous Unit (for conversion)
@ -103,13 +103,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
#################### ####################
def trigger_action( def trigger_action(
self, self,
action: typx.Literal[ action: ct.DataFlowAction,
'enable_lock',
'disable_lock',
'value_changed',
'show_preview',
'show_plot',
],
) -> None: ) -> None:
"""Called whenever the socket's output value has changed. """Called whenever the socket's output value has changed.
@ -157,24 +151,30 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
#################### ####################
# - Action Chain: Event Handlers # - Action Chain: Event Handlers
#################### ####################
def sync_is_list(self, context: bpy.types.Context): def sync_active_kind(self):
"""Called when the "is_list_ property has been updated.""" """Called when the active data flow kind of the socket changes.
if self.is_list:
if self.use_units:
self.display_shape = 'SQUARE_DOT'
else:
self.display_shape = 'SQUARE'
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.""" """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}' msg = f'Property {prop_name} not defined on socket {self}'
raise RuntimeError(msg) raise RuntimeError(msg)
self.trigger_action('value_changed')
def sync_link_added(self, link) -> bool: def sync_link_added(self, link) -> bool:
"""Called when a link has been added to this (input) socket. """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" msg = "Tried to sync 'link add' on output socket"
raise RuntimeError(msg) raise RuntimeError(msg)
self.trigger_action('value_changed') self.trigger_action(ct.DataFlowAction.DataChanged)
return True return True
@ -201,63 +201,78 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
msg = "Tried to sync 'link add' on output socket" msg = "Tried to sync 'link add' on output socket"
raise RuntimeError(msg) raise RuntimeError(msg)
self.trigger_action('value_changed') self.trigger_action(ct.DataFlowAction.DataChanged)
return True return True
#################### ####################
# - Data Chain # - Data Chain
#################### ####################
# Capabilities
@property @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 raise NotImplementedError
@value.setter @value.setter
def value(self, value: typ.Any) -> None: def value(self, value: ct.DataValue) -> None:
raise NotImplementedError raise NotImplementedError
# ValueArray
@property @property
def value_list(self) -> typ.Any: def value_array(self) -> ct.DataValueArray:
return [self.value]
@value_list.setter
def value_list(self, value: typ.Any) -> None:
raise NotImplementedError raise NotImplementedError
def value_as_unit_system( @value_array.setter
self, unit_system: dict, dimensionless: bool = True def value_array(self, value: ct.DataValueArray) -> None:
) -> typ.Any: raise NotImplementedError
## 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
)
# ValueSpectrum
@property @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 raise NotImplementedError
@lazy_value.setter @lazy_value.setter
def lazy_value(self, lazy_value: typ.Any) -> None: def lazy_value(self, lazy_value: ct.LazyDataValue) -> None:
raise NotImplementedError raise NotImplementedError
# LazyValueRange
@property @property
def lazy_value_list(self) -> typ.Any: def lazy_value_range(self) -> ct.LazyDataValueRange:
return [self.lazy_value]
@lazy_value_list.setter
def lazy_value_list(self, value: typ.Any) -> None:
raise NotImplementedError raise NotImplementedError
@lazy_value_range.setter
def lazy_value_range(self, value: tuple[ct.DataValue, ct.DataValue, int]) -> None:
raise NotImplementedError
# LazyValueSpectrum
@property @property
def capabilities(self) -> None: def lazy_value_spectrum(self) -> ct.LazyDataValueSpectrum:
raise NotImplementedError raise NotImplementedError
@lazy_value_spectrum.setter
def lazy_value_spectrum(self, value: ct.LazyDataValueSpectrum) -> None:
raise NotImplementedError
####################
# - Data Chain Computation
####################
def _compute_data( def _compute_data(
self, self,
kind: ct.DataFlowKind = ct.DataFlowKind.Value, kind: ct.DataFlowKind = ct.DataFlowKind.Value,
@ -266,18 +281,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
**NOTE**: Low-level method. Use `compute_data` instead. **NOTE**: Low-level method. Use `compute_data` instead.
""" """
if kind == ct.DataFlowKind.Value: return {
if self.is_list: ct.DataFlowKind.Value: lambda: self.value,
return self.value_list ct.DataFlowKind.ValueArray: lambda: self.value_array,
return self.value ct.DataFlowKind.ValueSpectrum: lambda: self.value_spectrum,
if kind == ct.DataFlowKind.LazyValue: ct.DataFlowKind.LazyValue: lambda: self.lazy_value,
if self.is_list: ct.DataFlowKind.LazyValueRange: lambda: self.lazy_value_range,
return self.lazy_value_list ct.DataFlowKind.LazyValueSpectrum: lambda: self.lazy_value_spectrum,
return self.lazy_value }[kind]()
if kind == ct.DataFlowKind.Capabilities:
return self.capabilities
return None msg = f'socket._compute_data was called with invalid kind "{kind}"'
raise RuntimeError(msg)
def compute_data( def compute_data(
self, self,
@ -291,22 +305,25 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
- If output socket, ask node for data. - If output socket, ask node for data.
""" """
# Compute Output Socket # Compute Output Socket
## List-like sockets guarantee that a list of a thing is passed.
if self.is_output: if self.is_output:
res = self.node.compute_output(self.name, kind=kind) return self.node.compute_output(self.name, kind=kind)
if self.is_list and not isinstance(res, list):
return [res]
return res
# Compute Input Socket # Compute Input Socket
## Unlinked: Retrieve Socket Value ## Unlinked: Retrieve Socket Value
if not self.is_linked: if not self.is_linked:
return self._compute_data(kind) 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] 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: if len(linked_values) == 1:
return linked_values[0] return linked_values[0]
return linked_values return linked_values
@ -361,14 +378,16 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Can be overridden if more specific logic is required. Can be overridden if more specific logic is required.
""" """
prev_value = self.value / self.unit * self.prev_unit if self.active_kind == ct.DataFlowKind.Value:
## After changing units, self.value is expressed in the wrong unit. self.value = self.value / self.unit * self.prev_unit
## - Therefore, we removing the new unit, and re-add the prev unit.
## - Using only self.value avoids implementation-specific details.
self.value = spu.convert_to( elif self.active_kind == ct.DataFlowKind.LazyValueRange:
prev_value, self.unit lazy_value_range = self.lazy_value_range
) ## Now, the unit conversion can be done correctly. 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 self.prev_active_unit = self.active_unit
@ -458,12 +477,16 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
elif self.locked: elif self.locked:
row.enabled = False row.enabled = False
# Value Column(s) # Data Column(s)
col = row.column(align=True) col = row.column(align=True)
if self.is_list: {
self.draw_value_list(col) ct.DataFlowKind.Value: self.draw_value,
else: ct.DataFlowKind.ValueArray: self.draw_value_array,
self.draw_value(col) 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( def draw_output(
self, self,
@ -489,14 +512,23 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
""" """
row.label(text=text) row.label(text=text)
####################
# - DataFlowKind draw() Methods
####################
def draw_value(self, col: bpy.types.UILayout) -> None: 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: def draw_value_spectrum(self, col: bpy.types.UILayout) -> None:
"""Called to draw the value list column in unlinked input sockets. 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

View File

@ -11,6 +11,14 @@ class AnyBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Any socket_type = ct.SocketType.Any
bl_label = 'Any' bl_label = 'Any'
@property
def capabilities(self):
return ct.DataCapabilities(
socket_type=self.socket_type,
active_kind=self.active_kind,
is_universal=True,
)
#################### ####################
# - Socket Configuration # - Socket Configuration

View File

@ -18,7 +18,8 @@ class MaxwellMonitorSocketDef(pyd.BaseModel):
is_list: bool = False is_list: bool = False
def init(self, bl_socket: MaxwellMonitorBLSocket) -> None: def init(self, bl_socket: MaxwellMonitorBLSocket) -> None:
bl_socket.is_list = self.is_list if self.is_list:
bl_socket.active_kind = ct.DataValueArray
#################### ####################

View File

@ -18,7 +18,8 @@ class MaxwellSourceSocketDef(pyd.BaseModel):
is_list: bool = False is_list: bool = False
def init(self, bl_socket: MaxwellSourceBLSocket) -> None: def init(self, bl_socket: MaxwellSourceBLSocket) -> None:
bl_socket.is_list = self.is_list if self.is_list:
bl_socket.active_kind = ct.DataValueArray
#################### ####################

View File

@ -18,7 +18,8 @@ class MaxwellStructureSocketDef(pyd.BaseModel):
is_list: bool = False is_list: bool = False
def init(self, bl_socket: MaxwellStructureBLSocket) -> None: def init(self, bl_socket: MaxwellStructureBLSocket) -> None:
bl_socket.is_list = self.is_list if self.is_list:
bl_socket.active_kind = ct.DataValueArray
#################### ####################

View File

@ -1,13 +1,16 @@
import bpy import bpy
import numpy as np
import pydantic as pyd import pydantic as pyd
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.pydantic_sympy import SympyExpr from .....utils.pydantic_sympy import SympyExpr
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
log = logger.get(__name__)
#################### ####################
# - Blender Socket # - Blender Socket
@ -55,7 +58,7 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'raw_value', text='') 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, 'min_freq', text='Min')
col.prop(self, 'max_freq', text='Max') col.prop(self, 'max_freq', text='Max')
col.prop(self, 'steps', text='Steps') col.prop(self, 'steps', text='Steps')
@ -69,32 +72,25 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
@value.setter @value.setter
def value(self, value: SympyExpr) -> None: 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 @property
def value_list(self) -> list[SympyExpr]: def lazy_value_range(self) -> ct.LazyDataValueRange:
return [ return ct.LazyDataValueRange(
el * self.unit symbols=set(),
for el in np.linspace(self.min_freq, self.max_freq, self.steps) has_unit=True,
] start=sp.S(self.min_freq) * self.unit,
stop=sp.S(self.max_freq) * self.unit,
@value_list.setter steps=self.steps,
def value_list(self, value: tuple[SympyExpr, SympyExpr, int]): scaling='lin',
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): class PhysicalFreqSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.PhysicalFreq socket_type: ct.SocketType = ct.SocketType.PhysicalFreq
is_array: bool = False
default_value: SympyExpr = 500 * spux.terahertz default_value: SympyExpr = 500 * spux.terahertz
default_unit: SympyExpr | None = None default_unit: SympyExpr = spux.terahertz
is_list: bool = False
min_freq: SympyExpr = 400.0 * spux.terahertz min_freq: SympyExpr = 400.0 * spux.terahertz
max_freq: SympyExpr = 600.0 * spux.terahertz max_freq: SympyExpr = 600.0 * spux.terahertz
steps: SympyExpr = 50 steps: SympyExpr = 50
def init(self, bl_socket: PhysicalFreqBLSocket) -> None: def init(self, bl_socket: PhysicalFreqBLSocket) -> None:
bl_socket.value = self.default_value
bl_socket.is_list = self.is_list
if self.default_unit:
bl_socket.unit = self.default_unit bl_socket.unit = self.default_unit
if self.is_list: bl_socket.value = self.default_value
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)
#################### ####################

View File

@ -1,10 +1,10 @@
import bpy import bpy
import numpy as np
import pydantic as pyd import pydantic as pyd
import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
from .....utils import logger
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import logger
from .....utils.pydantic_sympy import SympyExpr from .....utils.pydantic_sympy import SympyExpr
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
@ -58,7 +58,7 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'raw_value', text='') 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, 'min_len', text='Min')
col.prop(self, 'max_len', text='Max') col.prop(self, 'max_len', text='Max')
col.prop(self, 'steps', text='Steps') 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)) self.raw_value = spux.sympy_to_python(spux.scale_to_unit(value, self.unit))
@property @property
def value_list(self) -> list[SympyExpr]: def lazy_value_range(self) -> ct.LazyDataValueRange:
return [ return ct.LazyDataValueRange(
el * self.unit for el in np.linspace(self.min_len, self.max_len, self.steps) symbols=set(),
] has_unit=True,
start=sp.S(self.min_len) * self.unit,
@value_list.setter stop=sp.S(self.max_len) * self.unit,
def value_list(self, value: tuple[SympyExpr, SympyExpr, int]): steps=self.steps,
self.min_len, self.max_len, self.steps = [ scaling='lin',
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): class PhysicalLengthSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.PhysicalLength socket_type: ct.SocketType = ct.SocketType.PhysicalLength
is_array: bool = False
default_value: SympyExpr = 1 * spu.um default_value: SympyExpr = 1 * spu.um
default_unit: SympyExpr | None = None default_unit: SympyExpr | None = None
is_list: bool = False
min_len: SympyExpr = 400.0 * spu.nm min_len: SympyExpr = 400.0 * spu.nm
max_len: SympyExpr = 600.0 * spu.nm max_len: SympyExpr = 700.0 * spu.nm
steps: SympyExpr = 50 steps: SympyExpr = 50
def init(self, bl_socket: PhysicalLengthBLSocket) -> None: def init(self, bl_socket: PhysicalLengthBLSocket) -> None:
bl_socket.value = self.default_value
bl_socket.is_list = self.is_list
if self.default_unit: if self.default_unit:
bl_socket.unit = self.default_unit bl_socket.unit = self.default_unit
if self.is_list: bl_socket.value = self.default_value
bl_socket.value_list = (self.min_len, self.max_len, self.steps) if self.is_array:
bl_socket.active_kind = ct.DataFlowKind.LazyValueRange
bl_socket.lazy_value_range = (self.min_len, self.max_len, self.steps)
#################### ####################