diff --git a/src/blender_maxwell/assets/structures/primitives/box.blend b/src/blender_maxwell/assets/structures/primitives/box.blend
index 755b978..2ee7e2a 100644
--- a/src/blender_maxwell/assets/structures/primitives/box.blend
+++ b/src/blender_maxwell/assets/structures/primitives/box.blend
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:59d82a5231448784c9b1107fa439ff500e376b9e4ee906a95022476a8b7755d8
-size 852005
+oid sha256:79bdbdae875b5f005f2e981c1d0e9c303a52bf7b97e1744c4ef73e155d41c75b
+size 918460
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 b1d9b58..0828da2 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
@@ -46,10 +46,11 @@ from .flow_kinds import (
ArrayFlow,
CapabilitiesFlow,
FlowKind,
- InfoFlow,
- RangeFlow,
FuncFlow,
+ InfoFlow,
ParamsFlow,
+ PreviewsFlow,
+ RangeFlow,
ScalingMode,
ValueFlow,
)
@@ -118,6 +119,7 @@ __all__ = [
'CapabilitiesFlow',
'FlowKind',
'InfoFlow',
+ 'PreviewsFlow',
'RangeFlow',
'FuncFlow',
'ParamsFlow',
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py
index 46e62eb..244e54b 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py
@@ -230,7 +230,6 @@ class BLSocketType(enum.StrEnum):
return {
# Blender
# Basic
- BLST.Bool: MT.Bool,
# Float
BLST.Float: MT.Real,
BLST.FloatAngle: MT.Real,
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py
index d8bdffc..083873f 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_events.py
@@ -35,10 +35,6 @@ class FlowEvent(enum.StrEnum):
This event can lock a subset of the node tree graph.
DisableLock: Indicates that the node/socket should disable locking.
This event can unlock part of a locked subgraph.
- ShowPreview: Indicates that the node/socket should enable its primary preview.
- This should be used if a more specific preview-esque event doesn't apply.
- ShowPlot: Indicates that the node/socket should enable its plotted preview.
- This should generally be used if the node is rendering to an image, for viewing through the Blender image editor.
LinkChanged: Indicates that a link to a node/socket was added/removed.
Is translated to `DataChanged` on sockets before propagation.
DataChanged: Indicates that data flowing through a node/socket was altered.
@@ -50,15 +46,12 @@ class FlowEvent(enum.StrEnum):
EnableLock = enum.auto()
DisableLock = enum.auto()
- # Preview Events
- ShowPreview = enum.auto()
- ShowPlot = enum.auto()
-
# Data Events
LinkChanged = enum.auto()
DataChanged = enum.auto()
# Non-Triggered Events
+ ShowPlot = enum.auto()
OutputRequested = enum.auto()
# Properties
@@ -79,9 +72,6 @@ class FlowEvent(enum.StrEnum):
# Lock Events
FlowEvent.EnableLock: 'input',
FlowEvent.DisableLock: 'input',
- # Preview Events
- FlowEvent.ShowPreview: 'input',
- FlowEvent.ShowPlot: 'input',
# Data Events
FlowEvent.LinkChanged: 'output',
FlowEvent.DataChanged: 'output',
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/__init__.py
index a050944..619fdb7 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/__init__.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/__init__.py
@@ -21,6 +21,7 @@ from .info import InfoFlow
from .lazy_func import FuncFlow
from .lazy_range import RangeFlow, ScalingMode
from .params import ParamsFlow
+from .previews import PreviewsFlow
from .value import ValueFlow
__all__ = [
@@ -32,5 +33,6 @@ __all__ = [
'ScalingMode',
'FuncFlow',
'ParamsFlow',
+ 'PreviewsFlow',
'ValueFlow',
]
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/array.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/array.py
index d75afd9..0303908 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/array.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/array.py
@@ -21,7 +21,6 @@ import typing as typ
import jaxtyping as jtyp
import numpy as np
import sympy as sp
-import sympy.physics.units as spu
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
@@ -117,13 +116,19 @@ class ArrayFlow:
new_unit: An (optional) new unit to scale the result to.
"""
# Compile JAX-Compatible Rescale Function
+ ## -> Generally, we try to keep things nice and rational.
+ ## -> However, too-large ints may cause JAX to suffer from an overflow.
+ ## -> Jax works in 32-bit domain by default, for performance.
+ ## -> While it can be adjusted, that would also have tradeoffs.
+ ## -> Instead, a quick .n() turns all the big-ints into floats.
+ ## -> Not super satisfying, but hey - it's all numerical anyway.
a = self.mathtype.sp_symbol_a
rescale_expr = (
spux.scale_to_unit(rescale_func(a * self.unit), new_unit)
if self.unit is not None
else rescale_func(a)
)
- _rescale_func = sp.lambdify(a, rescale_expr, 'jax')
+ _rescale_func = sp.lambdify(a, rescale_expr.n(), 'jax')
values = _rescale_func(self.values)
# Return ArrayFlow
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/capabilities.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/capabilities.py
index 6a7e804..800b4d4 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/capabilities.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/capabilities.py
@@ -24,9 +24,30 @@ from .flow_kinds import FlowKind
@dataclasses.dataclass(frozen=True, kw_only=True)
class CapabilitiesFlow:
+ """Describes the compatibility relationship between two sockets, which governs whether they can be linked.
+
+ By default, socket type (which may impact color) and active `FlowKind` (which impacts shape) must match in order for two sockets to be compatible.
+
+ However, in many cases, more expressiveness beyond this naive constraint is desirable.
+ For example:
+
+ - Allow any socket to be linked to the `ViewerNode` input.
+ - Allow only _angled_ sources to be passed as inputs to the input-derived `BlochBoundCond` node.
+ - Allow `Expr:Value` to connect to `Expr:Func`, but only allow the converse if `PhysicalType`, `MathType`, and `Size` match.
+
+ In many cases, it's desirable
+
+ """
+
+ # Defaults
socket_type: SocketType
active_kind: FlowKind
+
+ # Relationships
allow_out_to_in: dict[FlowKind, FlowKind] = dataclasses.field(default_factory=dict)
+ allow_out_to_in_if_matches: dict[FlowKind, (FlowKind, bool)] = dataclasses.field(
+ default_factory=dict
+ )
is_universal: bool = False
@@ -41,12 +62,19 @@ class CapabilitiesFlow:
def is_compatible_with(self, other: typ.Self) -> bool:
return other.is_universal or (
- self.socket_type == other.socket_type
+ self.socket_type is other.socket_type
and (
- self.active_kind == other.active_kind
+ self.active_kind is other.active_kind
or (
other.active_kind in other.allow_out_to_in
- and self.active_kind == other.allow_out_to_in[other.active_kind]
+ and self.active_kind is other.allow_out_to_in[other.active_kind]
+ )
+ or (
+ other.active_kind in other.allow_out_to_in_if_matches
+ and self.active_kind
+ is other.allow_out_to_in_if_matches[other.active_kind][0]
+ and self.allow_out_to_in_if_matches[other.active_kind][1]
+ == other.allow_out_to_in_if_matches[other.active_kind][1]
)
)
# == Constraint
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/flow_kinds.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/flow_kinds.py
index d4058e9..8db2fc9 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/flow_kinds.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/flow_kinds.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
import enum
+import functools
import typing as typ
from blender_maxwell.utils import extra_sympy_units as spux
@@ -23,6 +24,17 @@ from blender_maxwell.utils.staticproperty import staticproperty
log = logger.get(__name__)
+_PROPERTY_NAMES = {
+ 'capabilities',
+ 'previews',
+ 'value',
+ 'array',
+ 'lazy_range',
+ 'lazy_func',
+ 'params',
+ 'info',
+}
+
class FlowKind(enum.StrEnum):
"""Defines a kind of data that can flow between nodes.
@@ -50,14 +62,15 @@ class FlowKind(enum.StrEnum):
"""
Capabilities = enum.auto()
+ Previews = enum.auto()
# Values
Value = enum.auto() ## 'value'
Array = enum.auto() ## 'array'
# Lazy
- Func = enum.auto() ## 'lazy_func'
Range = enum.auto() ## 'lazy_range'
+ Func = enum.auto() ## 'lazy_func'
# Auxiliary
Params = enum.auto() ## 'params'
@@ -70,12 +83,13 @@ class FlowKind(enum.StrEnum):
def to_name(v: typ.Self) -> str:
return {
FlowKind.Capabilities: 'Capabilities',
+ FlowKind.Previews: 'Previews',
# Values
FlowKind.Value: 'Value',
FlowKind.Array: 'Array',
# Lazy
- FlowKind.Range: 'Range',
FlowKind.Func: 'Func',
+ FlowKind.Range: 'Range',
# Auxiliary
FlowKind.Params: 'Params',
FlowKind.Info: 'Info',
@@ -88,6 +102,48 @@ class FlowKind(enum.StrEnum):
####################
# - Static Properties
####################
+ @staticproperty
+ def property_names() -> set[str]:
+ """Set of strings for (socket) properties associated with a `FlowKind`.
+
+ Usable for optimized O(1) lookup, to check whether a property name can be converted to a `FlowKind`.
+ To actually retrieve the `FlowKind` from one of these names, use `FlowKind.from_property_name()`.
+ """
+ return _PROPERTY_NAMES
+
+ @functools.cache
+ @staticmethod
+ def from_property_name(prop_name: str) -> typ.Self:
+ """Retrieve the `FlowKind` associated with a particular property name.
+
+ Parameters:
+ prop_name: The name of the property.
+ **Must** be a string defined in `FlowKind.property_names`.
+ """
+ return {
+ 'capabilities': FlowKind.Capabilities,
+ 'previews': FlowKind.Previews,
+ 'value': FlowKind.Value,
+ 'array': FlowKind.Array,
+ 'lazy_range': FlowKind.Range,
+ 'lazy_func': FlowKind.Func,
+ 'params': FlowKind.Params,
+ 'info': FlowKind.Info,
+ }[prop_name]
+
+ @functools.cached_property
+ def property_name(self) -> typ.Self:
+ """Retrieve the `FlowKind` associated with a particular property name.
+
+ Parameters:
+ prop_name: The name of the property.
+ **Must** be a string defined in `FlowKind.property_names`.
+ """
+ return {
+ FlowKind.from_property_name(prop_name): prop_name
+ for prop_name in FlowKind.property_names
+ }[self]
+
@staticproperty
def active_kinds() -> list[typ.Self]:
"""Return a list of `FlowKind`s that are able to be considered "active".
@@ -121,6 +177,7 @@ class FlowKind(enum.StrEnum):
####################
# - Class Methods
####################
+ ## TODO: Remove this (only events uses it).
@classmethod
def scale_to_unit_system(
cls,
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/info.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/info.py
index d93fb76..43dbfe5 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/info.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/info.py
@@ -93,7 +93,7 @@ class InfoFlow:
return list(self.dims.keys())[idx]
return None
- def dim_by_name(self, dim_name: str) -> int:
+ def dim_by_name(self, dim_name: str, optional: bool = False) -> int | None:
"""The integer axis occupied by the dimension.
Can be used to index `.shape` of the represented raw array.
@@ -102,6 +102,9 @@ class InfoFlow:
if len(dims_with_name) == 1:
return dims_with_name[0]
+ if optional:
+ return None
+
msg = f'Dim name {dim_name} not found in InfoFlow (or >1 found)'
raise ValueError(msg)
@@ -127,14 +130,15 @@ class InfoFlow:
return False
def is_idx_uniform(self, dim: sim_symbols.SimSymbol) -> bool:
- """Whether the (int) dim has explicitly uniform indexing.
+ """Whether the given dim has explicitly uniform indexing.
This is needed primarily to check whether a Fourier Transform can be meaningfully performed on the data over the dimension's axis.
In practice, we've decided that only `RangeFlow` really truly _guarantees_ uniform indexing.
While `ArrayFlow` may be uniform in practice, it's a very expensive to check, and it's far better to enforce that the user perform that check and opt for a `RangeFlow` instead, at the time of dimension definition.
"""
- return isinstance(self.dims[dim], RangeFlow) and self.dims[dim].scaling == 'lin'
+ dim_idx = self.dims[dim]
+ return isinstance(dim_idx, RangeFlow) and dim_idx.scaling == 'lin'
def dim_axis(self, dim: sim_symbols.SimSymbol) -> int:
"""The integer axis occupied by the dimension.
@@ -194,7 +198,7 @@ class InfoFlow:
return {
dim.name_pretty: {
'length': str(len(dim_idx)) if dim_idx is not None else 'ā',
- 'mathtype': dim.mathtype.label_pretty,
+ 'mathtype': dim.mathtype_size_label,
'unit': dim.unit_label,
}
for dim, dim_idx in self.dims.items()
@@ -315,27 +319,23 @@ class InfoFlow:
op: typ.Callable[[spux.SympyExpr, spux.SympyExpr], spux.SympyExpr],
unit_op: typ.Callable[[spux.SympyExpr, spux.SympyExpr], spux.SympyExpr],
) -> spux.SympyExpr:
- if self.dims == other.dims:
- sym_name = sim_symbols.SimSymbolName.Expr
- expr = op(self.output.sp_symbol_phy, other.output.sp_symbol_phy)
- unit_expr = unit_op(self.output.unit_factor, other.output.unit_factor)
+ sym_name = sim_symbols.SimSymbolName.Expr
+ expr = op(self.output.sp_symbol_phy, other.output.sp_symbol_phy)
+ unit_expr = unit_op(self.output.unit_factor, other.output.unit_factor)
+ ## TODO: Handle per-cell matrix units?
- return InfoFlow(
- dims=self.dims,
- output=sim_symbols.SimSymbol.from_expr(sym_name, expr, unit_expr),
- pinned_values=self.pinned_values,
- )
-
- msg = f'InfoFlow: operate_output cannot be used when dimensions are not identical ({self.dims} | {other.dims}).'
- raise ValueError(msg)
+ return InfoFlow(
+ dims=self.dims,
+ output=sim_symbols.SimSymbol.from_expr(sym_name, expr, unit_expr),
+ pinned_values=self.pinned_values,
+ )
####################
# - Operations: Fold
####################
def fold_last_input(self):
"""Fold the last input dimension into the output."""
- last_key = list(self.dims.keys())[-1]
- last_idx = list(self.dims.values())[-1]
+ last_idx = self.dims[self.last_dim]
rows = self.output.rows
cols = self.output.cols
@@ -351,7 +351,9 @@ class InfoFlow:
return InfoFlow(
dims={
- dim: dim_idx for dim, dim_idx in self.dims.items() if dim != last_key
+ dim: dim_idx
+ for dim, dim_idx in self.dims.items()
+ if dim != self.last_dim
},
output=new_output,
pinned_values=self.pinned_values,
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_func.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_func.py
index 0baf232..52c0fb2 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_func.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_func.py
@@ -20,10 +20,14 @@ import typing as typ
from types import MappingProxyType
import jax
+import jaxtyping as jtyp
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger, sim_symbols
+from .array import ArrayFlow
+from .info import InfoFlow
+from .lazy_range import RangeFlow
from .params import ParamsFlow
log = logger.get(__name__)
@@ -314,14 +318,66 @@ class FuncFlow:
) -> typ.Self:
if self.supports_jax:
return self.func_jax(
- *params.scaled_func_args(self.func_args, symbol_values),
- *params.scaled_func_kwargs(self.func_args, symbol_values),
+ *params.scaled_func_args(symbol_values),
+ **params.scaled_func_kwargs(symbol_values),
)
return self.func(
- *params.scaled_func_args(self.func_kwargs, symbol_values),
- *params.scaled_func_kwargs(self.func_kwargs, symbol_values),
+ *params.scaled_func_args(symbol_values),
+ **params.scaled_func_kwargs(symbol_values),
)
+ def realize_as_data(
+ self,
+ info: InfoFlow,
+ params: ParamsFlow,
+ symbol_values: dict[sim_symbols.SimSymbol, spux.SympyExpr] = MappingProxyType(
+ {}
+ ),
+ ) -> dict[sim_symbols.SimSymbol, jtyp.Inexact[jtyp.Array, '...']]:
+ """Realize as an ordered dictionary mapping each realized `self.dims` entry, with the last entry containing all output data as mapped from the `self.output`."""
+ data = {}
+ for dim, dim_idx in info.dims.items():
+ # Continuous Index (*)
+ ## -> Continuous dimensions **must** be symbols in ParamsFlow.
+ ## -> ...Since the output data shape is parameterized by it.
+ if info.has_idx_cont(dim):
+ if dim in params.symbols:
+ # Scalar Realization
+ ## -> Conform & cast the sympy expr to the dimension.
+ if isinstance(symbol_values[dim], spux.SympyType):
+ data |= {dim: dim.scale(symbol_values[dim])}
+
+ # Array Realization
+ ## -> Scale the array to the dimension's unit & get values.
+ if isinstance(symbol_values[dim], RangeFlow | ArrayFlow):
+ data |= {
+ dim: symbol_values[dim].rescale_to_unit(dim.unit).values
+ }
+ else:
+ msg = f'ParamsFlow does not contain dimension symbol {dim} (info={info}, params={params})'
+ raise RuntimeError(msg)
+
+ # Discrete Index (Q|R)
+ ## -> Realize ArrayFlow|RangeFlow
+ if info.has_idx_discrete(dim):
+ data |= {dim: dim_idx.values}
+
+ # Labelled Index (Z)
+ ## -> Passthrough the string labels.
+ if info.has_idx_labels(dim):
+ data |= {dim: dim_idx}
+
+ return data | {info.output: self.realize(params, symbol_values=symbol_values)}
+
+ # return {
+ # dim: (
+ # dim_idx
+ # if info.has_idx_cont(dim) or info.has_idx_labels(dim)
+ # else ??
+ # )
+ # for dim, dim_idx in self.dims
+ # } | {info.output: output_data}
+
####################
# - Composition Operations
####################
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_range.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_range.py
index a81cd5f..55cc3b5 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_range.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/lazy_range.py
@@ -218,9 +218,6 @@ class RangeFlow:
)
return combined_mathtype
- ####################
- # - Methods
- ####################
@property
def ideal_midpoint(self) -> spux.SympyExpr:
return (self.stop + self.start) / 2
@@ -229,6 +226,41 @@ class RangeFlow:
def ideal_range(self) -> spux.SympyExpr:
return self.stop - self.start
+ ####################
+ # - Methods
+ ####################
+ @staticmethod
+ def try_from_array(
+ array: ArrayFlow, uniformity_tolerance: float = 1e-9
+ ) -> ArrayFlow | typ.Self:
+ """Attempt to create a RangeFlow from a potentially uniform ArrayFlow, falling back to that same ArrayFlow if it isn't uniform.
+
+ For functional (ex. Fourier Transform) and memory-related reasons, it's important to be explicit about the uniformity of index elements.
+ For this reason, only `RangeFlow`s are considered uniform - `ArrayFlow`s are not, as it's expensive to check in a hot loop, while `RangeFlow`s have this property simply by existing.
+
+ Of course, real-world data sources may not come in a memory-efficient configuration, even if they are, in fact, monotonically increasing with uniform finite differences.
+ This method bridges that gap: If (within `uniformity_tolerance`) **all** finite differences are the same, then the `ArrayFlow` can be converted losslessly to a `RangeFlow.
+ **Otherwise**, the `ArrayFlow` is returned verbatim.
+
+ Notes:
+ A few other checks are also performed to guarantee the semantics of a resulting `RangeFlow`: The array must be sorted, there must be at least two values, and the first value must be strictly smaller than the last value.
+ """
+ diffs = jnp.diff(array.values)
+
+ if (
+ jnp.all(jnp.abs(diffs - diffs[0]) < uniformity_tolerance)
+ and len(array.values) > 2 # noqa: PLR2004
+ and array.values[0] < array.values[-1]
+ and array.is_sorted
+ ):
+ return RangeFlow(
+ start=sp.S(array.values[0]),
+ stop=sp.S(array.values[-1]),
+ steps=len(array.values),
+ unit=array.unit,
+ )
+ return array
+
def rescale(
self, rescale_func, reverse: bool = False, new_unit: spux.Unit | None = None
) -> typ.Self:
@@ -612,8 +644,8 @@ class RangeFlow:
symbols=self.symbols,
)
return RangeFlow(
- start=self.start * unit,
- stop=self.stop * unit,
+ start=self.start,
+ stop=self.stop,
steps=self.steps,
scaling=self.scaling,
unit=unit,
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/params.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/params.py
index cd2e8ae..fdc0568 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/params.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/params.py
@@ -31,8 +31,6 @@ from .expr_info import ExprInfo
from .flow_kinds import FlowKind
from .lazy_range import RangeFlow
-# from .info import InfoFlow
-
log = logger.get(__name__)
@@ -44,11 +42,18 @@ class ParamsFlow:
All symbols valid for use in the expression.
"""
+ arg_targets: list[sim_symbols.SimSymbol] = dataclasses.field(default_factory=list)
+ kwarg_targets: list[str, sim_symbols.SimSymbol] = dataclasses.field(
+ default_factory=dict
+ )
+
func_args: list[spux.SympyExpr] = dataclasses.field(default_factory=list)
func_kwargs: dict[str, spux.SympyExpr] = dataclasses.field(default_factory=dict)
symbols: frozenset[sim_symbols.SimSymbol] = frozenset()
+ is_differentiable: bool = False
+
####################
# - Symbols
####################
@@ -76,8 +81,9 @@ class ParamsFlow:
####################
# - JIT'ed Callables for Numerical Function Arguments
####################
+ @functools.cached_property
def func_args_n(
- self, target_syms: list[sim_symbols.SimSymbol]
+ self,
) -> list[
typ.Callable[
[int | float | complex | jtyp.Inexact[jtyp.Array, '...'], ...],
@@ -86,15 +92,12 @@ class ParamsFlow:
]:
"""Callable functions for evaluating each `self.func_args` entry numerically.
- Before simplification, each `self.func_args` entry will be conformed to the corresponding (by-index) `SimSymbol` in `target_syms`.
+ Before simplification, each `self.func_args` entry will be conformed to the corresponding (by-index) `SimSymbol` in `self.target_syms`.
Notes:
Before using any `sympy` expressions as arguments to the returned callablees, they **must** be fully conformed and scaled to the corresponding `self.symbols` entry using that entry's `SimSymbol.scale()` method.
This ensures conformance to the `SimSymbol` properties (like units), as well as adherance to a numerical type identity compatible with `sp.lambdify()`.
-
- Parameters:
- target_syms: `SimSymbol`s describing how a particular `ParamsFlow` function argument should be scaled when performing a purely numerical insertion.
"""
return [
sp.lambdify(
@@ -102,11 +105,14 @@ class ParamsFlow:
target_sym.conform(func_arg, strip_unit=True),
'jax',
)
- for func_arg, target_sym in zip(self.func_args, target_syms, strict=True)
+ for func_arg, target_sym in zip(
+ self.func_args, self.arg_targets, strict=True
+ )
]
+ @functools.cached_property
def func_kwargs_n(
- self, target_syms: dict[str, sim_symbols.SimSymbol]
+ self,
) -> dict[
str,
typ.Callable[
@@ -120,12 +126,12 @@ class ParamsFlow:
This ensures conformance to the `SimSymbol` properties, as well as adherance to a numerical type identity compatible with `sp.lambdify()`
"""
return {
- func_arg_key: sp.lambdify(
+ key: sp.lambdify(
self.sorted_sp_symbols,
- target_syms[func_arg_key].scale(func_arg),
+ self.kwarg_targets[key].conform(func_arg, strip_unit=True),
'jax',
)
- for func_arg_key, func_arg in self.func_kwargs.items()
+ for key, func_arg in self.func_kwargs.items()
}
####################
@@ -182,7 +188,6 @@ class ParamsFlow:
####################
def scaled_func_args(
self,
- target_syms: list[sim_symbols.SimSymbol] = (),
symbol_values: dict[sim_symbols.SimSymbol, spux.SympyExpr] = MappingProxyType(
{}
),
@@ -196,32 +201,24 @@ class ParamsFlow:
1. Conform Symbols: Arbitrary `sympy` expressions passed as `symbol_values` must first be conformed to match the ex. units of `SimSymbol`s found in `self.symbols`, before they can be used.
2. Conform Function Arguments: Arbitrary `sympy` expressions encoded in `self.func_args` must, **after** inserting the conformed numerical symbols, themselves be conformed to the expected ex. units of the function that they are to be used within.
- **`ParamsFlow` doesn't contain information about the `SimSymbol`s that `self.func_args` are expected to conform to** (on purpose).
- Therefore, the user is required to pass a `target_syms` with identical length to `self.func_args`, describing the `SimSymbol`s to conform the function arguments to.
Our implementation attempts to utilize simple, powerful primitives to accomplish this in roughly three steps:
1. **Realize Symbols**: Particular passed symbolic values `symbol_values`, which are arbitrary `sympy` expressions, are conformed to the definitions in `self.symbols` (ex. to match units), then cast to numerical values (pure Python / jax array).
- 2. **Lazy Function Arguments**: Stored function arguments `self.func_args`, which are arbitrary `sympy` expressions, are conformed to the definitions in `target_syms` (ex. to match units), then cast to numerical values (pure Python / jax array).
+ 2. **Lazy Function Arguments**: Stored function arguments `self.func_args`, which are arbitrary `sympy` expressions, are conformed to the definitions in `self.target_syms` (ex. to match units), then cast to numerical values (pure Python / jax array).
_Technically, this happens as part of `self.func_args_n`._
3. **Numerical Evaluation**: The numerical values for each symbol are passed as parameters to each (callable) element of `self.func_args_n`, which produces a correct numerical value for each function argument.
Parameters:
- target_syms: `SimSymbol`s describing how the function arguments returned by this method are intended to be used.
- **Generally**, the parallel `FuncFlow.func_args` should be inserted here, and guarantees correct results when this output is inserted into `FuncFlow.func(...)`.
- symbol_values: Particular values for all symbols in `self.symbols`, which will be conformed and used to compute the function arguments (before they are conformed to `target_syms`).
+ symbol_values: Particular values for all symbols in `self.symbols`, which will be conformed and used to compute the function arguments (before they are conformed to `self.target_syms`).
"""
realized_symbols = list(self.realize_symbols(symbol_values).values())
- return [
- func_arg_n(*realized_symbols)
- for func_arg_n in self.func_args_n(target_syms)
- ]
+ return [func_arg_n(*realized_symbols) for func_arg_n in self.func_args_n]
def scaled_func_kwargs(
self,
- target_syms: list[sim_symbols.SimSymbol] = (),
symbol_values: dict[spux.Symbol, spux.SympyExpr] = MappingProxyType({}),
) -> dict[
str, int | float | Fraction | float | complex | jtyp.Shaped[jtyp.Array, '...']
@@ -233,7 +230,7 @@ class ParamsFlow:
realized_symbols = self.realize_symbols(symbol_values)
return {
func_arg_name: func_arg_n(**realized_symbols)
- for func_arg_name, func_arg_n in self.func_kwargs_n(target_syms).items()
+ for func_arg_name, func_arg_n in self.func_kwargs_n.items()
}
####################
@@ -249,27 +246,41 @@ class ParamsFlow:
The next composed function will receive a tuple of two arrays, instead of just one, allowing binary operations to occur.
"""
return ParamsFlow(
+ arg_targets=self.arg_targets + other.arg_targets,
+ kwarg_targets=self.kwarg_targets | other.kwarg_targets,
func_args=self.func_args + other.func_args,
func_kwargs=self.func_kwargs | other.func_kwargs,
symbols=self.symbols | other.symbols,
+ is_differentiable=self.is_differentiable & other.is_differentiable,
)
def compose_within(
self,
+ enclosing_arg_targets: list[sim_symbols.SimSymbol] = (),
+ enclosing_kwarg_targets: list[sim_symbols.SimSymbol] = (),
enclosing_func_args: list[spux.SympyExpr] = (),
enclosing_func_kwargs: dict[str, spux.SympyExpr] = MappingProxyType({}),
- enclosing_symbols: frozenset[spux.Symbol] = frozenset(),
+ enclosing_symbols: frozenset[sim_symbols.SimSymbol] = frozenset(),
+ enclosing_is_differentiable: bool = False,
) -> typ.Self:
return ParamsFlow(
+ arg_targets=self.arg_targets + list(enclosing_arg_targets),
+ kwarg_targets=self.kwarg_targets | dict(enclosing_kwarg_targets),
func_args=self.func_args + list(enclosing_func_args),
func_kwargs=self.func_kwargs | dict(enclosing_func_kwargs),
symbols=self.symbols | enclosing_symbols,
+ is_differentiable=(
+ self.is_differentiable
+ if not enclosing_symbols
+ else (self.is_differentiable & enclosing_is_differentiable)
+ ),
)
####################
# - Generate ExprSocketDef
####################
- def sym_expr_infos(self, use_range: bool = False) -> dict[str, ExprInfo]:
+ @functools.cached_property
+ def sym_expr_infos(self) -> dict[str, ExprInfo]:
"""Generate keyword arguments for defining all `ExprSocket`s needed to realize all `self.symbols`.
Many nodes need actual data, and as such, they require that the user select actual values for any symbols in the `ParamsFlow`.
@@ -284,28 +295,16 @@ class ParamsFlow:
}
```
- Parameters:
- info: The InfoFlow associated with the `Expr` being realized.
- Each symbol in `self.symbols` **must** have an associated same-named dimension in `info`.
- use_range: Causes the
-
The `ExprInfo`s can be directly defererenced `**expr_info`)
"""
for sym in self.sorted_symbols:
- if use_range and sym.mathtype is spux.MathType.Complex:
- msg = 'No support for complex range in ExprInfo'
- raise NotImplementedError(msg)
- if use_range and (sym.rows > 1 or sym.cols > 1):
- msg = 'No support for non-scalar elements of range in ExprInfo'
- raise NotImplementedError(msg)
if sym.rows > 3 or sym.cols > 1:
msg = 'No support for >Vec3 / Matrix values in ExprInfo'
raise NotImplementedError(msg)
return {
- sym.name: {
- 'active_kind': FlowKind.Value if not use_range else FlowKind.Range,
- 'default_steps': 50,
+ sym: {
+ 'default_steps': 25,
}
| sym.expr_info
for sym in self.sorted_symbols
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/previews.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/previews.py
new file mode 100644
index 0000000..92b76e3
--- /dev/null
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds/previews.py
@@ -0,0 +1,193 @@
+# blender_maxwell
+# Copyright (C) 2024 blender_maxwell Project Contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import typing as typ
+
+import bpy
+import pydantic as pyd
+
+from blender_maxwell.utils import logger
+
+IMAGE_AREA_TYPE = 'IMAGE_EDITOR'
+IMAGE_SPACE_TYPE = 'IMAGE_EDITOR'
+
+log = logger.get(__name__)
+
+####################
+# - Global Collection Handling
+####################
+MANAGED_COLLECTION_NAME = 'BLMaxwell'
+PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
+
+
+def collection(collection_name: str, view_layer_exclude: bool) -> bpy.types.Collection:
+ # Init the "Managed Collection"
+ # Ensure Collection exists (and is in the Scene collection)
+ if collection_name not in bpy.data.collections:
+ collection = bpy.data.collections.new(collection_name)
+ bpy.context.scene.collection.children.link(collection)
+ else:
+ collection = bpy.data.collections[collection_name]
+
+ ## Ensure synced View Layer exclusion
+ if (
+ layer_collection := bpy.context.view_layer.layer_collection.children[
+ collection_name
+ ]
+ ).exclude != view_layer_exclude:
+ layer_collection.exclude = view_layer_exclude
+
+ return collection
+
+
+def managed_collection() -> bpy.types.Collection:
+ return collection(MANAGED_COLLECTION_NAME, view_layer_exclude=True)
+
+
+def preview_collection() -> bpy.types.Collection:
+ return collection(PREVIEW_COLLECTION_NAME, view_layer_exclude=False)
+
+
+####################
+# - Global Collection Handling
+####################
+class PreviewsFlow(pyd.BaseModel):
+ """Represents global references to previewable entries."""
+
+ model_config = pyd.ConfigDict(frozen=True)
+
+ bl_image_name: str | None = None
+ bl_object_names: frozenset[str] = frozenset()
+
+ ####################
+ # - Operations
+ ####################
+ def __or__(self, other: typ.Self) -> typ.Self:
+ return PreviewsFlow(
+ bl_image_name=other.bl_image_name,
+ bl_object_names=self.bl_object_names | other.bl_object_names,
+ )
+
+ ####################
+ # - Image Editor UX
+ ####################
+ @classmethod
+ def preview_area(cls) -> bpy.types.Area | None:
+ """Deduces a Blender UI area that can be used for image preview.
+
+ Returns:
+ A Blender UI area, if an appropriate one is visible; else `None`,
+ """
+ valid_areas = [
+ area for area in bpy.context.screen.areas if area.type == IMAGE_AREA_TYPE
+ ]
+ if valid_areas:
+ return valid_areas[0]
+ return None
+
+ @classmethod
+ def preview_space(cls) -> bpy.types.SpaceProperties | None:
+ """Deduces a Blender UI space, within `self.preview_area`, that can be used for image preview.
+
+ Returns:
+ A Blender UI space within `self.preview_area`, if it isn't None; else, `None`.
+ """
+ preview_area = cls.preview_area()
+ if preview_area is not None:
+ valid_spaces = [
+ space for space in preview_area.spaces if space.type == IMAGE_SPACE_TYPE
+ ]
+ if valid_spaces:
+ return valid_spaces[0]
+ return None
+ return None
+
+ ####################
+ # - Preview Plot
+ ####################
+ @classmethod
+ def hide_image_preview(cls) -> None:
+ """Show all image previews in the first available image editor.
+
+ If no image editors are visible, then nothing happens.
+ """
+ preview_space = cls.preview_space()
+ if preview_space is not None and preview_space.image is not None:
+ cls.preview_space().image = None
+
+ def update_image_preview(self) -> None:
+ """Show the image preview in the first available image editor.
+
+ If no image editors are visible, then nothing happens.
+ """
+ preview_space = self.preview_space()
+ if self.bl_image_name is not None:
+ bl_image = bpy.data.images.get(self.bl_image_name)
+
+ # Replace Preview Space Image
+ if (
+ bl_image is not None
+ and preview_space is not None
+ and preview_space.image is not bl_image
+ ):
+ preview_space.image = bl_image
+
+ # Remove Image
+ if bl_image is None and preview_space.image is not None:
+ preview_space.image = None
+ elif preview_space.image is not None:
+ preview_space.image = None
+
+ ####################
+ # - Preview Objects
+ ####################
+ @classmethod
+ def hide_bl_object_previews(cls) -> None:
+ """Hide all previewed Blender objects."""
+ for bl_object_name in [obj.name for obj in preview_collection().objects]:
+ bl_object = bpy.data.objects.get(bl_object_name)
+ if bl_object is not None and bl_object.name in preview_collection().objects:
+ preview_collection().objects.unlink(bl_object)
+
+ def update_bl_object_previews(self) -> frozenset[str]:
+ """Preview objects that need previewing and unpreview objects that need unpreviewing.
+
+ Designed to utilize the least possible amount of operations to achieve a desired set of previewed objects, from the current set of previewed objects.
+ """
+ # Deduce Change in Previewed Objects
+ ## -> Examine the preview collection for all currently previewed names.
+ ## -> Preview names that shouldn't exist should be added.
+ ## -> Preview names that should exist should be removed.
+ previewed_bl_objects = {obj.name for obj in preview_collection().objects}
+ bl_objects_to_remove = previewed_bl_objects - self.bl_object_names
+ bl_objects_to_add = self.bl_object_names - previewed_bl_objects
+
+ # Remove Previews
+ ## -> Never presume that bl_object is already defined.
+ for bl_object_name in bl_objects_to_remove:
+ bl_object = bpy.data.objects.get(bl_object_name)
+ if bl_object is not None and bl_object.name in preview_collection().objects:
+ preview_collection().objects.unlink(bl_object)
+
+ # Add Previews
+ ## -> Never presume that bl_object is already defined.
+ for bl_object_name in bl_objects_to_add:
+ bl_object = bpy.data.objects.get(bl_object_name)
+ if (
+ bl_object is not None
+ and bl_object.name not in preview_collection().objects
+ ):
+ preview_collection().objects.link(bl_object)
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py
index 0102a5f..fc12eeb 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py
@@ -38,6 +38,7 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
Scene = enum.auto()
## Inputs / Constants
ExprConstant = enum.auto()
+ SymbolConstant = enum.auto()
ScientificConstant = enum.auto()
UnitSystemConstant = enum.auto()
BlenderConstant = enum.auto()
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py
index a702e43..6243641 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py
@@ -19,7 +19,7 @@ import typing as typ
import bpy
-from blender_maxwell.utils import logger
+from blender_maxwell.utils import logger, serialize
from . import contracts as ct
from .managed_objs.managed_bl_image import ManagedBLImage
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py
index 4240d60..0c9c8d0 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py
@@ -86,11 +86,13 @@ def extract_info(monitor_data, monitor_attr: str) -> ct.InfoFlow | None: # noqa
if xarr is None:
return None
- def mk_idx_array(axis: str) -> ct.ArrayFlow:
- return ct.ArrayFlow(
- values=xarr.get_index(axis).values,
- unit=symbols[axis].unit,
- is_sorted=True,
+ def mk_idx_array(axis: str) -> ct.RangeFlow | ct.ArrayFlow:
+ return ct.RangeFlow.try_from_array(
+ ct.ArrayFlow(
+ values=xarr.get_index(axis).values,
+ unit=symbols[axis].unit,
+ is_sorted=True,
+ )
)
# Compute InfoFlow from XArray
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py
index f9a33c1..c90dbe5 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/filter_math.py
@@ -124,12 +124,12 @@ class FilterOperation(enum.StrEnum):
# - Computed Properties
####################
@property
- def func_args(self) -> list[spux.MathType]:
+ def func_args(self) -> list[sim_symbols.SimSymbol]:
FO = FilterOperation
return {
# Pin
- FO.Pin: [spux.MathType.Integer],
- FO.PinIdx: [spux.MathType.Integer],
+ FO.Pin: [sim_symbols.idx(None)],
+ FO.PinIdx: [sim_symbols.idx(None)],
}.get(self, [])
####################
@@ -155,10 +155,10 @@ class FilterOperation(enum.StrEnum):
match self:
# Slice
case FO.Slice:
- return [dim for dim in info.dims if not dim.has_idx_labels(dim)]
+ return [dim for dim in info.dims if not info.has_idx_labels(dim)]
case FO.SliceIdx:
- return [dim for dim in info.dims if not dim.has_idx_labels(dim)]
+ return [dim for dim in info.dims if not info.has_idx_labels(dim)]
# Pin
case FO.PinLen1:
@@ -272,10 +272,15 @@ class FilterMathNode(base.MaxwellSimNode):
# - Properties: Expr InfoFlow
####################
@events.on_value_changed(
+ # Trigger
socket_name={'Expr'},
+ # Loaded
input_sockets={'Expr'},
input_socket_kinds={'Expr': ct.FlowKind.Info},
input_sockets_optional={'Expr': True},
+ # Flow
+ ## -> See docs in TransformMathNode
+ stop_propagation=True,
)
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
@@ -593,11 +598,17 @@ class FilterMathNode(base.MaxwellSimNode):
pinned_value, require_sorted=True
)
- return params.compose_within(enclosing_func_args=[nearest_idx_to_value])
+ return params.compose_within(
+ enclosing_arg_targets=[sim_symbols.idx(None)],
+ enclosing_func_args=[sp.S(nearest_idx_to_value)],
+ )
# Pin by-Index
if props['operation'] is FilterOperation.PinIdx and has_pinned_axis:
- return params.compose_within(enclosing_func_args=[pinned_axis])
+ return params.compose_within(
+ enclosing_arg_targets=[sim_symbols.idx(None)],
+ enclosing_func_args=[sp.S(pinned_axis)],
+ )
return params
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py
index fde2938..7529a18 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py
@@ -236,7 +236,7 @@ class MapOperation(enum.StrEnum):
MO.Sinc: lambda expr: sp.sinc(expr),
# By Vector
# Vector -> Number
- MO.Norm2: lambda expr: sp.sqrt(expr.T @ expr),
+ MO.Norm2: lambda expr: sp.sqrt(expr.T @ expr)[0],
# By Matrix
# Matrix -> Number
MO.Det: lambda expr: sp.det(expr),
@@ -467,10 +467,15 @@ class MapMathNode(base.MaxwellSimNode):
# - Properties
####################
@events.on_value_changed(
+ # Trigger
socket_name={'Expr'},
+ # Loaded
input_sockets={'Expr'},
input_socket_kinds={'Expr': ct.FlowKind.Info},
input_sockets_optional={'Expr': True},
+ # Flow
+ ## -> See docs in TransformMathNode
+ stop_propagation=True,
)
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py
index 703ef77..578be8f 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py
@@ -210,7 +210,7 @@ class BinaryOperation(enum.StrEnum):
):
ops += [BO.Cross]
- return ops
+ return ops_el_el + ops
## Vector | Matrix
case (1, 2):
@@ -374,10 +374,15 @@ class OperateMathNode(base.MaxwellSimNode):
# - Properties
####################
@events.on_value_changed(
+ # Trigger
socket_name={'Expr L', 'Expr R'},
+ # Loaded
input_sockets={'Expr L', 'Expr R'},
input_socket_kinds={'Expr L': ct.FlowKind.Info, 'Expr R': ct.FlowKind.Info},
input_sockets_optional={'Expr L': True, 'Expr R': True},
+ # Flow
+ ## -> See docs in TransformMathNode
+ stop_propagation=True,
)
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
has_info_l = not ct.FlowSignal.check(input_sockets['Expr L'])
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/transform_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/transform_math.py
index ec9bfd1..f869326 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/transform_math.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/transform_math.py
@@ -17,7 +17,6 @@
"""Declares `TransformMathNode`."""
import enum
-import functools
import typing as typ
import bpy
@@ -39,13 +38,25 @@ log = logger.get(__name__)
# - Operation Enum
####################
class TransformOperation(enum.StrEnum):
- """Valid operations for the `MapMathNode`.
+ """Valid operations for the `TransformMathNode`.
Attributes:
- FreqToVacWL: Transform frequency axes to be indexed by vacuum wavelength.
- VacWLToFreq: Transform vacuum wavelength axes to be indexed by frequency.
- FFT: Compute the fourier transform of the input expression.
- InvFFT: Compute the inverse fourier transform of the input expression.
+ FreqToVacWL: Transform an frequency dimension to vacuum wavelength.
+ VacWLToFreq: Transform a vacuum wavelength dimension to frequency.
+ ConvertIdxUnit: Convert the unit of a dimension to a compatible unit.
+ SetIdxUnit: Set all properties of a dimension.
+ FirstColToFirstIdx: Extract the first data column and set the first dimension's index array equal to it.
+ **For 2D integer-indexed data only**.
+
+ IntDimToComplex: Fold a last length-2 integer dimension into the output, transforming it from a real-like type to complex type.
+ DimToVec: Fold the last dimension into the scalar output, creating a vector output type.
+ DimsToMat: Fold the last two dimensions into the scalar output, creating a matrix output type.
+ FT: Compute the 1D fourier transform along a dimension.
+ New dimensional bounds are computing using the Nyquist Limit.
+ For higher dimensions, simply repeat along more dimensions.
+ InvFT1D: Compute the inverse 1D fourier transform along a dimension.
+ New dimensional bounds are computing using the Nyquist Limit.
+ For higher dimensions, simply repeat along more dimensions.
"""
# Covariant Transform
@@ -79,7 +90,7 @@ class TransformOperation(enum.StrEnum):
TO.VacWLToFreq: 'Ī»įµ„ ā š',
TO.ConvertIdxUnit: 'Convert Dim',
TO.SetIdxUnit: 'Set Dim',
- TO.FirstColToFirstIdx: '1st Col ā Dim',
+ TO.FirstColToFirstIdx: '1st Col ā 1st Dim',
# Fold
TO.IntDimToComplex: 'ā ā',
TO.DimToVec: 'ā Vector',
@@ -87,10 +98,14 @@ class TransformOperation(enum.StrEnum):
## TODO: Vector to new last-dim integer
## TODO: Matrix to two last-dim integers
# Fourier
- TO.FT1D: 'ā š',
- TO.InvFT1D: 'š ā',
+ TO.FT1D: 'FT',
+ TO.InvFT1D: 'iFT',
}[value]
+ @property
+ def name(self) -> str:
+ return TransformOperation.to_name(self)
+
@staticmethod
def to_icon(_: typ.Self) -> str:
return ''
@@ -108,49 +123,32 @@ class TransformOperation(enum.StrEnum):
####################
# - Methods
####################
- @property
- def num_dim_inputs(self) -> None:
- """The number of axes that should be passed as inputs to `func_jax` when evaluating it.
-
- Especially useful for `ParamFlow`, when deciding whether to pass an integer-axis argument based on a user-selected dimension.
- """
- TO = TransformOperation
- return {
- # Covariant Transform
- TO.FreqToVacWL: 1,
- TO.VacWLToFreq: 1,
- TO.ConvertIdxUnit: 1,
- TO.SetIdxUnit: 1,
- TO.FirstColToFirstIdx: 0,
- # Fold
- TO.IntDimToComplex: 0,
- TO.DimToVec: 0,
- TO.DimsToMat: 0,
- ## TODO: Vector to new last-dim integer
- ## TODO: Matrix to two last-dim integers
- # Fourier
- TO.FT1D: 1,
- TO.InvFT1D: 1,
- }[self]
-
def valid_dims(self, info: ct.InfoFlow) -> list[typ.Self]:
TO = TransformOperation
match self:
- case TO.FreqToVacWL | TO.FT1D:
+ case TO.FreqToVacWL:
return [
dim
for dim in info.dims
if dim.physical_type is spux.PhysicalType.Freq
]
- case TO.VacWLToFreq | TO.InvFT1D:
+ case TO.VacWLToFreq:
return [
dim
for dim in info.dims
if dim.physical_type is spux.PhysicalType.Length
]
- case TO.ConvertIdxUnit | TO.SetIdxUnit:
+ case TO.ConvertIdxUnit:
+ return [
+ dim
+ for dim in info.dims
+ if not info.has_idx_labels(dim)
+ and spux.PhysicalType.from_unit(dim.unit, optional=True) is not None
+ ]
+
+ case TO.SetIdxUnit:
return [dim for dim in info.dims if not info.has_idx_labels(dim)]
## ColDimToComplex: Implicit Last Dimension
@@ -198,13 +196,11 @@ class TransformOperation(enum.StrEnum):
# Fold
## Last Dim -> Complex
if (
- info.dims
- # Output is Int|Rat|Real
+ len(info.dims) >= 1
and (
info.output.mathtype
in [spux.MathType.Integer, spux.MathType.Rational, spux.MathType.Real]
)
- # Last Axis is Integer of Length 2
and info.last_dim.mathtype is spux.MathType.Integer
and info.has_idx_labels(info.last_dim)
and len(info.dims[info.last_dim]) == 2 # noqa: PLR2004
@@ -231,14 +227,13 @@ class TransformOperation(enum.StrEnum):
####################
# - Function Properties
####################
- @functools.cached_property
- def jax_func(self):
+ def jax_func(self, axis: int | None = None):
TO = TransformOperation
return {
# Covariant Transform
## -> Freq <-> WL is a rescale (noop) AND flip (not noop).
- TO.FreqToVacWL: lambda expr, axis: jnp.flip(expr, axis=axis),
- TO.VacWLToFreq: lambda expr, axis: jnp.flip(expr, axis=axis),
+ TO.FreqToVacWL: lambda expr: jnp.flip(expr, axis=axis),
+ TO.VacWLToFreq: lambda expr: jnp.flip(expr, axis=axis),
TO.ConvertIdxUnit: lambda expr: expr,
TO.SetIdxUnit: lambda expr: expr,
TO.FirstColToFirstIdx: lambda expr: jnp.delete(expr, 0, axis=1),
@@ -250,8 +245,8 @@ class TransformOperation(enum.StrEnum):
TO.DimToVec: lambda expr: expr,
TO.DimsToMat: lambda expr: expr,
# Fourier
- TO.FT1D: lambda expr, axis: jnp.fft(expr, axis=axis),
- TO.InvFT1D: lambda expr, axis: jnp.ifft(expr, axis=axis),
+ TO.FT1D: lambda expr: jnp.fft(expr, axis=axis),
+ TO.InvFT1D: lambda expr: jnp.ifft(expr, axis=axis),
}[self]
def transform_info(
@@ -268,25 +263,21 @@ class TransformOperation(enum.StrEnum):
# Covariant Transform
TO.FreqToVacWL: lambda: info.replace_dim(
(f_dim := dim),
- [
- sim_symbols.wl(unit),
- info.dims[f_dim].rescale(
- lambda el: sci_constants.vac_speed_of_light / el,
- reverse=True,
- new_unit=unit,
- ),
- ],
+ sim_symbols.wl(unit),
+ info.dims[f_dim].rescale(
+ lambda el: sci_constants.vac_speed_of_light / el,
+ reverse=True,
+ new_unit=unit,
+ ),
),
TO.VacWLToFreq: lambda: info.replace_dim(
(wl_dim := dim),
- [
- sim_symbols.freq(unit),
- info.dims[wl_dim].rescale(
- lambda el: sci_constants.vac_speed_of_light / el,
- reverse=True,
- new_unit=unit,
- ),
- ],
+ sim_symbols.freq(unit),
+ info.dims[wl_dim].rescale(
+ lambda el: sci_constants.vac_speed_of_light / el,
+ reverse=True,
+ new_unit=unit,
+ ),
),
TO.ConvertIdxUnit: lambda: info.replace_dim(
dim,
@@ -300,7 +291,9 @@ class TransformOperation(enum.StrEnum):
TO.SetIdxUnit: lambda: info.replace_dim(
dim,
dim.update(
- sym_name=new_dim_name, physical_type=physical_type, unit=unit
+ sym_name=new_dim_name,
+ physical_type=physical_type,
+ unit=unit,
),
(
info.dims[dim].correct_unit(unit)
@@ -311,10 +304,12 @@ class TransformOperation(enum.StrEnum):
TO.FirstColToFirstIdx: lambda: info.replace_dim(
info.first_dim,
info.first_dim.update(
+ sym_name=new_dim_name,
mathtype=spux.MathType.from_jax_array(data_col),
+ physical_type=physical_type,
unit=unit,
),
- ct.ArrayFlow(values=data_col, unit=unit),
+ ct.RangeFlow.try_from_array(ct.ArrayFlow(values=data_col, unit=unit)),
).slice_dim(info.last_dim, (1, len(info.dims[info.last_dim]), 1)),
# Fold
TO.IntDimToComplex: lambda: info.delete_dim(info.last_dim).update_output(
@@ -380,10 +375,18 @@ class TransformMathNode(base.MaxwellSimNode):
# - Properties: Expr InfoFlow
####################
@events.on_value_changed(
+ # Trigger
socket_name={'Expr'},
+ # Loaded
input_sockets={'Expr'},
input_socket_kinds={'Expr': ct.FlowKind.Info},
input_sockets_optional={'Expr': True},
+ # Flow
+ ## -> Expr wants to emit DataChanged, which is usually fine.
+ ## -> However, this node sets `expr_info`, which causes DC to emit.
+ ## -> One action should emit one DataChanged pipe.
+ ## -> Therefore, defer responsibility for DataChanged to self.expr_info.
+ stop_propagation=True,
)
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
@@ -440,7 +443,7 @@ class TransformMathNode(base.MaxwellSimNode):
@bl_cache.cached_bl_property(depends_on={'expr_info', 'active_dim'})
def dim(self) -> sim_symbols.SimSymbol | None:
if self.expr_info is not None and self.active_dim is not None:
- return self.expr_info.dim_by_name(self.active_dim)
+ return self.expr_info.dim_by_name(self.active_dim, optional=True)
return None
####################
@@ -454,48 +457,52 @@ class TransformMathNode(base.MaxwellSimNode):
)
active_new_unit: enum.StrEnum = bl_cache.BLField(
enum_cb=lambda self, _: self.search_units(),
- cb_depends_on={'dim', 'new_physical_type'},
+ cb_depends_on={'dim', 'new_physical_type', 'operation'},
)
def search_units(self) -> list[ct.BLEnumElement]:
- if self.dim is not None:
- if self.dim.physical_type is not spux.PhysicalType.NonPhysical:
- unit_name = sp.sstr(self.dim.unit)
- return [
- (
- sp.sstr(unit),
- spux.sp_to_str(unit),
- sp.sstr(unit),
- '',
- 0,
- )
- for unit in self.dim.physical_type.valid_units
- ]
-
- if self.dim.unit is not None:
- unit_name = sp.sstr(self.dim.unit)
- return [
- (
- unit_name,
- spux.sp_to_str(self.dim.unit),
- unit_name,
- '',
- 0,
- )
- ]
- if self.new_physical_type is not spux.PhysicalType.NonPhysical:
- return [
- (
- sp.sstr(unit),
- spux.sp_to_str(unit),
- sp.sstr(unit),
- '',
- i,
+ TO = TransformOperation
+ match self.operation:
+ # Covariant Transform
+ case TO.ConvertIdxUnit if self.dim is not None:
+ physical_type = spux.PhysicalType.from_unit(
+ self.dim.unit, optional=True
)
- for i, unit in enumerate(self.new_physical_type.valid_units)
- ]
+ if physical_type is not None:
+ valid_units = physical_type.valid_units
+ else:
+ valid_units = []
- return []
+ case TO.FreqToVacWL if self.dim is not None:
+ valid_units = spux.PhysicalType.Length.valid_units
+
+ case TO.VacWLToFreq if self.dim is not None:
+ valid_units = spux.PhysicalType.Freq.valid_units
+
+ case TO.SetIdxUnit if (
+ self.dim is not None
+ and self.new_physical_type is not spux.PhysicalType.NonPhysical
+ ):
+ valid_units = self.new_physical_type.valid_units
+
+ case TO.FirstColToFirstIdx if (
+ self.new_physical_type is not spux.PhysicalType.NonPhysical
+ ):
+ valid_units = self.new_physical_type.valid_units
+
+ case _:
+ valid_units = []
+
+ return [
+ (
+ sp.sstr(unit),
+ spux.sp_to_str(unit),
+ sp.sstr(unit),
+ '',
+ i,
+ )
+ for i, unit in enumerate(valid_units)
+ ]
@bl_cache.cached_bl_property(depends_on={'active_new_unit'})
def new_unit(self) -> spux.Unit:
@@ -507,30 +514,85 @@ class TransformMathNode(base.MaxwellSimNode):
####################
# - UI
####################
- def draw_label(self):
- if self.operation is not None:
- return 'T: ' + TransformOperation.to_name(self.operation)
+ @bl_cache.cached_bl_property(depends_on={'new_unit'})
+ def new_unit_str(self) -> str:
+ if self.new_unit is None:
+ return ''
+ return spux.sp_to_str(self.new_unit)
- return self.bl_label
+ def draw_label(self):
+ TO = TransformOperation
+ match self.operation:
+ case TO.FreqToVacWL if self.dim is not None:
+ return f'T: {self.dim.name_pretty} | š ā {self.new_unit_str}'
+
+ case TO.VacWLToFreq if self.dim is not None:
+ return f'T: {self.dim.name_pretty} | Ī»įµ„ ā {self.new_unit_str}'
+
+ case TO.ConvertIdxUnit if self.dim is not None:
+ return f'T: {self.dim.name_pretty} ā {self.new_unit_str}'
+
+ case TO.SetIdxUnit if self.dim is not None:
+ return f'T: {self.dim.name_pretty} ā {self.new_name.name_pretty} | {self.new_unit_str}'
+
+ case (
+ TO.IntDimToComplex
+ | TO.DimToVec
+ | TO.DimsToMat
+ ) if self.expr_info is not None and self.expr_info.dims:
+ return f'T: {self.expr_info.last_dim.name_unit_label} {self.operation.name}'
+
+ case TO.FT1D if self.dim is not None:
+ return f'T: FT[{self.dim.name_unit_label}]'
+
+ case TO.InvFT1D if self.dim is not None:
+ return f'T: iFT[{self.dim.name_unit_label}]'
+
+ case _:
+ if self.operation is not None:
+ return f'T: {self.operation.name}'
+ return self.bl_label
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
layout.prop(self, self.blfields['operation'], text='')
- if self.operation is not None and self.operation.num_dim_inputs == 1:
- TO = TransformOperation
- layout.prop(self, self.blfields['active_dim'], text='')
+ TO = TransformOperation
+ match self.operation:
+ case TO.ConvertIdxUnit:
+ row = layout.row(align=True)
+ row.prop(self, self.blfields['active_dim'], text='')
+ row.prop(self, self.blfields['active_new_unit'], text='')
- if self.operation in [TO.ConvertIdxUnit, TO.SetIdxUnit]:
+ case TO.FreqToVacWL:
+ row = layout.row(align=True)
+ row.prop(self, self.blfields['active_dim'], text='')
+ row.prop(self, self.blfields['active_new_unit'], text='')
+
+ case TO.VacWLToFreq:
+ row = layout.row(align=True)
+ row.prop(self, self.blfields['active_dim'], text='')
+ row.prop(self, self.blfields['active_new_unit'], text='')
+
+ case TO.SetIdxUnit:
+ row = layout.row(align=True)
+ row.prop(self, self.blfields['active_dim'], text='')
+ row.prop(self, self.blfields['new_name'], text='')
+
+ row = layout.row(align=True)
+ row.prop(self, self.blfields['new_physical_type'], text='')
+ row.prop(self, self.blfields['active_new_unit'], text='')
+
+ case TO.FirstColToFirstIdx:
col = layout.column(align=True)
- if self.operation is TransformOperation.ConvertIdxUnit:
- col.prop(self, self.blfields['active_new_unit'], text='')
+ row = col.row(align=True)
+ row.prop(self, self.blfields['new_name'], text='')
+ row.prop(self, self.blfields['active_new_unit'], text='')
- if self.operation is TransformOperation.SetIdxUnit:
- col.prop(self, self.blfields['new_physical_type'], text='')
+ row = col.row(align=True)
+ row.prop(self, self.blfields['new_physical_type'], text='')
- row = col.row(align=True)
- row.prop(self, self.blfields['new_name'], text='')
- row.prop(self, self.blfields['active_new_unit'], text='')
+ case TO.FT1D | TO.InvFT1D:
+ layout.prop(self, self.blfields['active_dim'], text='')
####################
# - Compute: Func / Array
@@ -538,23 +600,43 @@ class TransformMathNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Expr',
kind=ct.FlowKind.Func,
- props={'operation'},
+ # Loaded
+ props={'operation', 'dim'},
input_sockets={'Expr'},
input_socket_kinds={
- 'Expr': ct.FlowKind.Func,
+ 'Expr': {ct.FlowKind.Func, ct.FlowKind.Info},
},
)
def compute_func(self, props, input_sockets) -> ct.FuncFlow | ct.FlowSignal:
+ """Transform the input `InfoFlow` depending on the transform operation."""
+ TO = TransformOperation
operation = props['operation']
- lazy_func = input_sockets['Expr']
+ lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
+ info = input_sockets['Expr'][ct.FlowKind.Info]
+ has_info = not ct.FlowSignal.check(info)
has_lazy_func = not ct.FlowSignal.check(lazy_func)
- if has_lazy_func and operation is not None:
- return lazy_func.compose_within(
- operation.jax_func,
- supports_jax=True,
- )
+ if operation is not None and has_lazy_func and has_info:
+ # Retrieve Properties
+ dim = props['dim']
+
+ # Match Pattern by Operation
+ match operation:
+ case TO.FreqToVacWL | TO.VacWLToFreq | TO.FT1D | TO.InvFT1D:
+ if dim is not None and info.has_idx_discrete(dim):
+ return lazy_func.compose_within(
+ operation.jax_func(axis=info.dim_axis(dim)),
+ supports_jax=True,
+ )
+ return ct.FlowSignal.FlowPending
+
+ case _:
+ return lazy_func.compose_within(
+ operation.jax_func(),
+ supports_jax=True,
+ )
+
return ct.FlowSignal.FlowPending
####################
@@ -563,54 +645,101 @@ class TransformMathNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Expr',
kind=ct.FlowKind.Info,
+ # Loaded
props={'operation', 'dim', 'new_name', 'new_unit', 'new_physical_type'},
input_sockets={'Expr'},
input_socket_kinds={
'Expr': {ct.FlowKind.Func, ct.FlowKind.Info, ct.FlowKind.Params}
},
)
- def compute_info(
+ def compute_info( # noqa: PLR0911
self, props: dict, input_sockets: dict
) -> ct.InfoFlow | typ.Literal[ct.FlowSignal.FlowPending]:
+ """Transform the input `InfoFlow` depending on the transform operation."""
+ TO = TransformOperation
operation = props['operation']
info = input_sockets['Expr'][ct.FlowKind.Info]
has_info = not ct.FlowSignal.check(info)
-
- dim = props['dim']
- new_name = props['new_name']
- new_unit = props['new_unit']
- new_physical_type = props['new_physical_type']
if has_info and operation is not None:
- # First Column to First Index
- ## -> We have to evaluate the lazy function at this point.
- ## -> It's the only way to get at the column data.
- if operation is TransformOperation.FirstColToFirstIdx:
- lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
- params = input_sockets['Expr'][ct.FlowKind.Params]
- has_lazy_func = not ct.FlowSignal.check(lazy_func)
- has_params = not ct.FlowSignal.check(lazy_func)
+ # Retrieve Properties
+ dim = props['dim']
+ new_name = props['new_name']
+ new_unit = props['new_unit']
+ new_physical_type = props['new_physical_type']
- if has_lazy_func and has_params and not params.symbols:
+ # Retrieve Expression Data
+ lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
+ params = input_sockets['Expr'][ct.FlowKind.Params]
+
+ has_lazy_func = not ct.FlowSignal.check(lazy_func)
+ has_params = not ct.FlowSignal.check(lazy_func)
+
+ # Match Pattern by Operation
+ match operation:
+ # Covariant Transform
+ ## -> Needs: Dim, Unit
+ case TO.ConvertIdxUnit if dim is not None and new_unit is not None:
+ physical_type = spux.PhysicalType.from_unit(dim.unit, optional=True)
+ if (
+ physical_type is not None
+ and new_unit in physical_type.valid_units
+ ):
+ return operation.transform_info(info, dim=dim, unit=new_unit)
+ return ct.FlowSignal.FlowPending
+
+ case TO.FreqToVacWL if dim is not None and new_unit is not None and new_unit in spux.PhysicalType.Length.valid_units:
+ return operation.transform_info(info, dim=dim, unit=new_unit)
+
+ case TO.VacWLToFreq if dim is not None and new_unit is not None and new_unit in spux.PhysicalType.Freq.valid_units:
+ return operation.transform_info(info, dim=dim, unit=new_unit)
+
+ ## -> Needs: Dim, Unit, Physical Type
+ case TO.SetIdxUnit if (
+ dim is not None
+ and new_physical_type is not None
+ and new_unit in new_physical_type.valid_units
+ ):
+ return operation.transform_info(
+ info,
+ dim=dim,
+ new_dim_name=new_name,
+ unit=new_unit,
+ physical_type=new_physical_type,
+ )
+
+ ## -> Needs: Data Column, Name, Unit, Physical Type
+ ## -> We have to evaluate the lazy function at this point.
+ ## -> It's the only way to get at the column's data.
+ case TO.FirstColToFirstIdx if (
+ has_lazy_func
+ and has_params
+ and not params.symbols
+ and new_name is not None
+ and new_physical_type is not None
+ and new_unit in new_physical_type.valid_units
+ ):
data = lazy_func.realize(params)
- if data.shape is not None and len(data.shape) == 2:
+ if data.shape is not None and len(data.shape) == 2: # noqa: PLR2004
data_col = data[:, 0]
- return operation.transform_info(info, data_col=data_col)
- return ct.FlowSignal.FlowPending
+ return operation.transform_info(
+ info,
+ new_dim_name=new_name,
+ data_col=data_col,
+ unit=new_unit,
+ physical_type=new_physical_type,
+ )
- # Check Not-Yet-Updated Dimension
- ## - Operation changes before dimensions.
- ## - If InfoFlow is requested in this interim, big problem.
- if dim is None and operation.num_dim_inputs > 0:
- return ct.FlowSignal.FlowPending
+ # Fold
+ ## -> Needs: Nothing
+ case TO.IntDimToComplex | TO.DimToVec | TO.DimsToMat:
+ return operation.transform_info(info)
+
+ # Fourier
+ ## -> Needs: Dimension
+ case TO.FT1D | TO.InvFT1D if dim is not None:
+ return operation.transform_info(info, dim=dim)
- return operation.transform_info(
- info,
- dim=dim,
- new_dim_name=new_name,
- unit=new_unit,
- physical_type=new_physical_type,
- )
return ct.FlowSignal.FlowPending
####################
@@ -619,30 +748,19 @@ class TransformMathNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Expr',
kind=ct.FlowKind.Params,
- props={'operation', 'dim'},
+ # Loaded
+ props={'operation'},
input_sockets={'Expr'},
- input_socket_kinds={'Expr': {ct.FlowKind.Params, ct.FlowKind.Info}},
+ input_socket_kinds={'Expr': ct.FlowKind.Params},
)
def compute_params(self, props, input_sockets) -> ct.ParamsFlow | ct.FlowSignal:
- info = input_sockets['Expr'][ct.FlowKind.Info]
- params = input_sockets['Expr'][ct.FlowKind.Params]
-
- has_info = not ct.FlowSignal.check(info)
- has_params = not ct.FlowSignal.check(params)
-
operation = props['operation']
- dim = props['dim']
- if has_info and has_params and operation is not None:
- # Axis Required: Insert by-Dimension
- ## -> Some transformations ex. FT require setting an axis.
- ## -> The user selects which dimension the op should be done along.
- ## -> This dimension is converted to an axis integer.
- ## -> Finally, we pass the argument via params.
- if operation.num_dim_inputs == 1:
- axis = info.dim_axis(dim) if dim is not None else None
- return params.compose_within(enclosing_func_args=[axis])
+ params = input_sockets['Expr']
+ has_params = not ct.FlowSignal.check(params)
+ if has_params and operation is not None:
return params
+
return ct.FlowSignal.FlowPending
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py
index 378fe24..b901275 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py
@@ -104,6 +104,7 @@ class VizMode(enum.StrEnum):
"""Given the input `InfoFlow`, deduce which visualization modes are valid to use with the described data."""
Z = spux.MathType.Integer
R = spux.MathType.Real
+ C = spux.MathType.Complex
VM = VizMode
return {
@@ -115,6 +116,9 @@ class VizMode(enum.StrEnum):
VM.Points2D,
VM.Bar,
],
+ ((R,), (1, 1, C)): [
+ VM.Curve2D,
+ ],
((R, Z), (1, 1, R)): [
VM.Curves2D,
VM.FilledCurves2D,
@@ -231,10 +235,15 @@ class VizNode(base.MaxwellSimNode):
## - Properties
#####################
@events.on_value_changed(
+ # Trigger
socket_name={'Expr'},
+ # Loaded
input_sockets={'Expr'},
input_socket_kinds={'Expr': ct.FlowKind.Info},
input_sockets_optional={'Expr': True},
+ # Flow
+ ## -> See docs in TransformMathNode
+ stop_propagation=True,
)
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
@@ -326,7 +335,7 @@ class VizNode(base.MaxwellSimNode):
if self.viz_target is VizTarget.Plot2D:
row = col.row(align=True)
row.alignment = 'CENTER'
- row.label(text='Width/Height/DPI')
+ row.label(text='Width | Height | DPI')
row = col.row(align=True)
row.prop(self, self.blfields['plot_width'], text='')
@@ -339,8 +348,10 @@ class VizNode(base.MaxwellSimNode):
# - Events
####################
@events.on_value_changed(
+ # Trigger
socket_name='Expr',
run_on_init=True,
+ # Loaded
input_sockets={'Expr'},
input_socket_kinds={'Expr': {ct.FlowKind.Info, ct.FlowKind.Params}},
input_sockets_optional={'Expr': True},
@@ -355,14 +366,19 @@ class VizNode(base.MaxwellSimNode):
# Declare Loose Sockets that Realize Symbols
## -> This happens if Params contains not-yet-realized symbols.
if has_info and has_params and params.symbols:
- if set(self.loose_input_sockets) != {
- sym.name for sym in params.symbols if sym in info.dims
- }:
+ if set(self.loose_input_sockets) != {sym.name for sym in params.symbols}:
self.loose_input_sockets = {
- dim_name: sockets.ExprSocketDef(**expr_info)
- for dim_name, expr_info in params.sym_expr_infos(
- use_range=True
- ).items()
+ sym.name: sockets.ExprSocketDef(
+ **(
+ expr_info
+ | {
+ 'active_kind': ct.FlowKind.Range
+ if sym in info.dims
+ else ct.FlowKind.Value
+ }
+ )
+ )
+ for sym, expr_info in params.sym_expr_infos.items()
}
elif self.loose_input_sockets:
@@ -373,9 +389,10 @@ class VizNode(base.MaxwellSimNode):
#####################
@events.computes_output_socket(
'Preview',
- kind=ct.FlowKind.Value,
+ kind=ct.FlowKind.Previews,
# Loaded
props={
+ 'sim_node_name',
'viz_mode',
'viz_target',
'colormap',
@@ -391,7 +408,7 @@ class VizNode(base.MaxwellSimNode):
)
def compute_dummy_value(self, props, input_sockets, loose_input_sockets):
"""Needed for the plot to regenerate in the viewer."""
- return ct.FlowSignal.NoFlow
+ return ct.PreviewsFlow(bl_image_name=props['sim_node_name'])
#####################
## - On Show Plot
@@ -416,6 +433,7 @@ class VizNode(base.MaxwellSimNode):
def on_show_plot(
self, managed_objs, props, input_sockets, loose_input_sockets
) -> None:
+ log.critical('Show Plot (too many times)')
lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
info = input_sockets['Expr'][ct.FlowKind.Info]
params = input_sockets['Expr'][ct.FlowKind.Params]
@@ -427,23 +445,17 @@ class VizNode(base.MaxwellSimNode):
viz_mode = props['viz_mode']
viz_target = props['viz_target']
if has_info and has_params and viz_mode is not None and viz_target is not None:
- # Realize Data w/Realized Symbols
+ # Retrieve Data
## -> The loose input socket values are user-selected symbol values.
- ## -> These expressions are used to realize the lazy data.
- ## -> `.realize()` ensures all ex. units are correctly conformed.
- realized_syms = {
- sym: loose_input_sockets[sym.name] for sym in params.sorted_symbols
- }
- output_data = lazy_func.realize(params, symbol_values=realized_syms)
-
- data = {
- dim: (
- realized_syms[dim].values
- if dim in realized_syms
- else info.dims[dim]
- )
- for dim in info.dims
- } | {info.output: output_data}
+ ## -> These are used to get rid of symbols in the ParamsFlow.
+ ## -> What's left is a dictionary from SimSymbol -> Data
+ data = lazy_func.realize_as_data(
+ info,
+ params,
+ symbol_values={
+ sym: loose_input_sockets[sym.name] for sym in params.sorted_symbols
+ },
+ )
# Match Viz Type & Perform Visualization
## -> Viz Target determines how to plot.
@@ -459,7 +471,6 @@ class VizNode(base.MaxwellSimNode):
width_inches=plot_width,
height_inches=plot_height,
dpi=plot_dpi,
- bl_select=True,
)
case VizTarget.Pixels:
@@ -468,7 +479,6 @@ class VizNode(base.MaxwellSimNode):
plot.map_2d_to_image(
data,
colormap=colormap,
- bl_select=True,
)
case VizTarget.PixelsPlane:
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 d12d20b..b81504e 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
@@ -21,6 +21,7 @@ Attributes:
"""
import enum
+import functools
import typing as typ
from collections import defaultdict
from types import MappingProxyType
@@ -62,7 +63,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
Used as a node-specific cache index.
sim_node_name: A unique human-readable name identifying the node.
Used when naming managed objects and exporting.
- preview_active: Whether the preview (if any) is currently active.
locked: Whether the node is currently 'locked' aka. non-editable.
"""
@@ -98,7 +98,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
loose_output_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
# UI Options
- preview_active: bool = bl_cache.BLField(False)
locked: bool = bl_cache.BLField(False, use_prop_update=False)
# Active Socket Set
@@ -264,35 +263,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
## TODO: Account for FlowKind
bl_socket.value = socket_value
- ####################
- # - Events: Preview | Plot
- ####################
- @events.on_show_plot(stop_propagation=False)
- def _on_show_plot(self):
- node_tree = self.id_data
- if len(self.event_methods_by_event[ct.FlowEvent.ShowPlot]) > 1:
- ## TODO: Is this check good enough?
- ## TODO: Set icon/indicator/something to make it clear which node is being previewed.
- node_tree.report_show_plot(self)
-
- @events.on_show_preview()
- def _on_show_preview(self):
- node_tree = self.id_data
- node_tree.report_show_preview(self)
-
- # Set Preview to Active
- ## Implicitly triggers any @on_value_changed for preview_active.
- if not self.preview_active:
- self.preview_active = True
-
- @events.on_value_changed(
- prop_name='preview_active', props={'preview_active'}, stop_propagation=True
- )
- def _on_preview_changed(self, props):
- if not props['preview_active']:
- for mobj in self.managed_objs.values():
- mobj.hide_preview()
-
####################
# - Events: Lock
####################
@@ -521,14 +491,17 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
return {
ct.FlowEvent.EnableLock: lambda *_: True,
ct.FlowEvent.DisableLock: lambda *_: True,
- ct.FlowEvent.DataChanged: lambda event_method, socket_name, prop_name, _: (
+ ct.FlowEvent.DataChanged: lambda event_method, socket_name, prop_names, _: (
(
socket_name
and socket_name in event_method.callback_info.on_changed_sockets
)
or (
- prop_name
- and prop_name in event_method.callback_info.on_changed_props
+ prop_names
+ and any(
+ prop_name in event_method.callback_info.on_changed_props
+ for prop_name in prop_names
+ )
)
or (
socket_name
@@ -536,6 +509,7 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
and socket_name in self.loose_input_sockets
)
),
+ # Non-Triggered
ct.FlowEvent.OutputRequested: lambda output_socket_method,
output_socket_name,
_,
@@ -546,7 +520,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
== output_socket_method.callback_info.output_socket_name
)
),
- ct.FlowEvent.ShowPreview: lambda *_: True,
ct.FlowEvent.ShowPlot: lambda *_: True,
}
@@ -595,6 +568,9 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
bl_socket = self.inputs.get(input_socket_name)
if bl_socket is not None:
if bl_socket.instance_id:
+ if kind is ct.FlowKind.Previews:
+ return bl_socket.compute_data(kind=kind)
+
return (
ct.FlowKind.scale_to_unit_system(
kind,
@@ -610,12 +586,10 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
## -> Anyone needing results will need to wait on preinit().
return ct.FlowSignal.FlowInitializing
- # if optional:
+ if kind is ct.FlowKind.Previews:
+ return ct.PreviewsFlow()
return ct.FlowSignal.NoFlow
- msg = f'{self.sim_node_name}: Input socket "{input_socket_name}" cannot be computed, as it is not an active input socket'
- raise ValueError(msg)
-
####################
# - Compute Event: Output Socket
####################
@@ -638,33 +612,64 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
The value of the output socket, as computed by the dedicated method
registered using the `@computes_output_socket` decorator.
"""
- if self.outputs.get(output_socket_name) is None:
- if optional:
- return None
+ # Previews: Aggregate All Input Sockets
+ ## -> All PreviewsFlows on all input sockets are combined.
+ ## -> Output Socket Methods can add additional PreviewsFlows.
+ if kind is ct.FlowKind.Previews:
+ input_previews = functools.reduce(
+ lambda a, b: a | b,
+ [
+ self._compute_input(
+ socket, kind=ct.FlowKind.Previews, unit_system=None
+ )
+ for socket in [bl_socket.name for bl_socket in self.inputs]
+ ],
+ ct.PreviewsFlow(),
+ )
- msg = f"Can't compute nonexistent output socket name {output_socket_name}, as it's not currently active"
- raise RuntimeError(msg)
+ # No Output Socket: No Flow
+ ## -> All PreviewsFlows on all input sockets are combined.
+ ## -> Output Socket Methods can add additional PreviewsFlows.
+ if self.outputs.get(output_socket_name) is None:
+ return ct.FlowSignal.NoFlow
output_socket_methods = self.filtered_event_methods_by_event(
ct.FlowEvent.OutputRequested,
(output_socket_name, None, kind),
)
- # Run (=1) Method
- if output_socket_methods:
- if len(output_socket_methods) > 1:
- msg = f'More than one method found for ({output_socket_name}, {kind.value!s}.'
- raise RuntimeError(msg)
+ # Exactly One Output Socket Method
+ ## -> All PreviewsFlows on all input sockets are combined.
+ ## -> Output Socket Methods can add additional PreviewsFlows.
+ if len(output_socket_methods) == 1:
+ res = output_socket_methods[0](self)
- return output_socket_methods[0](self)
+ # Res is PreviewsFlow: Concatenate
+ ## -> This will add the elements within the returned PreviewsFluw.
+ if kind is ct.FlowKind.Previews and not ct.FlowSignal.check(res):
+ input_previews |= res
- # Auxiliary Fallbacks
+ return res
+
+ # > One Output Socket Method: Error
+ if len(output_socket_methods) > 1:
+ msg = (
+ f'More than one method found for ({output_socket_name}, {kind.value!s}.'
+ )
+ raise RuntimeError(msg)
+
+ if kind is ct.FlowKind.Previews:
+ return input_previews
return ct.FlowSignal.NoFlow
- # if optional or kind in [ct.FlowKind.Info, ct.FlowKind.Params]:
- # return ct.FlowSignal.NoFlow
- # msg = f'No output method for ({output_socket_name}, {kind})'
- # raise ValueError(msg)
+ ####################
+ # - Plot
+ ####################
+ def compute_plot(self):
+ plot_methods = self.filtered_event_methods_by_event(ct.FlowEvent.ShowPlot, ())
+
+ for plot_method in plot_methods:
+ plot_method(self)
####################
# - Event Trigger
@@ -674,11 +679,11 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
method_info: events.InfoOutputRequested,
input_socket_name: ct.SocketName | None,
input_socket_kinds: set[ct.FlowKind] | None,
- prop_name: str | None,
+ prop_names: set[str] | None,
) -> bool:
return (
- prop_name is not None
- and prop_name in method_info.depon_props
+ prop_names is not None
+ and any(prop_name in method_info.depon_props for prop_name in prop_names)
or input_socket_name is not None
and (
input_socket_name in method_info.depon_input_sockets
@@ -704,41 +709,63 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
)
@bl_cache.cached_bl_property()
- def _dependent_outputs(
+ def output_socket_invalidates(
self,
) -> dict[
tuple[ct.SocketName, ct.FlowKind], set[tuple[ct.SocketName, ct.FlowKind]]
]:
- ## TODO: Cleanup
- ## TODO: Detect cycles?
- ## TODO: Networkx?
+ """Deduce which output socket | `FlowKind` combos are altered in response to a given output socket | `FlowKind` combo.
+
+ Returns:
+ A dictionary, wher eeach key is a tuple representing an output socket name and its flow kind that has been altered, and each value is a set of tuples representing output socket names and flow kind.
+
+ Indexing by any particular `(output_socket_name, flow_kind)` will produce a set of all `{(output_socket_name, flow_kind)}` that rely on it.
+ """
altered_to_invalidated = defaultdict(set)
+
+ # Iterate ALL Methods that Compute Output Sockets
+ ## -> We call it the "altered method".
+ ## -> Our approach will be to deduce what relies on it.
output_requested_methods = self.event_methods_by_event[
ct.FlowEvent.OutputRequested
]
-
for altered_method in output_requested_methods:
altered_info = altered_method.callback_info
altered_key = (altered_info.output_socket_name, altered_info.kind)
+ # Inner: Iterate ALL Methods that Compute Output Sockets
+ ## -> We call it the "invalidated method".
+ ## -> While O(n^2), it runs only once per-node, and is then cached.
+ ## -> `n` is rarely so large as to be a startup-time concern.
+ ## -> Thus, in this case, using a simple implementation is better.
for invalidated_method in output_requested_methods:
invalidated_info = invalidated_method.callback_info
+ # Check #0: Inv. Socket depends on Altered Socket
+ ## -> Simply check if the altered name is in the dependencies.
if (
altered_info.output_socket_name
in invalidated_info.depon_output_sockets
):
+ # Check #2: FlowKinds Match
+ ## -> Case 1: Single Altered Kind was Requested by Inv
+ ## -> Case 2: Altered Kind in set[Requested Kinds] is
+ ## -> Case 3: Altered Kind is FlowKind.Value
+ ## This encapsulates the actual events decorator semantics.
is_same_kind = (
altered_info.kind
- == (
+ is (
_kind := invalidated_info.depon_output_socket_kinds.get(
altered_info.output_socket_name
)
)
or (isinstance(_kind, set) and altered_info.kind in _kind)
- or altered_info.kind == ct.FlowKind.Value
+ or altered_info.kind is ct.FlowKind.Value
)
+ # Check Success: Add Invalidated (name,kind) to Altered Set
+ ## -> We've now confirmed a dependency.
+ ## -> Thus, this name|kind should be included.
if is_same_kind:
invalidated_key = (
invalidated_info.output_socket_name,
@@ -753,7 +780,7 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
event: ct.FlowEvent,
socket_name: ct.SocketName | None = None,
socket_kinds: set[ct.FlowKind] | None = None,
- prop_name: ct.SocketName | None = None,
+ prop_names: set[str] | None = None,
) -> None:
"""Recursively triggers events forwards or backwards along the node tree, allowing nodes in the update path to react.
@@ -770,124 +797,141 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
socket_name: The input socket that was altered, if any, in order to trigger this event.
pop_name: The property that was altered, if any, in order to trigger this event.
"""
- log.debug(
- '%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
- self.sim_node_name,
- event,
- str(socket_name),
- str(socket_kinds),
- str(prop_name),
- )
- # Outflow Socket Kinds
- ## -> Something has happened!
- ## -> The effect is yet to be determined...
- ## -> We will watch for which kinds actually invalidate.
- ## -> ...Then ONLY propagate kinds that have an invalidated outsck.
- ## -> This way, kinds get "their own" invalidation chains.
- ## -> ...While still respecting "crossovers".
- altered_socket_kinds = set()
+ # log.debug(
+ # '[%s] [%s] Triggered (socket_name=%s, socket_kinds=%s, prop_names=%s)',
+ # self.sim_node_name,
+ # event,
+ # str(socket_name),
+ # str(socket_kinds),
+ # str(prop_names),
+ # )
# Invalidate Caches on DataChanged
+ ## -> socket_kinds MUST NOT be None
+ ## -> Trigger direction is always 'forwards' for DataChanged
+ ## -> Track which FlowKinds are actually altered per-output-socket.
+ altered_socket_kinds: dict[ct.SocketName, set[ct.FlowKind]] = defaultdict(set)
if event is ct.FlowEvent.DataChanged:
- input_socket_name = socket_name ## Trigger direction is forwards
+ in_sckname = socket_name
- # Invalidate Input Socket Cache
- if input_socket_name is not None:
- if socket_kinds is None:
+ # Clear Input Socket Cache(s)
+ ## -> The input socket cache for each altered FlowKinds is cleared.
+ ## -> Since it's non-persistent, it will be lazily re-filled.
+ if in_sckname is not None:
+ for in_kind in socket_kinds:
+ # log.debug(
+ # '![%s] Clear Input Socket Cache (%s, %s)',
+ # self.sim_node_name,
+ # in_sckname,
+ # in_kind,
+ # )
self._compute_input.invalidate(
- input_socket_name=input_socket_name,
- kind=...,
+ input_socket_name=in_sckname,
+ kind=in_kind,
unit_system=...,
)
- else:
- for socket_kind in socket_kinds:
- self._compute_input.invalidate(
- input_socket_name=input_socket_name,
- kind=socket_kind,
- unit_system=...,
- )
- # Invalidate Output Socket Cache
+ # Clear Output Socket Cache(s)
for output_socket_method in self.event_methods_by_event[
ct.FlowEvent.OutputRequested
]:
+ # Determine Consequences of Changed (Socket|Kind) / Prop
+ ## -> Each '@computes_output_socket' declares data to load.
+ ## -> Compare what was changed to what each output socket needs.
+ ## -> IF what is needed, was changed, THEN:
+ ## --- The output socket needs recomputing.
method_info = output_socket_method.callback_info
if self._should_recompute_output_socket(
- method_info, socket_name, socket_kinds, prop_name
+ method_info, socket_name, socket_kinds, prop_names
):
out_sckname = method_info.output_socket_name
- kind = method_info.kind
+ out_kind = method_info.kind
- # Invalidate Output Directly
- # log.critical(
- # '[%s] Invalidating: (%s, %s)',
+ # log.debug(
+ # '![%s] Clear Output Socket Cache (%s, %s)',
# self.sim_node_name,
# out_sckname,
- # str(kind),
+ # out_kind,
# )
- altered_socket_kinds.add(kind)
self.compute_output.invalidate(
output_socket_name=out_sckname,
- kind=kind,
+ kind=out_kind,
)
+ altered_socket_kinds[out_sckname].add(out_kind)
- # Invalidate Any Dependent Outputs
- if (
- dep_outs := self._dependent_outputs.get((out_sckname, kind))
- ) is not None:
- for dep_out in dep_outs:
- # log.critical(
- # '![%s] Invalidating: (%s, %s)',
+ # Invalidate Dependent Output Sockets
+ ## -> Other outscks may depend on the altered outsck.
+ ## -> The property 'output_socket_invalidates' encodes this.
+ ## -> The property 'output_socket_invalidates' encodes this.
+ cleared_outscks_kinds = self.output_socket_invalidates.get(
+ (out_sckname, out_kind)
+ )
+ if cleared_outscks_kinds is not None:
+ for dep_out_sckname, dep_out_kind in cleared_outscks_kinds:
+ # log.debug(
+ # '!![%s] Clear Output Socket Cache (%s, %s)',
# self.sim_node_name,
- # dep_out[0],
- # dep_out[1],
+ # out_sckname,
+ # out_kind,
# )
- altered_socket_kinds.add(dep_out[1])
self.compute_output.invalidate(
- output_socket_name=dep_out[0],
- kind=dep_out[1],
+ output_socket_name=dep_out_sckname,
+ kind=dep_out_kind,
)
+ altered_socket_kinds[dep_out_sckname].add(dep_out_kind)
# Run Triggered Event Methods
+ ## -> A triggered event method may request to stop propagation.
+ ## -> A triggered event method may request to stop propagation.
stop_propagation = False
triggered_event_methods = self.filtered_event_methods_by_event(
- event, (socket_name, prop_name, None)
+ event, (socket_name, prop_names, None)
)
for event_method in triggered_event_methods:
stop_propagation |= event_method.stop_propagation
- # log.critical(
- # '%s: Running %s',
+ # log.debug(
+ # '![%s] Running: %s',
# self.sim_node_name,
# str(event_method.callback_info),
# )
event_method(self)
- # DataChanged Propagation Stop: No Altered Socket Kinds
- ## -> If no FlowKinds were altered, then propagation makes no sense.
- ## -> Semantically, **nothing has changed** == no DataChanged!
- if event is ct.FlowEvent.DataChanged and not altered_socket_kinds:
- return
-
- # Constrain ShowPlot to First Node: Workaround
- if event is ct.FlowEvent.ShowPlot:
- return
-
- # Propagate Event to All Sockets in "Trigger Direction"
+ # Propagate Event
+ ## -> If 'stop_propagation' was tripped, don't propagate.
+ ## -> If no sockets were altered during DataChanged, don't propagate.
+ ## -> Each FlowEvent decides whether to flow forwards/backwards.
## -> The trigger chain goes node/socket/socket/node/socket/...
+ ## -> Unlinked sockets naturally stop the propagation.
if not stop_propagation:
direc = ct.FlowEvent.flow_direction[event]
- triggered_sockets = self._bl_sockets(direc=direc)
- for bl_socket in triggered_sockets:
- if direc == 'output' and not bl_socket.is_linked:
- continue
+ for bl_socket in self._bl_sockets(direc=direc):
+ # DataChanged: Propagate Altered SocketKinds
+ ## -> Only altered FlowKinds for the socket will propagate.
+ ## -> In this way, we guarantee no extraneous (noop) flow.
+ if event is ct.FlowEvent.DataChanged:
+ if bl_socket.name in altered_socket_kinds:
+ # log.debug(
+ # '![%s] [%s] Propagating (direction=%s, altered_socket_kinds=%s)',
+ # self.sim_node_name,
+ # event,
+ # direc,
+ # altered_socket_kinds[bl_socket.name],
+ # )
+ bl_socket.trigger_event(
+ event, socket_kinds=altered_socket_kinds[bl_socket.name]
+ )
- # log.critical(
- # '![%s] Propagating: (%s, %s)',
- # self.sim_node_name,
- # event,
- # altered_socket_kinds,
- # )
- bl_socket.trigger_event(event, socket_kinds=altered_socket_kinds)
+ ## -> Otherwise, do nothing - guarantee no extraneous flow.
+
+ # Propagate Normally
+ else:
+ # log.debug(
+ # '![%s] [%s] Propagating (direction=%s)',
+ # self.sim_node_name,
+ # event,
+ # direc,
+ # )
+ bl_socket.trigger_event(event)
####################
# - Property Event: On Update
@@ -903,18 +947,22 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
Parameters:
prop_name: The name of the property that changed.
"""
- # All Attributes: Trigger Event
- ## -> This declares that the single property has changed.
- ## -> This should happen first, in case dependents need a cache.
- if hasattr(self, prop_name):
- self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
-
# BLField Attributes: Invalidate BLField Dependents
- ## -> Dependent props will generally also trigger on_prop_changed.
- ## -> The recursion ends with the depschain.
+ ## -> All invalidated blfields will have their caches cleared.
+ ## -> The (topologically) ordered list of cleared blfields is returned.
## -> WARNING: The chain is not checked for ex. cycles.
if prop_name in self.blfields:
- self.invalidate_blfield_deps(prop_name)
+ cleared_blfields = self.clear_blfields_after(prop_name)
+
+ # log.debug(
+ # '%s (Node): Set of Cleared BLFields: %s',
+ # self.bl_label,
+ # str(cleared_blfields),
+ # )
+ self.trigger_event(
+ ct.FlowEvent.DataChanged,
+ prop_names={prop_name for prop_name, _ in cleared_blfields},
+ )
####################
# - UI Methods
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py
index f02646f..502529a 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py
@@ -18,15 +18,18 @@ from . import (
blender_constant,
expr_constant,
scientific_constant,
+ symbol_constant,
)
BL_REGISTER = [
*expr_constant.BL_REGISTER,
+ *symbol_constant.BL_REGISTER,
*scientific_constant.BL_REGISTER,
*blender_constant.BL_REGISTER,
]
BL_NODES = {
**expr_constant.BL_NODES,
+ **symbol_constant.BL_NODES,
**scientific_constant.BL_NODES,
**blender_constant.BL_NODES,
}
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py
index 4870cac..8aac265 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py
@@ -76,13 +76,13 @@ class ScientificConstantNode(base.MaxwellSimNode):
"""Retrieve a symbol for the scientific constant."""
if self.sci_constant is not None and self.sci_constant_info is not None:
unit = self.sci_constant_info['units']
- return sim_symbols.SimSymbol(
- sym_name=self.sci_constant_name,
- mathtype=spux.MathType.from_expr(self.sci_constant),
- # physical_type= ## TODO: Formalize unit w/o physical_type
- unit=unit,
+ return sim_symbols.SimSymbol.from_expr(
+ self.sci_constant_name,
+ self.sci_constant,
+ unit,
is_constant=True,
)
+
return None
####################
@@ -125,7 +125,7 @@ class ScientificConstantNode(base.MaxwellSimNode):
if self.sci_constant_info:
row = _col.row(align=True)
# row.alignment = 'CENTER'
- row.label(text=f'{self.sci_constant_info["units"]}')
+ row.label(text=f'{spux.sp_to_str(self.sci_constant_info["units"].n(4))}')
row = _col.row(align=True)
# row.alignment = 'CENTER'
@@ -184,13 +184,18 @@ class ScientificConstantNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Expr',
kind=ct.FlowKind.Params,
- props={'sci_constant'},
+ props={'sci_constant', 'sci_constant_sym'},
)
def compute_params(self, props: dict) -> typ.Any:
sci_constant = props['sci_constant']
+ sci_constant_sym = props['sci_constant_sym']
- if sci_constant is not None:
- return ct.ParamsFlow(func_args=[sci_constant])
+ if sci_constant is not None and sci_constant_sym is not None:
+ return ct.ParamsFlow(
+ arg_targets=[sci_constant_sym],
+ func_args=[sci_constant],
+ is_differentiable=True,
+ )
return ct.FlowSignal.FlowPending
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/symbol_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/symbol_constant.py
new file mode 100644
index 0000000..240f16e
--- /dev/null
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/symbol_constant.py
@@ -0,0 +1,266 @@
+# blender_maxwell
+# Copyright (C) 2024 blender_maxwell Project Contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+import enum
+import typing as typ
+
+import bpy
+import sympy as sp
+
+from blender_maxwell.utils import bl_cache, logger, sim_symbols
+from blender_maxwell.utils import extra_sympy_units as spux
+
+from .... import contracts as ct
+from .... import sockets
+from ... import base, events
+
+log = logger.get(__name__)
+
+
+class SymbolConstantNode(base.MaxwellSimNode):
+ node_type = ct.NodeType.SymbolConstant
+ bl_label = 'Symbol'
+
+ input_sockets: typ.ClassVar = {}
+ output_sockets: typ.ClassVar = {
+ 'Expr': sockets.ExprSocketDef(
+ active_kind=ct.FlowKind.Func,
+ show_info_columns=True,
+ ),
+ }
+
+ ####################
+ # - Socket Interface
+ ####################
+ sym_name: sim_symbols.SimSymbolName = bl_cache.BLField(
+ sim_symbols.SimSymbolName.Constant
+ )
+
+ size: spux.NumberSize1D = bl_cache.BLField(spux.NumberSize1D.Scalar)
+ mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real)
+ physical_type: spux.PhysicalType = bl_cache.BLField(spux.PhysicalType.NonPhysical)
+
+ ####################
+ # - Properties: Unit
+ ####################
+ active_unit: enum.StrEnum = bl_cache.BLField(
+ enum_cb=lambda self, _: self.search_valid_units(),
+ cb_depends_on={'physical_type'},
+ )
+
+ def search_valid_units(self) -> list[ct.BLEnumElement]:
+ """Compute Blender enum elements of valid units for the current `physical_type`."""
+ if self.physical_type is not spux.PhysicalType.NonPhysical:
+ return [
+ (sp.sstr(unit), spux.sp_to_str(unit), sp.sstr(unit), '', i)
+ for i, unit in enumerate(self.physical_type.valid_units)
+ ]
+ return []
+
+ @bl_cache.cached_bl_property(depends_on={'active_unit'})
+ def unit(self) -> spux.Unit | None:
+ """Gets the current active unit.
+
+ Returns:
+ The current active `sympy` unit.
+
+ If the socket expression is unitless, this returns `None`.
+ """
+ if self.active_unit is not None:
+ return spux.unit_str_to_unit(self.active_unit)
+
+ return None
+
+ @property
+ def unit_factor(self) -> spux.Unit | None:
+ """Like `self.unit`, except `1` instead of `None` when unitless."""
+ return sp.Integer(1) if self.unit is None else self.unit
+
+ ####################
+ # - Domain
+ ####################
+ interval_finite_z: tuple[int, int] = bl_cache.BLField((0, 1))
+ interval_finite_q: tuple[tuple[int, int], tuple[int, int]] = bl_cache.BLField(
+ ((0, 1), (1, 1))
+ )
+ interval_finite_re: tuple[float, float] = bl_cache.BLField((0.0, 1.0))
+ interval_inf: tuple[bool, bool] = bl_cache.BLField((True, True))
+ interval_closed: tuple[bool, bool] = bl_cache.BLField((True, True))
+
+ interval_finite_im: tuple[float, float] = bl_cache.BLField((0.0, 1.0))
+ interval_inf_im: tuple[bool, bool] = bl_cache.BLField((True, True))
+ interval_closed_im: tuple[bool, bool] = bl_cache.BLField((True, True))
+
+ ####################
+ # - Computed Properties
+ ####################
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'sym_name',
+ 'size',
+ 'mathtype',
+ 'physical_type',
+ 'unit',
+ 'interval_finite_z',
+ 'interval_finite_q',
+ 'interval_finite_re',
+ 'interval_inf',
+ 'interval_closed',
+ 'interval_finite_im',
+ 'interval_inf_im',
+ 'interval_closed_im',
+ }
+ )
+ def symbol(self) -> sim_symbols.SimSymbol:
+ return sim_symbols.SimSymbol(
+ sym_name=self.sym_name,
+ mathtype=self.mathtype,
+ physical_type=self.physical_type,
+ unit=self.unit,
+ rows=self.size.rows,
+ cols=self.size.cols,
+ interval_finite_z=self.interval_finite_z,
+ interval_finite_q=self.interval_finite_q,
+ interval_finite_re=self.interval_finite_re,
+ interval_inf=self.interval_inf,
+ interval_closed=self.interval_closed,
+ interval_finite_im=self.interval_finite_im,
+ interval_inf_im=self.interval_inf_im,
+ interval_closed_im=self.interval_closed_im,
+ )
+
+ ####################
+ # - UI
+ ####################
+ def draw_label(self):
+ return self.symbol.def_label
+
+ def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
+ col.prop(self, self.blfields['sym_name'], text='')
+
+ row = col.row(align=True)
+ row.alignment = 'CENTER'
+ row.label(text='Assumptions')
+
+ row = col.row(align=True)
+ row.prop(self, self.blfields['mathtype'], text='')
+
+ row = col.row(align=True)
+ row.prop(self, self.blfields['size'], text='')
+ row.prop(self, self.blfields['active_unit'], text='')
+
+ col.prop(self, self.blfields['physical_type'], text='')
+
+ row = col.row(align=True)
+ row.alignment = 'CENTER'
+ row.label(text='Domain')
+
+ match self.mathtype:
+ case spux.MathType.Integer:
+ col.prop(self, self.blfields['interval_finite_z'], text='')
+ col.prop(self, self.blfields['interval_inf'], text='Infinite')
+ col.prop(self, self.blfields['interval_closed'], text='Closed')
+
+ case spux.MathType.Rational:
+ col.prop(self, self.blfields['interval_finite_q'], text='')
+ col.prop(self, self.blfields['interval_inf'], text='Infinite')
+ col.prop(self, self.blfields['interval_closed'], text='Closed')
+
+ case spux.MathType.Real:
+ col.prop(self, self.blfields['interval_finite_re'], text='')
+ col.prop(self, self.blfields['interval_inf'], text='Infinite')
+ col.prop(self, self.blfields['interval_closed'], text='Closed')
+
+ case spux.MathType.Complex:
+ col.prop(self, self.blfields['interval_finite_re'], text='ā')
+ col.prop(self, self.blfields['interval_inf'], text='ā Infinite')
+ col.prop(self, self.blfields['interval_closed'], text='ā Closed')
+
+ col.separator()
+
+ col.prop(self, self.blfields['interval_finite_im'], text='š')
+ col.prop(self, self.blfields['interval_inf'], text='š Infinite')
+ col.prop(self, self.blfields['interval_closed'], text='š Closed')
+
+ ####################
+ # - FlowKinds
+ ####################
+ @events.computes_output_socket(
+ # Trigger
+ 'Expr',
+ kind=ct.FlowKind.Value,
+ # Loaded
+ props={'symbol'},
+ )
+ def compute_value(self, props) -> typ.Any:
+ return props['symbol'].sp_symbol
+
+ @events.computes_output_socket(
+ # Trigger
+ 'Expr',
+ kind=ct.FlowKind.Func,
+ # Loaded
+ props={'symbol'},
+ )
+ def compute_lazy_func(self, props) -> typ.Any:
+ sp_sym = props['symbol'].sp_symbol
+ return ct.FuncFlow(
+ func=sp.lambdify(sp_sym, sp_sym, 'jax'),
+ func_args=[sp_sym],
+ supports_jax=True,
+ )
+
+ ####################
+ # - FlowKinds: Auxiliary
+ ####################
+ @events.computes_output_socket(
+ # Trigger
+ 'Expr',
+ kind=ct.FlowKind.Info,
+ # Loaded
+ props={'symbol'},
+ )
+ def compute_info(self, props) -> typ.Any:
+ return ct.InfoFlow(
+ output=props['symbol'],
+ )
+
+ @events.computes_output_socket(
+ # Trigger
+ 'Expr',
+ kind=ct.FlowKind.Params,
+ # Loaded
+ props={'symbol'},
+ )
+ def compute_params(self, props) -> typ.Any:
+ sym = props['symbol']
+ return ct.ParamsFlow(
+ arg_targets=[sym],
+ func_args=[sym.sp_symbol],
+ symbols={sym},
+ is_differentiable=(
+ sym.mathtype in [spux.MathType.Real, spux.MathType.Complex]
+ ),
+ )
+
+
+####################
+# - Blender Registration
+####################
+BL_REGISTER = [
+ SymbolConstantNode,
+]
+BL_NODES = {ct.NodeType.SymbolConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)}
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/data_file_importer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/data_file_importer.py
index 4bbe43e..3d5af11 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/data_file_importer.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/file_importers/data_file_importer.py
@@ -50,10 +50,15 @@ class DataFileImporterNode(base.MaxwellSimNode):
# - Properties
####################
@events.on_value_changed(
+ # Trigger
socket_name={'File Path'},
+ # Loaded
input_sockets={'File Path'},
input_socket_kinds={'File Path': ct.FlowKind.Value},
input_sockets_optional={'File Path': True},
+ # Flow
+ ## -> See docs in TransformMathNode
+ stop_propagation=True,
)
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
has_file_path = not ct.FlowSignal.check(input_sockets['File Path'])
@@ -83,7 +88,15 @@ class DataFileImporterNode(base.MaxwellSimNode):
####################
# - Output Info
####################
- @bl_cache.cached_bl_property(depends_on={'file_path'})
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'output_name',
+ 'output_mathtype',
+ 'output_physical_type',
+ 'output_unit',
+ }
+ | {f'dim_{i}_name' for i in range(6)}
+ )
def expr_info(self) -> ct.InfoFlow | None:
"""Retrieve the output expression's `InfoFlow`."""
info = self.compute_output('Expr', kind=ct.FlowKind.Info)
@@ -184,19 +197,19 @@ class DataFileImporterNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Expr',
kind=ct.FlowKind.Func,
+ # Loaded
input_sockets={'File Path'},
)
- def compute_func(self, input_sockets: dict) -> td.Simulation:
+ def compute_func(self, input_sockets) -> td.Simulation:
"""Declare a lazy, composable function that returns the loaded data.
Returns:
A completely empty `ParamsFlow`, ready to be composed.
"""
file_path = input_sockets['File Path']
+ has_file_path = not ct.FlowSignal.check(file_path)
- has_file_path = not ct.FlowSignal.check(input_sockets['File Path'])
-
- if has_file_path:
+ if has_file_path and file_path is not None:
data_file_format = ct.DataFileFormat.from_path(file_path)
if data_file_format is not None:
# Jax Compatibility: Lazy Data Loading
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 b2aed00..7528717 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
@@ -195,18 +195,23 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
####################
# - Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Time Monitor',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews_time(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
+
+ @events.computes_output_socket(
+ 'Freq Monitor',
+ kind=ct.FlowKind.Previews,
+ # Loaded
+ props={'sim_node_name'},
+ )
+ def compute_previews_freq(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
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 2ed61f0..319ce0b 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
@@ -170,18 +170,23 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
####################
# - Preview - Changes to Input Sockets
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Time Monitor',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews_time(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
+
+ @events.computes_output_socket(
+ 'Freq Monitor',
+ kind=ct.FlowKind.Previews,
+ # Loaded
+ props={'sim_node_name'},
+ )
+ def compute_previews_freq(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/permittivity_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/permittivity_monitor.py
index 0246f97..b5a287c 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/permittivity_monitor.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/permittivity_monitor.py
@@ -119,18 +119,14 @@ class PermittivityMonitorNode(base.MaxwellSimNode):
####################
# - Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Permittivity Monitor',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews_freq(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/file_exporters/data_file_exporter.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/file_exporters/data_file_exporter.py
index 99b7257..4cf1503 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/file_exporters/data_file_exporter.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/file_exporters/data_file_exporter.py
@@ -232,8 +232,17 @@ class DataFileExporterNode(base.MaxwellSimNode):
dim.name for dim in params.symbols if dim in info.dims
}:
self.loose_input_sockets = {
- dim_name: sockets.ExprSocketDef(**expr_info)
- for dim_name, expr_info in params.sym_expr_infos(info).items()
+ sym.name: sockets.ExprSocketDef(
+ **(
+ expr_info
+ | {
+ 'active_kind': ct.FlowKind.Range
+ if sym in info.dims
+ else ct.FlowKind.Value
+ }
+ )
+ )
+ for sym, expr_info in params.sym_expr_infos.items()
}
elif self.loose_input_sockets:
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 7af1295..9198f6a 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
@@ -18,6 +18,7 @@ import typing as typ
import bpy
import sympy as sp
+import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux
@@ -88,32 +89,79 @@ class ViewerNode(base.MaxwellSimNode):
socket_name='Any',
)
def on_input_changed(self) -> None:
- self.input_flow = bl_cache.Signal.InvalidateCache
-
- @bl_cache.cached_bl_property()
- def input_flow(self) -> dict[ct.FlowKind, typ.Any | None]:
- input_flow = {}
+ """Lightweight invalidator, which invalidates the more specific `cached_bl_property` used to determine when something ex. plot-related has changed.
+ Calls `get_flow`, which will be called again when regenerating the `cached_bl_property`s.
+ This **does not** call the flow twice, as `self._compute_input()` will be cached the first time.
+ """
for flow_kind in list(ct.FlowKind):
+ flow = self.get_flow(
+ flow_kind, always_load=flow_kind is ct.FlowKind.Previews
+ )
+ if flow is not None:
+ setattr(
+ self,
+ 'input_' + flow_kind.property_name,
+ bl_cache.Signal.InvalidateCache,
+ )
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_capabilities(self) -> ct.CapabilitiesFlow | None:
+ return self.get_flow(ct.FlowKind.Capabilities)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_previews(self) -> ct.PreviewsFlow | None:
+ return self.get_flow(ct.FlowKind.Previews, always_load=True)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_value(self) -> ct.ValueFlow | None:
+ return self.get_flow(ct.FlowKind.Value)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_array(self) -> ct.ArrayFlow | None:
+ return self.get_flow(ct.FlowKind.Array)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_lazy_range(self) -> ct.RangeFlow | None:
+ return self.get_flow(ct.FlowKind.Range)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_lazy_func(self) -> ct.FuncFlow | None:
+ return self.get_flow(ct.FlowKind.Func)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_params(self) -> ct.ParamsFlow | None:
+ return self.get_flow(ct.FlowKind.Params)
+
+ @bl_cache.cached_bl_property(depends_on={'auto_expr'})
+ def input_info(self) -> ct.InfoFlow | None:
+ return self.get_flow(ct.FlowKind.Info)
+
+ def get_flow(
+ self, flow_kind: ct.FlowKind, always_load: bool = False
+ ) -> typ.Any | None:
+ """Generic interface to simplify getting `FlowKind` properties on the viewer node."""
+ if self.auto_expr or always_load:
flow = self._compute_input('Any', kind=flow_kind)
has_flow = not ct.FlowSignal.check(flow)
if has_flow:
- input_flow |= {flow_kind: flow}
- else:
- input_flow |= {flow_kind: None}
-
- return input_flow
+ return flow
+ return None
+ return None
####################
# - Property: Input Expression String Lines
####################
- @bl_cache.cached_bl_property(depends_on={'input_flow'})
+ @bl_cache.cached_bl_property(depends_on={'input_value'})
def input_expr_str_entries(self) -> list[list[str]] | None:
- value = self.input_flow.get(ct.FlowKind.Value)
+ value = self.input_value
+ if value is None:
+ return None
+ # Parse SympyType
def sp_pretty(v: spux.SympyExpr) -> spux.SympyExpr:
- ## sp.pretty makes new lines and wreaks havoc.
+ ## -> The real sp.pretty makes new lines and wreaks havoc.
return spux.sp_to_str(v.n(4))
if isinstance(value, spux.SympyType):
@@ -124,6 +172,25 @@ class ViewerNode(base.MaxwellSimNode):
]
return [[sp_pretty(value)]]
+
+ # Parse Tidy3D Types
+ if isinstance(value, td.Structure):
+ return [
+ [str(key), str(value)]
+ for key, value in dict(value).items()
+ if key not in ['type', 'geometry', 'medium']
+ ] + [
+ [str(key), str(value)]
+ for key, value in dict(value.geometry).items()
+ if key != 'type'
+ ]
+ if isinstance(value, td.components.base.Tidy3dBaseModel):
+ return [
+ [str(key), str(value)]
+ for key, value in dict(value).items()
+ if key != 'type'
+ ]
+
return None
####################
@@ -132,12 +199,12 @@ class ViewerNode(base.MaxwellSimNode):
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
row = layout.row(align=True)
+ # Automatic Expression Printing
+ row.prop(self, self.blfields['auto_expr'], text='Live', toggle=True)
+
# Debug Mode On/Off
row.prop(self, self.blfields['debug_mode'], text='Debug', toggle=True)
- # Automatic Expression Printing
- row.prop(self, self.blfields['auto_expr'], text='Expr', toggle=True)
-
# Debug Mode Operators
if self.debug_mode:
layout.prop(self, self.blfields['console_print_kind'], text='')
@@ -210,47 +277,47 @@ class ViewerNode(base.MaxwellSimNode):
# - Methods
####################
def print_data_to_console(self):
- if not self.inputs['Any'].is_linked:
- return
+ flow = self._compute_input('Any', kind=self.console_print_kind)
log.info('Printing to Console')
- data = self._compute_input('Any', kind=self.console_print_kind, optional=True)
-
- if isinstance(data, spux.SympyType):
- console.print(sp.pretty(data, use_unicode=True))
+ if isinstance(flow, spux.SympyType):
+ console.print(sp.pretty(flow, use_unicode=True))
else:
- console.print(data)
+ console.print(flow)
####################
# - Event Methods
####################
@events.on_value_changed(
- socket_name='Any',
- prop_name='auto_plot',
- props={'auto_plot'},
+ # Trigger
+ prop_name={'input_previews', 'auto_plot'},
+ # Loaded
+ props={'input_previews', 'auto_plot'},
)
def on_changed_plot_preview(self, props):
- node_tree = self.id_data
+ previews = props['input_previews']
+ if previews is not None:
+ if props['auto_plot']:
+ bl_socket = self.inputs['Any']
+ if bl_socket.is_linked:
+ bl_socket.links[0].from_node.compute_plot()
- # Unset Plot if Nothing Plotted
- with node_tree.replot():
- if props['auto_plot'] and self.inputs['Any'].is_linked:
- self.inputs['Any'].links[0].from_socket.node.trigger_event(
- ct.FlowEvent.ShowPlot
- )
+ previews.update_image_preview()
+ else:
+ ct.PreviewsFlow.hide_image_preview()
@events.on_value_changed(
- socket_name='Any',
- prop_name='auto_3d_preview',
- props={'auto_3d_preview'},
+ # Trigger
+ prop_name={'input_previews', 'auto_3d_preview'},
+ # Loaded
+ props={'input_previews', 'auto_3d_preview'},
)
def on_changed_3d_preview(self, props):
- node_tree = self.id_data
-
- # Remove Non-Repreviewed Previews on Close
- with node_tree.repreview_all():
- if props['auto_3d_preview']:
- self.trigger_event(ct.FlowEvent.ShowPreview)
+ previews = props['input_previews']
+ if previews is not None and props['auto_3d_preview']:
+ previews.update_bl_object_previews()
+ else:
+ ct.PreviewsFlow.hide_bl_object_previews()
####################
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py
index 2ef45a0..5a1901c 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/fdtd_sim.py
@@ -64,20 +64,22 @@ class FDTDSimNode(base.MaxwellSimNode):
},
)
def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr:
- ## TODO: Visualize the boundary conditions on top of the sim domain
+ if any(ct.FlowSignal.check(inp) for inp in input_sockets):
+ return ct.FlowSignal.FlowPending
+
sim_domain = input_sockets['Domain']
sources = input_sockets['Sources']
structures = input_sockets['Structures']
bounds = input_sockets['BCs']
monitors = input_sockets['Monitors']
-
return td.Simulation(
- **sim_domain, ## run_time=, size=, grid=, medium=
+ **sim_domain,
structures=structures,
sources=sources,
monitors=monitors,
boundary_spec=bounds,
)
+ ## TODO: Visualize the boundary conditions on top of the sim domain
####################
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py
index d8aa1a2..66a0696 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/simulations/sim_domain.py
@@ -93,18 +93,14 @@ class SimDomainNode(base.MaxwellSimNode):
####################
# - Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Domain',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
## Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py
index d3c246a..85f95dc 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/gaussian_beam_source.py
@@ -165,18 +165,14 @@ class GaussianBeamSourceNode(base.MaxwellSimNode):
####################
# - Preview - Changes to Input Sockets
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Angled Source',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py
index 496881c..5d3f26a 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/plane_wave_source.py
@@ -129,18 +129,14 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
####################
# - Preview - Changes to Input Sockets
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Angled Source',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py
index 6dfe144..ad438bc 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/sources/point_dipole_source.py
@@ -104,18 +104,14 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
####################
# - Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Source',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
socket_name={'Center'},
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py
index 367156e..27369d5 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/geonodes_structure.py
@@ -132,18 +132,14 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
####################
# - Events: Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Structure',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py
index 6911682..d4d54b2 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/box_structure.py
@@ -16,13 +16,15 @@
import typing as typ
+import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
+import tidy3d.plugins.adjoint as tdadj
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
+from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux
-from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import managed_objs, sockets
@@ -62,41 +64,172 @@ class BoxStructureNode(base.MaxwellSimNode):
}
####################
- # - Outputs
+ # - Properties
+ ####################
+ differentiable: bool = bl_cache.BLField(False)
+
+ ####################
+ # - UI
+ ####################
+ def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
+ layout.prop(
+ self,
+ self.blfields['differentiable'],
+ text='Differentiable',
+ toggle=True,
+ )
+
+ ####################
+ # - FlowKind.Value
####################
@events.computes_output_socket(
'Structure',
+ kind=ct.FlowKind.Value,
+ # Loaded
+ props={'differentiable'},
input_sockets={'Medium', 'Center', 'Size'},
- unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
- scale_input_sockets={
- 'Center': 'Tidy3DUnits',
- 'Size': 'Tidy3DUnits',
+ output_sockets={'Structure'},
+ output_socket_kinds={'Structure': ct.FlowKind.Params},
+ )
+ def compute_value(self, props, input_sockets, output_sockets) -> td.Box:
+ output_params = output_sockets['Structure']
+ center = input_sockets['Center']
+ size = input_sockets['Size']
+ medium = input_sockets['Medium']
+
+ has_output_params = not ct.FlowSignal.check(output_params)
+ has_center = not ct.FlowSignal.check(center)
+ has_size = not ct.FlowSignal.check(size)
+ has_medium = not ct.FlowSignal.check(medium)
+
+ if (
+ has_center
+ and has_size
+ and has_medium
+ and has_output_params
+ and not props['differentiable']
+ and not output_params.symbols
+ ):
+ return td.Structure(
+ geometry=td.Box(
+ center=spux.scale_to_unit_system(center, ct.UNITS_TIDY3D),
+ size=spux.scale_to_unit_system(size, ct.UNITS_TIDY3D),
+ ),
+ medium=medium,
+ )
+ return ct.FlowSignal.FlowPending
+
+ ####################
+ # - FlowKind.Func
+ ####################
+ @events.computes_output_socket(
+ 'Structure',
+ kind=ct.FlowKind.Func,
+ # Loaded
+ props={'differentiable'},
+ input_sockets={'Medium', 'Center', 'Size'},
+ input_socket_kinds={
+ 'Medium': ct.FlowKind.Func,
+ 'Center': ct.FlowKind.Func,
+ 'Size': ct.FlowKind.Func,
+ },
+ output_sockets={'Structure'},
+ output_socket_kinds={'Structure': ct.FlowKind.Params},
+ )
+ def compute_lazy_structure(self, props, input_sockets, output_sockets) -> td.Box:
+ output_params = output_sockets['Structure']
+ center = input_sockets['Center']
+ size = input_sockets['Size']
+ medium = input_sockets['Medium']
+
+ has_output_params = not ct.FlowSignal.check(output_params)
+ has_center = not ct.FlowSignal.check(center)
+ has_size = not ct.FlowSignal.check(size)
+ has_medium = not ct.FlowSignal.check(medium)
+
+ differentiable = props['differentiable']
+ if (
+ has_output_params
+ and has_center
+ and has_size
+ and has_medium
+ and differentiable == output_params.is_differentiable
+ ):
+ if differentiable:
+ return (center | size | medium).compose_within(
+ enclosing_func=lambda els: tdadj.JaxStructure(
+ geometry=tdadj.JaxBox(
+ center=tuple(els[0][0].flatten()),
+ size=tuple(els[0][1].flatten()),
+ ),
+ medium=els[1],
+ ),
+ supports_jax=True,
+ )
+ return (center | size | medium).compose_within(
+ enclosing_func=lambda els: td.Structure(
+ geometry=td.Box(
+ center=tuple(els[0][0].flatten()),
+ size=tuple(els[0][1].flatten()),
+ ),
+ medium=els[1],
+ ),
+ supports_jax=False,
+ )
+ return ct.FlowSignal.FlowPending
+
+ ####################
+ # - FlowKind.Params
+ ####################
+ @events.computes_output_socket(
+ 'Structure',
+ kind=ct.FlowKind.Params,
+ # Loaded
+ props={'differentiable'},
+ input_sockets={'Medium', 'Center', 'Size'},
+ input_socket_kinds={
+ 'Medium': ct.FlowKind.Params,
+ 'Center': ct.FlowKind.Params,
+ 'Size': ct.FlowKind.Params,
},
)
- def compute_structure(self, input_sockets, unit_systems) -> td.Box:
- return td.Structure(
- geometry=td.Box(
- center=input_sockets['Center'],
- size=input_sockets['Size'],
- ),
- medium=input_sockets['Medium'],
- )
+ def compute_params(self, props, input_sockets) -> td.Box:
+ center = input_sockets['Center']
+ size = input_sockets['Size']
+ medium = input_sockets['Medium']
+
+ has_center = not ct.FlowSignal.check(center)
+ has_size = not ct.FlowSignal.check(size)
+ has_medium = not ct.FlowSignal.check(medium)
+
+ if has_center and has_size and has_medium:
+ if props['differentiable'] == (
+ center.is_differentiable
+ & size.is_differentiable
+ & medium.is_differentiable
+ ):
+ return center | size | medium
+ return ct.FlowSignal.FlowPending
+ return ct.FlowSignal.FlowPending
####################
# - Events: Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Structure',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
+ output_sockets={'Structure'},
+ output_socket_kinds={'Structure': ct.FlowKind.Params},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props, output_sockets):
+ output_params = output_sockets['Structure']
+ has_output_params = not ct.FlowSignal.check(output_params)
+
+ if has_output_params and not output_params.symbols:
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
+ return ct.PreviewsFlow()
@events.on_value_changed(
# Trigger
@@ -105,29 +238,26 @@ class BoxStructureNode(base.MaxwellSimNode):
# Loaded
input_sockets={'Center', 'Size'},
managed_objs={'modifier'},
- unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
- scale_input_sockets={
- 'Center': 'BlenderUnits',
- },
+ output_sockets={'Structure'},
+ output_socket_kinds={'Structure': ct.FlowKind.Params},
)
- def on_inputs_changed(
- self,
- managed_objs,
- input_sockets,
- unit_systems,
- ):
- # Push Loose Input Values to GeoNodes Modifier
- managed_objs['modifier'].bl_modifier(
- 'NODES',
- {
- 'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
- 'unit_system': unit_systems['BlenderUnits'],
- 'inputs': {
- 'Size': input_sockets['Size'],
+ def on_inputs_changed(self, managed_objs, input_sockets, output_sockets):
+ output_params = output_sockets['Structure']
+ has_output_params = not ct.FlowSignal.check(output_params)
+ if has_output_params and not output_params.symbols:
+ # Push Loose Input Values to GeoNodes Modifier
+ center = input_sockets['Center']
+ managed_objs['modifier'].bl_modifier(
+ 'NODES',
+ {
+ 'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
+ 'unit_system': ct.UNITS_BLENDER,
+ 'inputs': {
+ 'Size': input_sockets['Size'],
+ },
},
- },
- location=input_sockets['Center'],
- )
+ location=spux.scale_to_unit_system(center, ct.UNITS_BLENDER),
+ )
####################
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/cylinder_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/cylinder_structure.py
index a89ef06..c1f7440 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/cylinder_structure.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/cylinder_structure.py
@@ -89,18 +89,14 @@ class CylinderStructureNode(base.MaxwellSimNode):
####################
# - Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Structure',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py
index 1b4bca3..d5a83b4 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/structures/primitives/sphere_structure.py
@@ -83,18 +83,14 @@ class SphereStructureNode(base.MaxwellSimNode):
####################
# - Preview
####################
- @events.on_value_changed(
- # Trigger
- prop_name='preview_active',
+ @events.computes_output_socket(
+ 'Structure',
+ kind=ct.FlowKind.Previews,
# Loaded
- managed_objs={'modifier'},
- props={'preview_active'},
+ props={'sim_node_name'},
)
- def on_preview_changed(self, managed_objs, props):
- if props['preview_active']:
- managed_objs['modifier'].show_preview()
- else:
- managed_objs['modifier'].hide_preview()
+ def compute_previews(self, props):
+ return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
@events.on_value_changed(
# Trigger
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 e75bb21..2cb3138 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
@@ -50,8 +50,10 @@ class SocketDef(pyd.BaseModel, abc.ABC):
Parameters:
bl_socket: The Blender node socket to alter using data from this SocketDef.
"""
+ log.debug('%s: Start Socket Preinit', bl_socket.bl_label)
bl_socket.reset_instance_id()
bl_socket.regenerate_dynamic_field_persistance()
+ log.debug('%s: End Socket Preinit', bl_socket.bl_label)
def postinit(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Pre-initialize a real Blender node socket from this socket definition.
@@ -59,8 +61,12 @@ class SocketDef(pyd.BaseModel, abc.ABC):
Parameters:
bl_socket: The Blender node socket to alter using data from this SocketDef.
"""
+ log.debug('%s: Start Socket Postinit', bl_socket.bl_label)
bl_socket.is_initializing = False
bl_socket.on_active_kind_changed()
+ bl_socket.on_socket_props_changed(set(bl_socket.blfields))
+ bl_socket.on_data_changed(set(ct.FlowKind))
+ log.debug('%s: End Socket Postinit', bl_socket.bl_label)
@abc.abstractmethod
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
@@ -135,6 +141,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
socket_type: ct.SocketType
bl_label: str
+ use_linked_capabilities: bool = bl_cache.BLField(False, use_prop_update=False)
+
## Computed by Subclass
bl_idname: str
@@ -181,17 +189,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
"""
self.display_shape = self.active_kind.socket_shape
- def on_socket_prop_changed(self, prop_name: str) -> None:
- """Called when a property has been updated.
+ def on_socket_props_changed(self, prop_names: set[str]) -> None:
+ """Called when a set of properties has been updated.
Notes:
- Can be overridden if a socket needs to respond to a property change.
+ Can be overridden if a socket needs to respond to property changes.
**Always prefer using node events instead of overriding this in a socket**.
Think **very carefully** before using this, and use it with the greatest of care.
Attributes:
- prop_name: The name of the property that was changed.
+ prop_names: The set of property names that were changed.
"""
def on_prop_changed(self, prop_name: str) -> None:
@@ -207,30 +215,49 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
Attributes:
prop_name: The name of the property that was changed.
"""
- # All Attributes: Trigger Local Event
- ## -> While initializing, only `DataChanged` won't trigger.
- if hasattr(self, prop_name):
- # Property Callbacks: Active Kind
- ## -> WARNING: May NOT rely on flow.
- if prop_name == 'active_kind':
+ # BLField Attributes: Invalidate BLField Dependents
+ ## -> All invalidated blfields will have their caches cleared.
+ ## -> The (topologically) ordered list of cleared blfields is returned.
+ ## -> WARNING: The chain is not checked for ex. cycles.
+ if not self.is_initializing and prop_name in self.blfields:
+ cleared_blfields = self.clear_blfields_after(prop_name)
+ set_of_cleared_blfields = set(cleared_blfields)
+
+ # Property Callbacks: Internal
+ ## -> NOTE: May NOT recurse on_prop_changed.
+ if ('active_kind', 'invalidate') in set_of_cleared_blfields:
+ # log.debug(
+ # '%s (NodeSocket): Changed Active Kind',
+ # self.bl_label,
+ # )
self.on_active_kind_changed()
# Property Callbacks: Per-Socket
- ## -> WARNING: May NOT rely on flow.
- self.on_socket_prop_changed(prop_name)
+ ## -> NOTE: User-defined handlers might recurse on_prop_changed.
+ self.is_initializing = True
+ self.on_socket_props_changed(set_of_cleared_blfields)
+ self.is_initializing = False
- # Not Initializing: Trigger Event
- ## -> This declares that the socket has changed.
- ## -> This should happen first, in case dependents need a cache.
- if not self.is_initializing:
- self.trigger_event(ct.FlowEvent.DataChanged)
-
- # BLField Attributes: Invalidate BLField Dependents
- ## -> Dependent props will generally also trigger on_prop_changed.
- ## -> The recursion ends with the depschain.
- ## -> WARNING: The chain is not checked for ex. cycles.
- if prop_name in self.blfields:
- self.invalidate_blfield_deps(prop_name)
+ # Trigger Event
+ ## -> Before SocketDef.postinit(), never emit DataChanged.
+ ## -> ONLY emit DataChanged if a FlowKind-bound prop was cleared.
+ ## -> ONLY emit a single DataChanged w/set of altered FlowKinds.
+ ## w/node's trigger_event, we've guaranteed a minimal action.
+ socket_kinds = {
+ ct.FlowKind.from_property_name(prop_name)
+ for prop_name in {
+ prop_name
+ for prop_name, clear_method in set_of_cleared_blfields
+ if clear_method == 'invalidate'
+ }.intersection(ct.FlowKind.property_names)
+ }
+ # log.debug(
+ # '%s (NodeSocket): Computed SocketKind Frontier: %s',
+ # self.bl_label,
+ # str(socket_kinds),
+ # )
+ if socket_kinds:
+ self.trigger_event(ct.FlowEvent.DataChanged, socket_kinds=socket_kinds)
####################
# - Link Event: Consent / On Change
@@ -273,11 +300,29 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
return False
# Capability Check
- if not link.from_socket.capabilities.is_compatible_with(self.capabilities):
+ ## -> "Use Linked Capabilities" allow sockets flow-dependent caps.
+ ## -> The tradeoff: No link if there is no InfoFlow.
+ if self.use_linked_capabilities:
+ info = self.compute_data(kind=ct.FlowKind.Info)
+ has_info = not ct.FlowSignal.check(info)
+ if has_info:
+ incoming_capabilities = link.from_socket.linked_capabilities(info)
+ else:
+ log.error(
+ 'Attempted to link output socket "%s" to input socket "%s" (%s), but linked capabilities of the output socket could not be determined',
+ link.from_socket.bl_label,
+ self.bl_label,
+ self.capabilities,
+ )
+ return False
+ else:
+ incoming_capabilities = link.from_socket.capabilities
+
+ if not incoming_capabilities.is_compatible_with(self.capabilities):
log.error(
'Attempted to link output socket "%s" (%s) to input socket "%s" (%s), but capabilities are incompatible',
link.from_socket.bl_label,
- link.from_socket.capabilities,
+ incoming_capabilities,
self.bl_label,
self.capabilities,
)
@@ -288,6 +333,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
def on_link_added(self, link: bpy.types.NodeLink) -> None: # noqa: ARG002
"""Triggers a `ct.FlowEvent.LinkChanged` event when a link is added.
+ Calls `self.trigger_event()` with `FlowKind`s, since an added link requires recomputing **all** data that depends on flow.
+
Notes:
Called by the node tree, generally (but not guaranteed) after `self.allow_add_link()` has given consent to add the link.
@@ -295,7 +342,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
link: The node link that was added.
Currently unused.
"""
- self.trigger_event(ct.FlowEvent.LinkChanged)
+ self.trigger_event(ct.FlowEvent.LinkChanged, socket_kinds=set(ct.FlowKind))
def allow_remove_link(self, from_socket: bpy.types.NodeSocket) -> bool: # noqa: ARG002
"""Called to ask whether a link may be removed from this `to_socket`.
@@ -333,6 +380,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
def on_link_removed(self, from_socket: bpy.types.NodeSocket) -> None: # noqa: ARG002
"""Triggers a `ct.FlowEvent.LinkChanged` event when a link is removed.
+ Calls `self.trigger_event()` with `FlowKind`s, since a removed link requires recomputing **all** data that depends on flow.
+
Notes:
Called by the node tree, generally (but not guaranteed) after `self.allow_remove_link()` has given consent to remove the link.
@@ -340,7 +389,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
from_socket: The node socket that was attached to before link removal.
Currently unused.
"""
- self.trigger_event(ct.FlowEvent.LinkChanged)
+ self.trigger_event(ct.FlowEvent.LinkChanged, socket_kinds=set(ct.FlowKind))
def remove_invalidated_links(self) -> None:
"""Reevaluates the capabilities of all socket links, and removes any that no longer match.
@@ -371,6 +420,41 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
####################
# - Event Chain
####################
+ def on_data_changed(self, socket_kinds: set[ct.FlowKind]) -> None:
+ """Called when `ct.FlowEvent.DataChanged` flows through this socket.
+
+ Parameters:
+ socket_kinds: The altered `ct.FlowKind`s flowing through.
+ """
+ self.on_socket_data_changed(socket_kinds)
+
+ def on_socket_data_changed(self, socket_kinds: set[ct.FlowKind]) -> None:
+ """Called when `ct.FlowEvent.DataChanged` flows through this socket.
+
+ Notes:
+ Can be overridden if a socket needs to respond to `DataChanged` in a custom way.
+
+ **Always prefer using node events instead of overriding this in a socket**.
+ Think **very carefully** before using this, and use it with the greatest of care.
+
+ Parameters:
+ socket_kinds: The altered `ct.FlowKind`s flowing through.
+ """
+
+ def on_link_changed(self) -> None:
+ """Called when `ct.FlowEvent.LinkChanged` flows through this socket."""
+ self.on_socket_link_changed()
+
+ def on_socket_link_changed(self) -> None:
+ """Called when `ct.FlowEvent.LinkChanged` flows through this socket.
+
+ Notes:
+ Can be overridden if a socket needs to respond to `LinkChanged` in a custom way.
+
+ **Always prefer using node events instead of overriding this in a socket**.
+ Think **very carefully** before using this, and use it with the greatest of care.
+ """
+
def trigger_event(
self,
event: ct.FlowEvent,
@@ -384,7 +468,6 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
- **Output Socket -> Input**: Trigger event on node (w/`socket_name`).
- **Output Socket -> Output**: Trigger event on `to_socket`s along output links.
-
Notes:
This can be an unpredictably heavy function, depending on the node graph topology.
@@ -395,11 +478,41 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
event: The event to report along the node tree.
The value of `ct.FlowEvent.flow_direction[event]` (`input` or `output`) determines the direction that an event flows.
"""
+ # log.debug(
+ # '[%s] [%s] Triggered (socket_kinds=%s)',
+ # self.name,
+ # event,
+ # str(socket_kinds),
+ # )
+ # Local DataChanged Callbacks
+ ## -> socket_kinds MUST NOT be None
+ if event is ct.FlowEvent.DataChanged:
+ # WORKAROUND
+ ## -> Altering value/lazy_range like this causes MANY DataChanged
+ ## -> If we pretend we're initializing, we can block on_prop_changed
+ ## -> This works because _unit conversion doesn't change the value_
+ ## -> Only the displayed values change - which are inv. on __set__.
+ ## -> For this reason alone, we can get away with it :)
+ ## -> TODO: This is not clean :)
+ self.is_initializing = True
+ self.on_data_changed(socket_kinds)
+ self.is_initializing = False
+
+ # Local LinkChanged Callbacks
+ ## -> socket_kinds MUST NOT be None
+ if event is ct.FlowEvent.LinkChanged:
+ self.is_initializing = True
+ self.on_link_changed()
+ self.on_data_changed(socket_kinds)
+ self.is_initializing = False
+
flow_direction = ct.FlowEvent.flow_direction[event]
# Locking
- if event in [ct.FlowEvent.EnableLock, ct.FlowEvent.DisableLock]:
- self.locked = event == ct.FlowEvent.EnableLock
+ if event is ct.FlowEvent.EnableLock:
+ self.locked = True
+ elif event is ct.FlowEvent.DisableLock:
+ self.locked = False
# Event by Socket Orientation | Flow Direction
match (self.is_output, flow_direction):
@@ -408,7 +521,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
link.from_socket.trigger_event(event, socket_kinds=socket_kinds)
case (False, 'output'):
- if event == ct.FlowEvent.LinkChanged:
+ if event is ct.FlowEvent.LinkChanged:
self.node.trigger_event(
ct.FlowEvent.DataChanged,
socket_name=self.name,
@@ -432,6 +545,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
# - FlowKind: Auxiliary
####################
# Capabilities
+ def linked_capabilities(self, info: ct.InfoFlow) -> ct.CapabilitiesFlow:
+ """Try this first when `is_linked and use_linked_capabilities`."""
+ raise NotImplementedError
+
@property
def capabilities(self) -> None:
"""By default, the socket is linkeable with any other socket of the same type and active kind.
@@ -592,21 +709,16 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
Raises:
ValueError: When referencing a socket that's meant to be directly referenced.
"""
- kind_data_map = {
+ return {
ct.FlowKind.Capabilities: lambda: self.capabilities,
+ ct.FlowKind.Previews: lambda: ct.PreviewsFlow(),
ct.FlowKind.Value: lambda: self.value,
ct.FlowKind.Array: lambda: self.array,
ct.FlowKind.Func: lambda: self.lazy_func,
ct.FlowKind.Range: lambda: self.lazy_range,
ct.FlowKind.Params: lambda: self.params,
ct.FlowKind.Info: lambda: self.info,
- }
- if kind in kind_data_map:
- return kind_data_map[kind]()
-
- ## TODO: Reflect this constraint in the type
- msg = f'Socket {self.bl_label} ({self.socket_type}): Kind {kind} cannot be computed within a socket "compute_data", as it is meant to be referenced directly'
- raise ValueError(msg)
+ }[kind]()
def compute_data(
self,
@@ -635,7 +747,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
return self.node.compute_output(self.name, kind=kind)
# Compute Input Socket
- ## Unlinked: Retrieve Socket Value
+ ## -> Unlinked: Retrieve Socket Value
if not self.is_linked:
return self._compute_data(kind)
@@ -645,7 +757,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
linked_values = [link.from_socket.compute_data(kind) for link in self.links]
# Return Single Value / List of Values
- if len(linked_values) == 1:
+ ## -> Multi-input sockets are not yet supported.
+ if linked_values:
return linked_values[0]
# Edge Case: While Dragging Link (but not yet removed)
@@ -653,11 +766,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
## - self.is_linked = True, since the user hasn't confirmed anything.
## - self.links will be empty, since the link object was freed.
## When this particular condition is met, pretend that we're not linked.
- if len(linked_values) == 0:
- return self._compute_data(kind)
-
- msg = f'Socket {self.bl_label} ({self.socket_type}): Multi-input sockets are not yet supported'
- raise NotImplementedError(msg)
+ return self._compute_data(kind)
####################
# - UI - Color
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 45051e0..72eb220 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
@@ -14,6 +14,8 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
+from blender_maxwell.utils import bl_cache
+
from ... import contracts as ct
from .. import base
@@ -25,8 +27,8 @@ class AnyBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Any
bl_label = 'Any'
- @property
- def capabilities(self):
+ @bl_cache.cached_bl_property(depends_on={'active_kind'})
+ def capabilities(self) -> ct.CapabilitiesFlow:
return ct.CapabilitiesFlow(
socket_type=self.socket_type,
active_kind=self.active_kind,
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool.py
index b4b9913..d2c4f4b 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/bool.py
@@ -16,7 +16,7 @@
import bpy
-from blender_maxwell.utils import bl_cache, logger
+from blender_maxwell.utils import bl_cache
from ... import contracts as ct
from .. import base
@@ -43,7 +43,7 @@ class BoolBLSocket(base.MaxwellSimSocket):
####################
# - Computation of Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> bool:
return self.raw_value
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path.py
index a9738b0..e6a2e5f 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/file_path.py
@@ -48,7 +48,7 @@ class FilePathBLSocket(base.MaxwellSimSocket):
####################
# - FlowKind: Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> Path:
return self.raw_value
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/string.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/string.py
index 9aba1bb..b198935 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/string.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/string.py
@@ -16,6 +16,8 @@
import bpy
+from blender_maxwell.utils import bl_cache
+
from ... import contracts as ct
from .. import base
@@ -30,12 +32,7 @@ class StringBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- raw_value: bpy.props.StringProperty(
- name='String',
- description='Represents a string',
- default='',
- update=(lambda self, context: self.on_prop_changed('raw_value', context)),
- )
+ raw_value: str = bl_cache.BLField('')
####################
# - Socket UI
@@ -46,7 +43,7 @@ class StringBLSocket(base.MaxwellSimSocket):
####################
# - Computation of Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> str:
return self.raw_value
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py
index 78fb328..b812e99 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/geonodes.py
@@ -71,7 +71,7 @@ class BlenderGeoNodesBLSocket(base.MaxwellSimSocket):
####################
# - Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> bpy.types.NodeTree | ct.FlowSignal:
return self.raw_value if self.raw_value is not None else ct.FlowSignal.NoFlow
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image.py
index c832152..1d6b100 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/image.py
@@ -16,6 +16,8 @@
import bpy
+from blender_maxwell.utils import bl_cache, logger
+
from ... import contracts as ct
from .. import base
@@ -30,12 +32,7 @@ class BlenderImageBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- raw_value: bpy.props.PointerProperty(
- name='Blender Image',
- description='Represents a Blender Image',
- type=bpy.types.Image,
- update=(lambda self, context: self.on_prop_changed('raw_value', context)),
- )
+ raw_value: bpy.types.Image = bl_cache.BLField()
####################
# - UI
@@ -46,7 +43,7 @@ class BlenderImageBLSocket(base.MaxwellSimSocket):
####################
# - Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> bpy.types.Image | None:
return self.raw_value
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py
index 52f4a16..b31e716 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/material.py
@@ -16,6 +16,8 @@
import bpy
+from blender_maxwell.utils import bl_cache, logger
+
from ... import contracts as ct
from .. import base
@@ -27,12 +29,7 @@ class BlenderMaterialBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- raw_value: bpy.props.PointerProperty(
- name='Blender Material',
- description='Represents a Blender material',
- type=bpy.types.Material,
- update=(lambda self, context: self.on_prop_changed('raw_value', context)),
- )
+ raw_value: bpy.types.Material = bl_cache.BLField()
####################
# - UI
@@ -43,7 +40,7 @@ class BlenderMaterialBLSocket(base.MaxwellSimSocket):
####################
# - Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> bpy.types.Material | None:
return self.raw_value
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py
index e1251ee..c2f53fe 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/object.py
@@ -16,29 +16,12 @@
import bpy
+from blender_maxwell.utils import bl_cache, logger
+
from ... import contracts as ct
from .. import base
-
-####################
-# - Create and Assign BL Object
-####################
-class BlenderMaxwellCreateAndAssignBLObject(bpy.types.Operator):
- bl_idname = 'blender_maxwell.create_and_assign_bl_object'
- bl_label = 'Create and Assign BL Object'
-
- node_tree_name = bpy.props.StringProperty(name='Node Tree Name')
- node_name = bpy.props.StringProperty(name='Node Name')
- socket_name = bpy.props.StringProperty(name='Socket Name')
-
- def execute(self, context):
- node_tree = bpy.data.node_groups[self.node_tree_name]
- node = node_tree.nodes[self.node_name]
- socket = node.inputs[self.socket_name]
-
- socket.create_and_assign_bl_object()
-
- return {'FINISHED'}
+log = logger.get(__name__)
####################
@@ -51,47 +34,18 @@ class BlenderObjectBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- raw_value: bpy.props.PointerProperty(
- name='Blender Object',
- description='Represents a Blender object',
- type=bpy.types.Object,
- update=(lambda self, context: self.on_prop_changed('raw_value', context)),
- )
+ raw_value: bpy.types.Object = bl_cache.BLField()
####################
# - UI
####################
- def draw_label_row(self, label_col_row, text):
- label_col_row.label(text=text)
-
- op = label_col_row.operator(
- 'blender_maxwell.create_and_assign_bl_object',
- text='',
- icon='ADD',
- )
- op.socket_name = self.name
- op.node_name = self.node.name
- op.node_tree_name = self.node.id_data.name
-
def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'raw_value', text='')
- ####################
- # - Methods
- ####################
- def create_and_assign_bl_object(self):
- node_tree = self.node.id_data
- mesh = bpy.data.meshes.new('MaxwellMesh')
- new_bl_object = bpy.data.objects.new('MaxwellObject', mesh)
-
- bpy.context.collection.objects.link(new_bl_object)
-
- self.value = new_bl_object
-
####################
# - Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> bpy.types.Object | None:
return self.raw_value
@@ -114,6 +68,5 @@ class BlenderObjectSocketDef(base.SocketDef):
# - Blender Registration
####################
BL_REGISTER = [
- BlenderMaxwellCreateAndAssignBLObject,
BlenderObjectBLSocket,
]
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text.py
index cf01d75..7ea4264 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/blender/text.py
@@ -16,9 +16,13 @@
import bpy
+from blender_maxwell.utils import bl_cache, logger
+
from ... import contracts as ct
from .. import base
+log = logger.get(__name__)
+
####################
# - Blender Socket
@@ -30,12 +34,7 @@ class BlenderTextBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- raw_value: bpy.props.PointerProperty(
- name='Blender Text',
- description='Represents a Blender text datablock',
- type=bpy.types.Text,
- update=(lambda self, context: self.on_prop_changed('raw_value', context)),
- )
+ raw_value: bpy.types.Text = bl_cache.BLField()
####################
# - UI
@@ -46,7 +45,7 @@ class BlenderTextBLSocket(base.MaxwellSimSocket):
####################
# - Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'raw_value'})
def value(self) -> bpy.types.Text:
return self.raw_value
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py
index 0212d90..cf7d47e 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py
@@ -64,21 +64,21 @@ class InfoDisplayCol(enum.StrEnum):
@staticmethod
def to_name(value: typ.Self) -> str:
+ """Friendly, single-letter, human-readable column names.
+
+ Must be concise, as there is not a lot of header space to contain these.
+ """
IDC = InfoDisplayCol
return {
IDC.Length: 'L',
- IDC.MathType: 'ā',
+ IDC.MathType: 'M',
IDC.Unit: 'U',
}[value]
@staticmethod
- def to_icon(value: typ.Self) -> str:
- IDC = InfoDisplayCol
- return {
- IDC.Length: '',
- IDC.MathType: '',
- IDC.Unit: '',
- }[value]
+ def to_icon(_: typ.Self) -> str:
+ """No icons."""
+ return ''
####################
@@ -109,6 +109,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Expr
bl_label = 'Expr'
+ use_socket_color = True
####################
# - Socket Interface
@@ -117,6 +118,58 @@ class ExprBLSocket(base.MaxwellSimSocket):
mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real)
physical_type: spux.PhysicalType = bl_cache.BLField(spux.PhysicalType.NonPhysical)
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'active_kind',
+ 'symbols',
+ 'raw_value_spstr',
+ 'raw_min_spstr',
+ 'raw_max_spstr',
+ 'output_name',
+ 'mathtype',
+ 'physical_type',
+ 'unit',
+ 'size',
+ }
+ )
+ def output_sym(self) -> sim_symbols.SimSymbol | None:
+ """Compute an appropriate `SimSymbol` to represent the mathematical and physical properties of the socket's own output.
+
+ For the parsed string expression, functionality is derived heavily from the internal method `self._parse_expr_symbol()`.
+
+ Raises:
+ NotImplementedError: When `active_kind` is neither `Value`, `Func`, or `Range`.
+ """
+ if self.symbols:
+ if self.active_kind in [ct.FlowKind.Value, ct.FlowKind.Func]:
+ return self._parse_expr_symbol(
+ self._parse_expr_str(self.raw_value_spstr)
+ )
+
+ if self.active_kind is ct.FlowKind.Range:
+ ## TODO: Support RangeFlow
+ ## -- It's hard; we need a min-span set over bound domains.
+ ## -- We... Don't use this anywhere. Yet?
+ # sym_start = self._parse_expr_symbol(
+ # self._parse_expr_str(self.raw_min_spstr)
+ # )
+ # sym_stop = self._parse_expr_symbol(
+ # self._parse_expr_str(self.raw_max_spstr)
+ # )
+ msg = 'RangeFlow support not yet implemented for when self.symbols is not empty'
+ raise NotImplementedError(msg)
+
+ raise NotImplementedError
+
+ return sim_symbols.SimSymbol(
+ sym_name=self.output_name,
+ mathtype=self.mathtype,
+ physical_type=self.physical_type,
+ unit=self.unit,
+ rows=self.size.rows,
+ cols=self.size.cols,
+ )
+
####################
# - Symbols
####################
@@ -140,6 +193,11 @@ class ExprBLSocket(base.MaxwellSimSocket):
"""Computes `sympy` symbols from `self.sorted_symbols`."""
return [sym.sp_symbol_matsym for sym in self.sorted_symbols]
+ @bl_cache.cached_bl_property(depends_on={'symbols'})
+ def sorted_symbol_names(self) -> list[sp.Symbol | sp.MatrixSymbol]:
+ """Computes the name of symbols in `self.sorted_symbols`."""
+ return [sym.name for sym in self.sorted_symbols]
+
####################
# - Units
####################
@@ -171,8 +229,13 @@ class ExprBLSocket(base.MaxwellSimSocket):
return None
- @property
+ @bl_cache.cached_bl_property(depends_on={'unit'})
def unit_factor(self) -> spux.Unit | None:
+ """Gets the current active unit as a factor, where unitless is `1`.
+
+ Returns:
+ Same as `self.unit`, except `1` instead of `None` when there is no units.
+ """
return sp.Integer(1) if self.unit is None else self.unit
prev_unit: str | None = bl_cache.BLField(None)
@@ -228,26 +291,92 @@ class ExprBLSocket(base.MaxwellSimSocket):
####################
# - Computed String Expressions
####################
- @bl_cache.cached_bl_property(depends_on={'raw_value_spstr'})
+ @bl_cache.cached_bl_property(
+ depends_on={'raw_value_spstr', 'sorted_symbol_names', 'symbols'}
+ )
def raw_value_sp(self) -> spux.SympyExpr:
+ """Parse the given symbolic `FlowKind.Value` string into a `sympy` expression.
+
+ Notes:
+ The `self.*` properties used by `_parse_expr_str` must be included in the `depends_on` of any `cached_bl_property`s that use it.
+
+ Directly derived from the internal method `self._parse_expr_str()`, which acts on `raw_value_spstr`.
+ """
return self._parse_expr_str(self.raw_value_spstr)
- @bl_cache.cached_bl_property(depends_on={'raw_min_spstr'})
+ @bl_cache.cached_bl_property(
+ depends_on={'raw_min_spstr', 'sorted_symbol_names', 'symbols'}
+ )
def raw_min_sp(self) -> spux.SympyExpr:
+ """Parse the given symbolic `FlowKind.Range` string (for the lower bound) into a `sympy` expression.
+
+ Notes:
+ The `self.*` properties used by `_parse_expr_str` must be included in the `depends_on` of any `cached_bl_property`s that use it.
+
+ Directly derived from the internal method `self._parse_expr_str()`, which acts on `raw_min_spstr`.
+ """
return self._parse_expr_str(self.raw_min_spstr)
- @bl_cache.cached_bl_property(depends_on={'raw_max_spstr'})
+ @bl_cache.cached_bl_property(
+ depends_on={'raw_max_spstr', 'sorted_symbol_names', 'symbols'}
+ )
def raw_max_sp(self) -> spux.SympyExpr:
+ """Parse the given symbolic `FlowKind.Range` string (for the upper bound) into a `sympy` expression.
+
+ Notes:
+ The `self.*` properties used by `_parse_expr_str` must be included in the `depends_on` of any `cached_bl_property`s that use it.
+
+ Directly derived from the internal method `self._parse_expr_str()`, which acts on `raw_max_spstr`.
+ """
return self._parse_expr_str(self.raw_max_spstr)
####################
- # - Prop-Change Callback
+ # - Event Callbacks
####################
- def on_socket_prop_changed(self, prop_name: str) -> None:
+ def on_socket_data_changed(self, socket_kinds: set[ct.FlowKind]) -> None:
+ """Alter the socket's color in response to flow.
+
+ - `FlowKind.Info`: Any change causes the socket color to be updated with the physical type of the output symbol.
+
+ Notes:
+ Overridden method called whenever `FlowEvent.LinkChanged` is generated on this socket, in response to link add/link remove.
+
+ See `MaxwellSimTree` for more detail on the link callbacks.
+ """
+ ## NODE: Depends on suppressed on_prop_changed
+
+ if ct.FlowKind.Info in socket_kinds:
+ info = self.compute_data(kind=ct.FlowKind.Info)
+ has_info = not ct.FlowSignal.check(info)
+
+ # Alter Color
+ pt_color = (
+ info.output.physical_type.color
+ if has_info
+ else self.physical_type.color
+ )
+ if self.socket_color != pt_color:
+ self.socket_color = pt_color
+
+ def on_socket_props_changed(
+ self,
+ cleared_blfields: set[
+ tuple[str, typ.Literal['invalidate', 'reset_enum', 'reset_strsearch']]
+ ],
+ ) -> None:
+ """Alter the socket in response to local property changes.
+
+ Notes:
+ Overridden method called whenever `FlowEvent.LinkChanged` is generated on this socket, in response to link add/link remove.
+
+ See `MaxwellSimTree` for more detail on the link callbacks.
+ """
+ ## NODE: Depends on suppressed on_prop_changed
+
# Conditional Unit-Conversion
## -> This is niche functionality, but the only way to convert units.
## -> We can only catch 'unit' since it's at the end of a depschain.
- if prop_name == 'unit':
+ if ('unit', 'invalidate') in cleared_blfields:
# Check Unit Change
## -> self.prev_unit only updates here; "lags" behind self.unit.
## -> 1. "Laggy" unit must be different than new unit.
@@ -272,37 +401,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
####################
# - Value Utilities
####################
- def _parse_expr_info(
- self, expr: spux.SympyExpr
- ) -> tuple[spux.MathType, tuple[int, ...] | None, spux.UnitDimension]:
- """Parse a given expression for mathtype and size information.
-
- Various compatibility checks are also performed, allowing this method to serve as a generic runtime validator/parser for any expressions that need to enter the socket.
- """
- # Parse MathType
- mathtype = spux.MathType.from_expr(expr)
- if not self.mathtype.is_compatible(mathtype):
- msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with mathtype {mathtype}'
- raise ValueError(msg)
-
- # Parse Symbols
- if expr.free_symbols and not expr.free_symbols.issubset(self.sp_symbols):
- msg = f'Tried to set expr {expr} with free symbols {expr.free_symbols}, which is incompatible with socket symbols {self.symbols}'
- raise ValueError(msg)
-
- # Parse Dimensions
- shape = spux.parse_shape(expr)
- if not self.size.supports_shape(shape):
- msg = f'Expr {expr} has non-1D shape {shape}, which is incompatible with the expr socket (shape {self.shape})'
- raise ValueError(msg)
-
- size = spux.NumberSize1D.from_shape(shape)
- if self.size != size:
- msg = f'Expr {expr} has 1D size {size}, which is incompatible with the expr socket (size {self.size})'
- raise ValueError(msg)
-
- return mathtype, size
-
def _to_raw_value(self, expr: spux.SympyExpr, force_complex: bool = False):
"""Cast the given expression to the appropriate raw value, with scaling guided by `self.unit`."""
if self.unit is not None:
@@ -324,38 +422,117 @@ class ExprBLSocket(base.MaxwellSimSocket):
return pyvalue
+ def _parse_expr_symbol(
+ self, expr: spux.SympyExpr | None
+ ) -> sim_symbols.SimSymbol | None:
+ """Deduce the `SimSymbol` corresponding to the given `expr`, else None."""
+ if expr is not None and (
+ not expr.free_symbols or expr.free_symbols.issubset(self.sp_symbols)
+ ):
+ # Compute Units of Expression
+ ## -> The output units may not be physically meaningful.
+ ## -> However, "weird units" may be a good indicator of problems.
+ ## -> So, we let the user shoot their foot off.
+ unit_expr = expr.subs(
+ {sym.sp_symbol: sym.unit_factor for sym in self.symbols}
+ )
+
+ return sim_symbols.SimSymbol.from_expr(
+ self.output_name, expr, unit_expr, optional=True
+ )
+
+ return None
+
def _parse_expr_str(self, expr_spstr: str) -> spux.SympyExpr | None:
"""Parse an expression string by choosing opinionated options for `sp.sympify`.
- Uses `self._parse_expr_info()` to validate the parsed result.
+ Uses `self._parse_expr_symbol()` to validate the parsed result.
Returns:
The parsed expression, if it manages to validate; else None.
"""
- expr = sp.sympify(
+ expr = sp.parsing.sympy_parser.parse_expr(
expr_spstr,
- locals={sym.name: sym.sp_symbol_matsym for sym in self.symbols},
- strict=False,
- convert_xor=True,
- ).subs(spux.UNIT_BY_SYMBOL)
+ local_dict=(
+ {sym.name: sym.sp_symbol_matsym for sym in self.symbols}
+ | {sym.name: unit for sym, unit in spux.UNIT_BY_SYMBOL.items()}
+ ),
+ transformations=[
+ # Lambda Notation: Symbolic Anonymous Functions
+ ## -> Interpret 'lambda: x/8' to sp.Lambda((), x/0)
+ sp.parsing.sympy_parser.lambda_notation,
+ # Automatic Symbols
+ ## -> Interpret known functions as their sympy equivs.
+ ## -> Interpret unknown 'x' as sp.Symbol('x')
+ ## -> NOTE: Must check for extraneous/unwelcome unknowns.
+ sp.parsing.sympy_parser.auto_symbol,
+ # Repeated Decimals
+ ## -> Interpret '0.2[1]' as 0.211111...
+ sp.parsing.sympy_parser.repeated_decimals,
+ # Number Literals
+ ## -> Interpret ints/float literals.
+ ## -> Interpret 'I' as the imaginary number literal.
+ ## -> TODO: Maybe special-case the variable name 'I'?
+ sp.parsing.sympy_parser.auto_number,
+ # Factorial Notation
+ ## -> Allow 'x!' to be the factorial of x.
+ sp.parsing.sympy_parser.factorial_notation,
+ # Rationalize Float -> Rational
+ ## -> Helps numerical stability for pure-symbolic math.
+ ## -> AFTER auto_number
+ sp.parsing.sympy_parser.rationalize,
+ # Carrot Exponentiation
+ ## -> Interpret '^' as power, instead of as XOR.
+ sp.parsing.sympy_parser.convert_xor,
+ # Symbol Splitting
+ ## -> Interpret 'xyz' as 'x*y*z' for convenience.
+ ## -> NEVER split greek character names (ex. theta).
+ ## -> NEVER split symbol names in 'self.symbols'.
+ sp.parsing.sympy_parser.split_symbols_custom(
+ predicate=lambda sym_name: (
+ sp.parsing.sympy_parser._token_splittable(sym_name) # noqa: SLF001
+ if sym_name not in self.sorted_symbol_names
+ else False
+ )
+ ),
+ # Implicit Mult/Call
+ ## -> Most times, allow '2x' as '2*x' / '2 x y' as '2*x*y'.
+ ## -> Sometimes, allow 'sin 2x' as 'sin(2*x)'
+ ## -> Allow functions to be exponentiated ex. 'sin^2 x'
+ sp.parsing.sympy_parser.implicit_multiplication,
+ sp.parsing.sympy_parser.implicit_application,
+ sp.parsing.sympy_parser.function_exponentiation,
+ ],
+ )
- # Try Parsing and Returning the Expression
- try:
- self._parse_expr_info(expr)
- except ValueError:
- log.exception(
- 'Couldn\'t parse expression "%s" in Expr socket.',
- expr_spstr,
- )
- else:
+ if self._parse_expr_symbol(expr) is not None:
return expr
-
return None
####################
# - FlowKind: Value
####################
- @property
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'symbols',
+ 'unit',
+ 'mathtype',
+ 'size',
+ 'raw_value_sp',
+ 'raw_value_int',
+ 'raw_value_rat',
+ 'raw_value_float',
+ 'raw_value_complex',
+ 'raw_value_int2',
+ 'raw_value_rat2',
+ 'raw_value_float2',
+ 'raw_value_complex2',
+ 'raw_value_int3',
+ 'raw_value_rat3',
+ 'raw_value_float3',
+ 'raw_value_complex3',
+ }
+ )
def value(self) -> spux.SympyExpr:
"""Return the expression defined by the socket as `FlowKind.Value`.
@@ -382,8 +559,8 @@ class ExprBLSocket(base.MaxwellSimSocket):
## -> ExprSocket doesn't support Vec4 (yet?).
## -> I mean, have you _seen_ that mess of attributes up top?
NS = spux.NumberSize1D
- if self.size == NS.Vec4:
- return ct.Flow
+ if self.size is NS.Vec4:
+ return ct.FlowSignal.NoFlow
MT_Z = spux.MathType.Integer
MT_Q = spux.MathType.Rational
@@ -430,7 +607,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
Notes:
Called to set the internal `FlowKind.Value` of this socket.
"""
- _mathtype, _size = self._parse_expr_info(expr)
if self.symbols:
self.raw_value_spstr = sp.sstr(expr)
else:
@@ -473,7 +649,22 @@ class ExprBLSocket(base.MaxwellSimSocket):
####################
# - FlowKind: Range
####################
- @property
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'symbols',
+ 'unit',
+ 'mathtype',
+ 'size',
+ 'steps',
+ 'scaling',
+ 'raw_min_sp',
+ 'raw_max_sp',
+ 'raw_range_int',
+ 'raw_range_rat',
+ 'raw_range_float',
+ 'raw_range_complex',
+ }
+ )
def lazy_range(self) -> ct.RangeFlow:
"""Return the not-yet-computed uniform array defined by the socket.
@@ -519,18 +710,18 @@ class ExprBLSocket(base.MaxwellSimSocket):
)
@lazy_range.setter
- def lazy_range(self, value: ct.RangeFlow) -> None:
+ def lazy_range(self, lazy_range: ct.RangeFlow) -> None:
"""Set the not-yet-computed uniform array defined by the socket.
Notes:
Called to compute the internal `FlowKind.Range` of this socket.
"""
- self.steps = value.steps
- self.scaling = value.scaling
+ self.steps = lazy_range.steps
+ self.scaling = lazy_range.scaling
if self.symbols:
- self.raw_min_spstr = sp.sstr(value.start)
- self.raw_max_spstr = sp.sstr(value.stop)
+ self.raw_min_spstr = sp.sstr(lazy_range.start)
+ self.raw_max_spstr = sp.sstr(lazy_range.stop)
else:
MT_Z = spux.MathType.Integer
@@ -538,32 +729,40 @@ class ExprBLSocket(base.MaxwellSimSocket):
MT_R = spux.MathType.Real
MT_C = spux.MathType.Complex
- unit = value.unit if value.unit is not None else 1
+ unit = lazy_range.unit if lazy_range.unit is not None else 1
if self.mathtype == MT_Z:
self.raw_range_int = [
self._to_raw_value(bound * unit)
- for bound in [value.start, value.stop]
+ for bound in [lazy_range.start, lazy_range.stop]
]
elif self.mathtype == MT_Q:
self.raw_range_rat = [
self._to_raw_value(bound * unit)
- for bound in [value.start, value.stop]
+ for bound in [lazy_range.start, lazy_range.stop]
]
elif self.mathtype == MT_R:
self.raw_range_float = [
self._to_raw_value(bound * unit)
- for bound in [value.start, value.stop]
+ for bound in [lazy_range.start, lazy_range.stop]
]
elif self.mathtype == MT_C:
self.raw_range_complex = [
self._to_raw_value(bound * unit, force_complex=True)
- for bound in [value.start, value.stop]
+ for bound in [lazy_range.start, lazy_range.stop]
]
####################
# - FlowKind: Func (w/Params if Constant)
####################
- @property
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'value',
+ 'symbols',
+ 'sorted_sp_symbols',
+ 'sorted_symbols',
+ 'output_sym',
+ }
+ )
def lazy_func(self) -> ct.FuncFlow:
"""Returns a lazy value that computes the expression returned by `self.value`.
@@ -574,15 +773,21 @@ class ExprBLSocket(base.MaxwellSimSocket):
## -> `self.value` is guaranteed to be an expression with unknowns.
## -> The function computes `self.value` with unknowns as arguments.
if self.symbols:
- return ct.FuncFlow(
- func=sp.lambdify(
- self.sorted_sp_symbols,
- spux.strip_unit_system(self.value),
- 'jax',
- ),
- func_args=list(self.sorted_symbols),
- supports_jax=True,
- )
+ value = self.value
+ has_value = not ct.FlowSignal.check(value)
+
+ output_sym = self.output_sym
+ if output_sym is not None and has_value:
+ return ct.FuncFlow(
+ func=sp.lambdify(
+ self.sorted_sp_symbols,
+ output_sym.conform(value, strip_unit=True),
+ 'jax',
+ ),
+ func_args=list(self.sorted_symbols),
+ supports_jax=True,
+ )
+ return ct.FlowSignal.FlowPending
# Constant
## -> When a `self.value` has no unknowns, use a dummy function.
@@ -591,15 +796,25 @@ class ExprBLSocket(base.MaxwellSimSocket):
## -> Generally only useful for operations with other expressions.
return ct.FuncFlow(
func=lambda v: v,
- func_args=[
- sim_symbols.SimSymbol.from_expr(
- sim_symbols.SimSymbolName.Constant, self.value, self.unit_factor
- )
- ],
+ func_args=[self.output_sym],
supports_jax=True,
)
- @property
+ @bl_cache.cached_bl_property(depends_on={'sorted_symbols'})
+ def is_differentiable(self) -> bool:
+ """Whether all symbols are differentiable.
+
+ If there are no symbols, then there is nothing to differentiate, and thus the expression is differentiable.
+ """
+ if not self.sorted_symbols:
+ return True
+
+ return all(
+ sym.mathtype in [spux.MathType.Real, spux.MathType.Complex]
+ for sym in self.sorted_symbols
+ )
+
+ @bl_cache.cached_bl_property(depends_on={'sorted_symbols', 'output_sym', 'value'})
def params(self) -> ct.ParamsFlow:
"""Returns parameter symbols/values to accompany `self.lazy_func`.
@@ -611,19 +826,28 @@ class ExprBLSocket(base.MaxwellSimSocket):
## -> They should be realized later, ex. in a Viz node.
## -> Therefore, we just dump the symbols. Easy!
## -> NOTE: func_args must have the same symbol order as was lambdified.
- if self.symbols:
- return ct.ParamsFlow(
- func_args=[sym.sp_symbol_phy for sym in self.sorted_symbols],
- symbols=self.sorted_symbols,
- )
+ if self.sorted_symbols:
+ output_sym = self.output_sym
+ if output_sym is not None:
+ return ct.ParamsFlow(
+ arg_targets=list(self.sorted_symbols),
+ func_args=[sym.sp_symbol for sym in self.sorted_symbols],
+ symbols=self.sorted_symbols,
+ is_differentiable=self.is_differentiable,
+ )
+ return ct.FlowSignal.FlowPending
# Constant
## -> Simply pass self.value verbatim as a function argument.
## -> Easy dice, easy life!
- return ct.ParamsFlow(func_args=[self.value])
+ return ct.ParamsFlow(
+ arg_targets=[self.output_sym],
+ func_args=[self.value],
+ is_differentiable=self.is_differentiable,
+ )
- @property
- def info(self) -> ct.ArrayFlow:
+ @bl_cache.cached_bl_property(depends_on={'sorted_symbols', 'output_sym'})
+ def info(self) -> ct.InfoFlow:
r"""Returns parameter symbols/values to accompany `self.lazy_func`.
The output name/size/mathtype/unit corresponds directly the `ExprSocket`.
@@ -634,37 +858,78 @@ class ExprBLSocket(base.MaxwellSimSocket):
Otherwise, only the output name/size/mathtype/unit corresponding to the socket is passed along.
"""
- output_sym = sim_symbols.SimSymbol(
- sym_name=self.output_name,
- mathtype=self.mathtype,
- physical_type=self.physical_type,
- unit=self.unit,
- rows=self.size.rows,
- cols=self.size.cols,
- )
-
# Constant
## -> The input SimSymbols become continuous dimensional indices.
## -> All domain validity information is defined on the SimSymbol keys.
- if self.symbols:
- return ct.InfoFlow(
- dims={sym: None for sym in self.sorted_symbols},
- output=output_sym,
- )
+ if self.sorted_symbols:
+ output_sym = self.output_sym
+ if output_sym is not None:
+ return ct.InfoFlow(
+ dims={sym: None for sym in self.sorted_symbols},
+ output=self.output_sym,
+ )
+ return ct.FlowSignal.FlowPending
# Constant
## -> We only need the output symbol to describe the raw data.
- return ct.InfoFlow(output=output_sym)
+ return ct.InfoFlow(output=self.output_sym)
####################
# - FlowKind: Capabilities
####################
- @property
- def capabilities(self) -> None:
+ def linked_capabilities(self, info: ct.InfoFlow) -> ct.CapabilitiesFlow:
+ """When this socket is linked as an output socket, expose these capabilities instead of querying `self.capabilities`.
+
+ Only used when `use_linked_capabilities == True`.
+ """
return ct.CapabilitiesFlow(
socket_type=self.socket_type,
active_kind=self.active_kind,
- allow_out_to_in={ct.FlowKind.Func: ct.FlowKind.Value},
+ allow_out_to_in={
+ ct.FlowKind.Func: ct.FlowKind.Value,
+ },
+ allow_out_to_in_if_matches={
+ ct.FlowKind.Value: (
+ ct.FlowKind.Func,
+ (
+ info.output.physical_type,
+ info.output.mathtype,
+ info.output.rows,
+ info.output.cols,
+ ),
+ ),
+ },
+ )
+
+ @bl_cache.cached_bl_property(depends_on={'active_kind', 'output_sym'})
+ def capabilities(self) -> ct.CapabilitiesFlow:
+ """Expose capabilities for use when checking socket link compatibility.
+
+ Only used when `use_linked_capabilities == True`.
+ """
+ output_sym = self.output_sym
+ if output_sym is not None:
+ allow_out_to_in_if_matches = {
+ ct.FlowKind.Value: (
+ ct.FlowKind.Func,
+ (
+ output_sym.physical_type,
+ output_sym.mathtype,
+ output_sym.rows,
+ output_sym.cols,
+ ),
+ ),
+ }
+ else:
+ allow_out_to_in_if_matches = {}
+
+ return ct.CapabilitiesFlow(
+ socket_type=self.socket_type,
+ active_kind=self.active_kind,
+ allow_out_to_in={
+ ct.FlowKind.Func: ct.FlowKind.Value,
+ },
+ allow_out_to_in_if_matches=allow_out_to_in_if_matches,
)
####################
@@ -692,29 +957,32 @@ class ExprBLSocket(base.MaxwellSimSocket):
Notes:
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
"""
- info = self.compute_data(kind=ct.FlowKind.Info)
- has_info = not ct.FlowSignal.check(info)
+ if self.active_kind is ct.FlowKind.Func:
+ info = self.compute_data(kind=ct.FlowKind.Info)
+ has_info = not ct.FlowSignal.check(info)
- if has_info:
- split = row.split(factor=0.85, align=True)
- _row = split.row(align=False)
+ if has_info:
+ split = row.split(factor=0.85, align=True)
+ _row = split.row(align=False)
+ else:
+ _row = row
+
+ _row.label(text=text)
+ if has_info:
+ if self.show_info_columns:
+ _row.prop(self, self.blfields['info_columns'])
+
+ _row = split.row(align=True)
+ _row.alignment = 'RIGHT'
+ _row.prop(
+ self,
+ self.blfields['show_info_columns'],
+ toggle=True,
+ text='',
+ icon=ct.Icon.ToggleSocketInfo,
+ )
else:
- _row = row
-
- _row.label(text=text)
- if has_info:
- if self.show_info_columns:
- _row.prop(self, self.blfields['info_columns'])
-
- _row = split.row(align=True)
- _row.alignment = 'RIGHT'
- _row.prop(
- self,
- self.blfields['show_info_columns'],
- toggle=True,
- text='',
- icon=ct.Icon.ToggleSocketInfo,
- )
+ row.label(text=text)
def draw_output_label_row(self, row: bpy.types.UILayout, text) -> None:
"""Provide a dropdown for enabling the `InfoFlow` UI in the linked output label row.
@@ -724,29 +992,32 @@ class ExprBLSocket(base.MaxwellSimSocket):
Notes:
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
"""
- info = self.compute_data(kind=ct.FlowKind.Info)
- has_info = not ct.FlowSignal.check(info)
+ if self.active_kind is ct.FlowKind.Func:
+ info = self.compute_data(kind=ct.FlowKind.Info)
+ has_info = not ct.FlowSignal.check(info)
- if has_info:
- split = row.split(factor=0.15, align=True)
+ if has_info:
+ split = row.split(factor=0.15, align=True)
- _row = split.row(align=True)
- _row.prop(
- self,
- self.blfields['show_info_columns'],
- toggle=True,
- text='',
- icon=ct.Icon.ToggleSocketInfo,
- )
+ _row = split.row(align=True)
+ _row.prop(
+ self,
+ self.blfields['show_info_columns'],
+ toggle=True,
+ text='',
+ icon=ct.Icon.ToggleSocketInfo,
+ )
- _row = split.row(align=False)
- _row.alignment = 'RIGHT'
- if self.show_info_columns:
- _row.prop(self, self.blfields['info_columns'])
+ _row = split.row(align=False)
+ _row.alignment = 'RIGHT'
+ if self.show_info_columns:
+ _row.prop(self, self.blfields['info_columns'])
+ else:
+ _col = _row.column()
+ _col.alignment = 'EXPAND'
+ _col.label(text='')
else:
- _col = _row.column()
- _col.alignment = 'EXPAND'
- _col.label(text='')
+ _row = row
else:
_row = row
@@ -860,42 +1131,38 @@ class ExprBLSocket(base.MaxwellSimSocket):
Uses `draw_value` to draw the base UI
"""
if self.show_func_ui:
- # Non-Symbolic: Size/Mathtype Selector
- ## -> Symbols imply str expr input.
- ## -> For arbitrary str exprs, size/mathtype are derived from the expr.
- ## -> Otherwise, size/mathtype must be pre-specified for a nice UI.
- if not self.symbols:
- row = col.row(align=True)
- row.prop(self, self.blfields['size'], text='')
- row.prop(self, self.blfields['mathtype'], text='')
-
- # Base UI
- ## -> Draws the UI appropriate for the above choice of constraints.
- self.draw_value(col)
-
- # Physical Type Selector
- ## -> Determines whether/which unit-dropdown will be shown.
- col.prop(self, self.blfields['physical_type'], text='')
-
- # Symbol UI
- ## -> Draws the UI appropriate for the above choice of constraints.
- ## -> TODO
-
# Output Name Selector
## -> The name of the output
if self.show_name_selector:
row = col.row()
+ row.alignment = 'CENTER'
row.prop(self, self.blfields['output_name'], text='Name')
+ # Non-Symbolic: Size/Mathtype Selector
+ ## -> Symbols imply str expr input.
+ ## -> For arbitrary str exprs, size/mathtype are derived from the expr.
+ ## -> Otherwise, size/mathtype must be pre-specified for a nice UI.
+ if self.symbols:
+ self.draw_value(col)
+
+ # TODO: Symbol UI
+ else:
+ row = col.row(align=True)
+ row.prop(self, self.blfields['size'], text='')
+ row.prop(self, self.blfields['mathtype'], text='')
+
+ self.draw_value(col)
+ col.prop(self, self.blfields['physical_type'], text='')
+
####################
# - UI: InfoFlow
####################
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
"""Visualize the `InfoFlow` information passing through the socket."""
if (
- self.active_kind == ct.FlowKind.Func
+ self.active_kind is ct.FlowKind.Func
and self.show_info_columns
- and self.is_linked
+ and (self.is_linked or self.is_output)
):
row = col.row()
box = row.box()
@@ -922,7 +1189,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
if InfoDisplayCol.Length in self.info_columns:
grid.label(text='', icon=ct.Icon.DataSocketOutput)
if InfoDisplayCol.MathType in self.info_columns:
- grid.label(text=info.output.def_label)
+ grid.label(text=info.output.mathtype_size_label)
if InfoDisplayCol.Unit in self.info_columns:
grid.label(text=info.output.unit_label)
@@ -935,7 +1202,6 @@ class ExprSocketDef(base.SocketDef):
active_kind: typ.Literal[
ct.FlowKind.Value,
ct.FlowKind.Range,
- ct.FlowKind.Array,
ct.FlowKind.Func,
] = ct.FlowKind.Value
output_name: sim_symbols.SimSymbolName = sim_symbols.SimSymbolName.Expr
@@ -1240,6 +1506,7 @@ class ExprSocketDef(base.SocketDef):
def init(self, bl_socket: ExprBLSocket) -> None:
bl_socket.active_kind = self.active_kind
bl_socket.output_name = self.output_name
+ bl_socket.use_linked_capabilities = True
# Socket Interface
## -> Recall that auto-updates are turned off during init()
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_cond.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_cond.py
index 6b1da12..0c3ae97 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_cond.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_cond.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
import bpy
+import pydantic as pyd
import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
@@ -59,7 +60,9 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
####################
# - FlowKind
####################
- @property
+ @bl_cache.cached_bl_property(
+ depends_on={'active_kind', 'allow_axes', 'present_axes'}
+ )
def capabilities(self) -> ct.CapabilitiesFlow:
return ct.CapabilitiesFlow(
socket_type=self.socket_type,
@@ -68,7 +71,7 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
present_any=self.present_axes,
)
- @property
+ @bl_cache.cached_bl_property(depends_on={'default'})
def value(self) -> td.BoundaryEdge:
return self.default.tidy3d_boundary_edge
@@ -84,16 +87,20 @@ class MaxwellBoundCondSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.MaxwellBoundCond
default: ct.BoundCondType = ct.BoundCondType.Pml
- allow_axes: set[ct.SimSpaceAxis] = {
- ct.SimSpaceAxis.X,
- ct.SimSpaceAxis.Y,
- ct.SimSpaceAxis.Z,
- }
- present_axes: set[ct.SimSpaceAxis] = {
- ct.SimSpaceAxis.X,
- ct.SimSpaceAxis.Y,
- ct.SimSpaceAxis.Z,
- }
+ allow_axes: set[ct.SimSpaceAxis] = pyd.Field(
+ default={
+ ct.SimSpaceAxis.X,
+ ct.SimSpaceAxis.Y,
+ ct.SimSpaceAxis.Z,
+ }
+ )
+ present_axes: set[ct.SimSpaceAxis] = pyd.Field(
+ default={
+ ct.SimSpaceAxis.X,
+ ct.SimSpaceAxis.Y,
+ ct.SimSpaceAxis.Z,
+ }
+ )
def init(self, bl_socket: MaxwellBoundCondBLSocket) -> None:
bl_socket.default = self.default
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_conds.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_conds.py
index 48b1dc9..ea69736 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_conds.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/bound_conds.py
@@ -86,7 +86,9 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
####################
# - Computation of Default Value
####################
- @property
+ @bl_cache.cached_bl_property(
+ depends_on={'x_pos', 'x_neg', 'y_pos', 'y_neg', 'z_pos', 'z_neg'}
+ )
def value(self) -> td.BoundarySpec:
"""Compute a user-defined default value for simulation boundary conditions, from certain common/sensible options.
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py
index 55f707e..4b92a24 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/medium.py
@@ -18,6 +18,7 @@ import bpy
import scipy as sc
import sympy.physics.units as spu
import tidy3d as td
+import tidy3d.plugins.adjoint as tdadj
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux
@@ -39,12 +40,14 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- rel_permittivity: tuple[float, float] = bl_cache.BLField((1.0, 0.0), float_prec=2)
+ eps_rel: tuple[float, float] = bl_cache.BLField((1.0, 0.0), float_prec=2)
+
+ differentiable: bool = bl_cache.BLField(False)
####################
# - FlowKinds
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'eps_rel', 'differentiable'})
def value(self) -> td.Medium:
freq = (
spu.convert_to(
@@ -53,31 +56,49 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
)
/ spu.hertz
)
+
+ if self.differentiable:
+ return tdadj.JaxMedium.from_nk(
+ n=self.eps_rel[0],
+ k=self.eps_rel[1],
+ freq=freq,
+ )
return td.Medium.from_nk(
- n=self.rel_permittivity[0],
- k=self.rel_permittivity[1],
+ n=self.eps_rel[0],
+ k=self.eps_rel[1],
freq=freq,
)
@value.setter
- def value(
- self, value: tuple[spux.ConstrSympyExpr(allow_variables=False), complex]
- ) -> None:
- rel_permittivity = value
+ def value(self, eps_rel: tuple[float, float]) -> None:
+ self.eps_rel = eps_rel
- self.rel_permittivity = (rel_permittivity.real, rel_permittivity.imag)
+ @bl_cache.cached_bl_property(depends_on={'value', 'differentiable'})
+ def lazy_func(self) -> ct.FuncFlow:
+ return ct.FuncFlow(
+ func=lambda: self.value,
+ supports_jax=self.differentiable,
+ )
+
+ @bl_cache.cached_bl_property(depends_on={'differentiable'})
+ def params(self) -> ct.FuncFlow:
+ return ct.ParamsFlow(is_differentiable=self.differentiable)
####################
# - UI
####################
def draw_value(self, col: bpy.types.UILayout) -> None:
- split = col.split(factor=0.35, align=False)
+ col.prop(
+ self, self.blfields['differentiable'], text='Differentiable', toggle=True
+ )
+ col.separator()
+ split = col.split(factor=0.25, align=False)
- col = split.column(align=True)
- col.label(text='Ļµ_r (ā)')
+ _col = split.column(align=True)
+ _col.label(text='Īµįµ£')
- col = split.column(align=True)
- col.prop(self, self.blfields['rel_permittivity'], text='')
+ _col = split.column(align=True)
+ _col.prop(self, self.blfields['eps_rel'], text='')
####################
@@ -90,7 +111,7 @@ class MaxwellMediumSocketDef(base.SocketDef):
default_permittivity_imag: float = 0.0
def init(self, bl_socket: MaxwellMediumBLSocket) -> None:
- bl_socket.rel_permittivity = (
+ bl_socket.eps_rel = (
self.default_permittivity_real,
self.default_permittivity_imag,
)
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid.py
index 3267fa6..80e4635 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/maxwell/sim_grid.py
@@ -49,7 +49,7 @@ class MaxwellSimGridBLSocket(base.MaxwellSimSocket):
####################
# - Computation of Default Value
####################
- @property
+ @bl_cache.cached_bl_property(depends_on={'min_steps_per_wl'})
def value(self) -> td.GridSpec:
return td.GridSpec.auto(
min_steps_per_wvl=self.min_steps_per_wl,
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py
index ad6d08b..28fe9e8 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py
@@ -50,7 +50,9 @@ class ReloadFolderList(bpy.types.Operator):
tdcloud.TidyCloudTasks.update_tasks(bl_socket.existing_folder_id)
bl_socket.existing_folder_id = bl_cache.Signal.ResetEnumItems
+ bl_socket.existing_folder_id = bl_cache.Signal.InvalidateCache
bl_socket.existing_task_id = bl_cache.Signal.ResetEnumItems
+ bl_socket.existing_task_id = bl_cache.Signal.InvalidateCache
return {'FINISHED'}
@@ -77,7 +79,9 @@ class Authenticate(bpy.types.Operator):
bl_socket.api_key = ''
bl_socket.existing_folder_id = bl_cache.Signal.ResetEnumItems
+ bl_socket.existing_folder_id = bl_cache.Signal.InvalidateCache
bl_socket.existing_task_id = bl_cache.Signal.ResetEnumItems
+ bl_socket.existing_task_id = bl_cache.Signal.InvalidateCache
return {'FINISHED'}
@@ -102,62 +106,18 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
- api_key: str = bl_cache.BLField('', prop_ui=True, str_secret=True)
+ api_key: str = bl_cache.BLField('', str_secret=True)
should_exist: bool = bl_cache.BLField(False)
+ new_task_name: str = bl_cache.BLField('')
+
+ ####################
+ # - Properties: Cloud Folders
+ ####################
existing_folder_id: enum.StrEnum = bl_cache.BLField(
- prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
- )
- existing_task_id: enum.StrEnum = bl_cache.BLField(
- prop_ui=True, enum_cb=lambda self, _: self.search_cloud_tasks()
+ enum_cb=lambda self, _: self.search_cloud_folders()
)
- new_task_name: str = bl_cache.BLField('', prop_ui=True)
-
- ####################
- # - FlowKinds
- ####################
- @property
- def capabilities(self) -> ct.CapabilitiesFlow:
- return ct.CapabilitiesFlow(
- socket_type=self.socket_type,
- active_kind=self.active_kind,
- must_match={'should_exist': self.should_exist},
- )
-
- @property
- def value(
- self,
- ) -> ct.NewSimCloudTask | tdcloud.CloudTask | ct.FlowSignal:
- if tdcloud.IS_AUTHENTICATED:
- # Retrieve Folder
- cloud_folder = tdcloud.TidyCloudFolders.folders().get(
- self.existing_folder_id
- )
- if cloud_folder is None:
- return ct.FlowSignal.NoFlow ## Folder deleted somewhere else
-
- # Case: New Task
- if not self.should_exist:
- return ct.NewSimCloudTask(
- task_name=self.new_task_name, cloud_folder=cloud_folder
- )
-
- # Case: Existing Task
- if self.existing_task_id is not None:
- cloud_task = tdcloud.TidyCloudTasks.tasks(cloud_folder).get(
- self.existing_task_id
- )
- if cloud_folder is None:
- return ct.FlowSignal.NoFlow ## Task deleted somewhere else
-
- return cloud_task
-
- return ct.FlowSignal.FlowPending
-
- ####################
- # - Searchers
- ####################
def search_cloud_folders(self) -> list[ct.BLEnumElement]:
if tdcloud.IS_AUTHENTICATED:
return [
@@ -175,6 +135,13 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return []
+ ####################
+ # - Properties: Cloud Tasks
+ ####################
+ existing_task_id: enum.StrEnum = bl_cache.BLField(
+ enum_cb=lambda self, _: self.search_cloud_tasks()
+ )
+
def search_cloud_tasks(self) -> list[ct.BLEnumElement]:
if self.existing_folder_id is None or not tdcloud.IS_AUTHENTICATED:
return []
@@ -228,6 +195,54 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
)
]
+ ####################
+ # - FlowKinds
+ ####################
+ @bl_cache.cached_bl_property(depends_on={'active_kind', 'should_exist'})
+ def capabilities(self) -> ct.CapabilitiesFlow:
+ return ct.CapabilitiesFlow(
+ socket_type=self.socket_type,
+ active_kind=self.active_kind,
+ must_match={'should_exist': self.should_exist},
+ )
+
+ @bl_cache.cached_bl_property(
+ depends_on={
+ 'should_exist',
+ 'new_task_name',
+ 'existing_folder_id',
+ 'existing_task_id',
+ }
+ )
+ def value(
+ self,
+ ) -> ct.NewSimCloudTask | tdcloud.CloudTask | ct.FlowSignal:
+ if tdcloud.IS_AUTHENTICATED:
+ # Retrieve Folder
+ cloud_folder = tdcloud.TidyCloudFolders.folders().get(
+ self.existing_folder_id
+ )
+ if cloud_folder is None:
+ return ct.FlowSignal.NoFlow ## Folder deleted somewhere else
+
+ # Case: New Task
+ if not self.should_exist:
+ return ct.NewSimCloudTask(
+ task_name=self.new_task_name, cloud_folder=cloud_folder
+ )
+
+ # Case: Existing Task
+ if self.existing_task_id is not None:
+ cloud_task = tdcloud.TidyCloudTasks.tasks(cloud_folder).get(
+ self.existing_task_id
+ )
+ if cloud_folder is None:
+ return ct.FlowSignal.NoFlow ## Task deleted somewhere else
+
+ return cloud_task
+
+ return ct.FlowSignal.FlowPending
+
####################
# - UI
####################
diff --git a/src/blender_maxwell/utils/bl_cache/bl_field.py b/src/blender_maxwell/utils/bl_cache/bl_field.py
index 00b8140..110a985 100644
--- a/src/blender_maxwell/utils/bl_cache/bl_field.py
+++ b/src/blender_maxwell/utils/bl_cache/bl_field.py
@@ -16,6 +16,7 @@
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
+import contextlib
import functools
import inspect
import typing as typ
@@ -166,7 +167,7 @@ class BLField:
self.cb_depends_on: set[str] | None = cb_depends_on
# Update Suppressing
- self.suppress_update: dict[str, bool] = {}
+ self.suppressed_update: dict[str, bool] = {}
####################
# - Descriptor Setup
@@ -253,9 +254,38 @@ class BLField:
return self.bl_prop.default_value ## TODO: Good idea?
return cached_value
- def suppress_next_update(self, bl_instance) -> None:
- self.suppress_update[bl_instance.instance_id] = True
- ## TODO: Make it a context manager to prevent the worst of surprises
+ @contextlib.contextmanager
+ def suppress_update(self, bl_instance: bl_instance.BLInstance) -> None:
+ """A context manager that suppresses all calls to `on_prop_changed()` for fields of the given `bl_instance` while active.
+
+ Any change to a `BLProp` managed by this descriptor inevitably trips `bl_instance.on_bl_prop_changed()`.
+ In response to these changes, `bl_instance.on_bl_prop_changed()` always signals the `Signal.InvalidateCache` via this descriptor.
+ Unless something interferes, this results in a call to `bl_instance.on_prop_changed()`.
+
+ Usually, this is great.
+ But sometimes, like when ex. refreshing enum items, we **want** to be able to set the value of the `BLProp` **without** triggering that `bl_instance.on_prop_changed()`.
+ By default, there is absolutely no way to accomplish this.
+
+ That's where this context manager comes into play.
+ While active, all calls to `bl_instance.on_prop_changed()` will be ignored for the given `bl_instance`, allowing us to freely set persistent properties without side effects.
+
+ Examples:
+ A simple illustrative example could look something like:
+
+ ```python
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, 'I won't trigger an update')
+
+ self.bl_prop.write(bl_instance, 'I will trigger an update')
+ ```
+ """
+ self.suppressed_update[bl_instance.instance_id] = True
+ try:
+ yield
+ finally:
+ self.suppressed_update[bl_instance.instance_id] = False
+ ## -> We could .pop(None).
+ ## -> But keeping a reused memory location around is GC friendly.
def __set__(
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
@@ -263,7 +293,7 @@ class BLField:
"""Sets the value described by the BLField.
In general, any BLField modified in the UI will set `InvalidateCache` on this descriptor.
- If `self.prop_info['use_prop_update']` is set, the method `bl_instance.on_prop_changed(self.bl_prop.name)` will then be called and start a `FlowKind.DataChanged` event chain.
+ If `self.prop_info['use_prop_update']` is set, the method `bl_instance.on_prop_changed(self.bl_prop.name)` will then be called and start a `FlowEvent.DataChanged` event chain.
Notes:
Run by Python when the attribute described by the descriptor is set.
@@ -273,28 +303,29 @@ class BLField:
bl_instance: Instance that is accessing the attribute.
owner: The class that owns the instance.
"""
- # Perform Update Chain
- ## -> We still respect 'use_prop_update', since it is user-sourced.
- if value is Signal.DoUpdate:
- if self.prop_info['use_prop_update']:
- bl_instance.on_prop_changed(self.bl_prop.name)
-
# Invalidate Cache
## -> This empties the non-persistent cache.
## -> As a result, the value must be reloaded from the property.
## The 'on_prop_changed' method on the bl_instance might also be called.
- elif value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
+ if value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
self.bl_prop.invalidate_nonpersist(bl_instance)
- # Update Suppression
- if self.suppress_update.get(bl_instance.instance_id):
- self.suppress_update[bl_instance.instance_id] = False
-
- # ELSE: Trigger Update Chain
- elif self.prop_info['use_prop_update'] and value is Signal.InvalidateCache:
+ # Trigger Update Chain
+ ## -> User can disable w/'use_prop_update=False'.
+ ## -> Use InvalidateCacheNoUpdate to explicitly disable update.
+ ## -> If 'suppressed_update' context manager is active, don't update.
+ if (
+ self.prop_info['use_prop_update']
+ and value is Signal.InvalidateCache
+ and not self.suppressed_update.get(bl_instance.instance_id, False)
+ ):
bl_instance.on_prop_changed(self.bl_prop.name)
# Reset Enum Items
+ ## -> If there is no enum items callback, do nothing.
+ ## -> Re-run the enum items callback and set it active.
+ ## -> If the old item can be retained, then do so.
+ ## -> Otherwise, set the first item.
elif value is Signal.ResetEnumItems:
if self.bl_prop_enum_items is None:
return
@@ -335,8 +366,8 @@ class BLField:
# Swap Enum Items
## -> This is the hot stuff - the enum elements are overwritten.
## -> The safe_enum_cb will pick up on this immediately.
- self.suppress_next_update(bl_instance)
- self.bl_prop_enum_items.write(bl_instance, current_items)
+ with self.suppress_update(bl_instance):
+ self.bl_prop_enum_items.write(bl_instance, current_items)
# Old Item in Current Items
## -> It's possible that the old enum key is in the new enum.
@@ -344,9 +375,8 @@ class BLField:
## -> Thus, we set it - Blender sees a change, user doesn't.
## -> DO NOT trigger on_prop_changed (since "nothing changed").
if any(raw_old_item == item[0] for item in current_items):
- self.suppress_next_update(bl_instance)
- self.bl_prop.write(bl_instance, old_item)
- ## -> TODO: Don't write if not needed.
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, old_item)
# Old Item Not in Current Items
## -> In this case, fallback to the first current item.
@@ -355,28 +385,27 @@ class BLField:
raw_first_current_item = current_items[0][0]
first_current_item = self.bl_prop.decode(raw_first_current_item)
- self.suppress_next_update(bl_instance)
- self.bl_prop.write(bl_instance, first_current_item)
-
- if self.prop_info['use_prop_update']:
- bl_instance.on_prop_changed(self.bl_prop.name)
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, first_current_item)
# Reset Str Search
+ ## -> If there is no string search method, do nothing.
+ ## -> Simply invalidate the non-persistent cache
elif value is Signal.ResetStrSearch:
if self.bl_prop_str_search is None:
return
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
- # General __set__
+ # Default __set__
else:
- self.bl_prop.write(bl_instance, value)
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, value)
# Update Semantics
- if self.suppress_update.get(bl_instance.instance_id):
- self.suppress_update[bl_instance.instance_id] = False
-
- elif self.prop_info['use_prop_update']:
+ if self.prop_info['use_prop_update'] and not self.suppressed_update.get(
+ bl_instance.instance_id, False
+ ):
bl_instance.on_prop_changed(self.bl_prop.name)
####################
diff --git a/src/blender_maxwell/utils/bl_cache/cached_bl_property.py b/src/blender_maxwell/utils/bl_cache/cached_bl_property.py
index cbeac2e..aec3596 100644
--- a/src/blender_maxwell/utils/bl_cache/cached_bl_property.py
+++ b/src/blender_maxwell/utils/bl_cache/cached_bl_property.py
@@ -16,6 +16,7 @@
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
+import contextlib
import inspect
import typing as typ
@@ -76,7 +77,7 @@ class CachedBLProperty:
self.decode_type: type = inspect.signature(getter_method).return_annotation
# Write Suppressing
- self.suppress_write: dict[str, bool] = {}
+ self.suppressed_update: dict[str, bool] = {}
# Check Non-Empty Type Annotation
## For now, just presume that all types can be encoded/decoded.
@@ -125,9 +126,38 @@ class CachedBLProperty:
return Signal.CacheNotReady
return cached_value
- def suppress_next_write(self, bl_instance) -> None:
- self.suppress_write[bl_instance.instance_id] = True
- ## TODO: Make it a context manager to prevent the worst of surprises
+ @contextlib.contextmanager
+ def suppress_update(self, bl_instance: bl_instance.BLInstance) -> None:
+ """A context manager that suppresses all calls to `on_prop_changed()` for fields of the given `bl_instance` while active.
+
+ Any change to a `BLProp` managed by this descriptor inevitably trips `bl_instance.on_bl_prop_changed()`.
+ In response to these changes, `bl_instance.on_bl_prop_changed()` always signals the `Signal.InvalidateCache` via this descriptor.
+ Unless something interferes, this results in a call to `bl_instance.on_prop_changed()`.
+
+ Usually, this is great.
+ But sometimes, like when ex. refreshing enum items, we **want** to be able to set the value of the `BLProp` **without** triggering that `bl_instance.on_prop_changed()`.
+ By default, there is absolutely no way to accomplish this.
+
+ That's where this context manager comes into play.
+ While active, all calls to `bl_instance.on_prop_changed()` will be ignored for the given `bl_instance`, allowing us to freely set persistent properties without side effects.
+
+ Examples:
+ A simple illustrative example could look something like:
+
+ ```python
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, 'I won't trigger an update')
+
+ self.bl_prop.write(bl_instance, 'I will trigger an update')
+ ```
+ """
+ self.suppressed_update[bl_instance.instance_id] = True
+ try:
+ yield
+ finally:
+ self.suppressed_update[bl_instance.instance_id] = False
+ ## -> We could .pop(None).
+ ## -> But keeping a reused memory location around is GC friendly.
def __set__(
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
@@ -141,44 +171,59 @@ class CachedBLProperty:
Parameters:
bl_instance: The Blender object this prop
"""
- if value is Signal.DoUpdate:
- bl_instance.on_prop_changed(self.bl_prop.name)
-
- elif value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
+ # Invalidate Cache
+ ## -> This empties the non-persistent cache.
+ ## -> If persist=True, this also writes the persistent cache (no update).
+ ## The 'on_prop_changed' method on the bl_instance might also be called.
+ if value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
# Invalidate Partner Non-Persistent Caches
## -> Only for the invalidation case do we also invalidate partners.
if bl_instance is not None:
# Fill Caches
- ## -> persist: Fill Persist and Non-Persist Cache
- ## -> else: Fill Non-Persist Cache
- if self.persist and not self.suppress_write.get(
- bl_instance.instance_id
- ):
- self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
+ ## -> persist=True: Fill Persist and Non-Persist Cache
+ ## -> persist=False: Fill Non-Persist Cache
+ if self.persist:
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
else:
self.bl_prop.write_nonpersist(
bl_instance, self.getter_method(bl_instance)
)
- if value == Signal.InvalidateCache:
+ # Trigger Update
+ ## -> Use InvalidateCacheNoUpdate to explicitly disable update.
+ ## -> If 'suppress_update' context manager is active, don't update.
+ if value is Signal.InvalidateCache and not self.suppressed_update.get(
+ bl_instance.instance_id
+ ):
bl_instance.on_prop_changed(self.bl_prop.name)
+ # Call Setter
elif self.setter_method is not None:
- # Run Setter
- ## -> The user-provided setter should do any updating of partners.
- if self.setter_method is not None:
- self.setter_method(bl_instance, value)
+ if bl_instance is not None:
+ # Run Setter
+ ## -> The user-provided setter can set values as it sees fit.
+ ## -> The user-provided setter will not immediately trigger updates.
+ with self.suppress_update(bl_instance):
+ self.setter_method(bl_instance, value)
- # Fill Non-Persistant (and maybe Persistent) Cache
- if self.persist and not self.suppress_write.get(bl_instance.instance_id):
- self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
+ # Fill Caches
+ ## -> persist=True: Fill Persist and Non-Persist Cache
+ ## -> persist=False: Fill Non-Persist Cache
+ if self.persist:
+ with self.suppress_update(bl_instance):
+ self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
- else:
- self.bl_prop.write_nonpersist(
- bl_instance, self.getter_method(bl_instance)
- )
- bl_instance.on_prop_changed(self.bl_prop.name)
+ else:
+ self.bl_prop.write_nonpersist(
+ bl_instance, self.getter_method(bl_instance)
+ )
+
+ # Trigger Update
+ ## -> If 'suppress_update' context manager is active, don't update.
+ if not self.suppressed_update.get(bl_instance.instance_id):
+ bl_instance.on_prop_changed(self.bl_prop.name)
else:
msg = f'Tried to set "{value}" to "{self.prop_name}" on "{bl_instance.bl_label}", but a setter was not defined'
diff --git a/src/blender_maxwell/utils/bl_cache/signal.py b/src/blender_maxwell/utils/bl_cache/signal.py
index 08c337d..a1ebda3 100644
--- a/src/blender_maxwell/utils/bl_cache/signal.py
+++ b/src/blender_maxwell/utils/bl_cache/signal.py
@@ -39,8 +39,6 @@ class Signal(enum.StrEnum):
InvalidateCache: The cache should be invalidated.
InvalidateCacheNoUpdate: The cache should be invalidated, but no update method should be run.
- DoUpdate: Any update method that the cache triggers on change should be run.
- An update is **not guaranteeed** to be run, merely requested.
ResetEnumItems: Cached dynamic enum items should be recomputed on next use.
ResetStrSearch: Cached string-search items should be recomputed on next use.
@@ -53,7 +51,6 @@ class Signal(enum.StrEnum):
# Invalidation
InvalidateCache: str = str(uuid.uuid4())
InvalidateCacheNoUpdate: str = str(uuid.uuid4())
- DoUpdate: str = str(uuid.uuid4())
# Reset Signals
## -> Invalidates data adjascent to fields.
diff --git a/src/blender_maxwell/utils/bl_instance.py b/src/blender_maxwell/utils/bl_instance.py
index cf11c6d..f17c12a 100644
--- a/src/blender_maxwell/utils/bl_instance.py
+++ b/src/blender_maxwell/utils/bl_instance.py
@@ -220,7 +220,13 @@ class BLInstance:
for str_search_prop_name in self.blfields_str_search:
setattr(self, str_search_prop_name, bl_cache.Signal.ResetStrSearch)
- def invalidate_blfield_deps(self, prop_name: str) -> None:
+ def trace_blfields_to_clear(
+ self,
+ prop_name: str,
+ prev_blfields_to_clear: list[
+ tuple[str, typ.Literal['invalidate', 'reset_enum', 'reset_strsearch']]
+ ] = (),
+ ) -> list[str]:
"""Invalidates all properties that depend on `prop_name`.
A property can recursively depend on other properties, including specificity as to whether the cache should be invalidated, the enum items be recomputed, or the string search items be recomputed.
@@ -232,35 +238,110 @@ class BLInstance:
The dictionaries governing exactly what invalidates what, and how, are encoded as `self.blfield_deps`, `self.blfield_dynamic_enum_deps`, and `self.blfield_str_search_deps`.
All of these are filled when creating the `BLInstance` subclass, using `self.declare_blfield_dep()`, generally via the `BLField` descriptor (which internally uses `BLProp`).
"""
+ if prev_blfields_to_clear:
+ blfields_to_clear = prev_blfields_to_clear.copy()
+ else:
+ blfields_to_clear = []
+
# Invalidate Dependent Properties (incl. DynEnums and StrSearch)
- ## -> NOTE: Dependent props may also trigger `on_prop_changed`.
- ## -> Don't abuse dependencies :)
- for deps, invalidate_signal in zip(
+ ## -> InvalidateCacheNoUpdate: Exactly what it sounds like.
+ ## -> ResetEnumItems: Won't trigger on_prop_changed.
+ ## -> -- To get on_prop_changed after, do explicit 'InvalidateCache'.
+ ## -> StrSearch: It's a straight computation, no on_prop_changed.
+ for deps, clear_method in zip(
[
self.blfield_deps,
self.blfield_dynamic_enum_deps,
self.blfield_str_search_deps,
],
- [
- bl_cache.Signal.InvalidateCache,
- bl_cache.Signal.ResetEnumItems,
- bl_cache.Signal.ResetStrSearch,
- ],
+ ['invalidate', 'reset_enum', 'reset_strsearch'],
strict=True,
):
if prop_name in deps:
for dst_prop_name in deps[prop_name]:
- log.debug(
- '%s: "%s" is invalidating "%s"',
- self.bl_label,
- prop_name,
- dst_prop_name,
- )
- setattr(
- self,
- dst_prop_name,
- invalidate_signal,
- )
+ # Mark Dependency for Clearance
+ ## -> Duplicates are OK for now, we'll clear them later.
+ blfields_to_clear.append((dst_prop_name, clear_method))
+
+ # Compute Recursive Dependencies for Clearance
+ ## -> As we go deeper, 'previous fields' is set.
+ if dst_prop_name in self.blfields:
+ blfields_to_clear += self.trace_blfields_to_clear(
+ dst_prop_name,
+ prev_blfields_to_clear=blfields_to_clear,
+ )
+
+ match (bool(prev_blfields_to_clear), bool(blfields_to_clear)):
+ # Nothing to Clear
+ ## -> This is a recursive base case for no-dependency BLFields.
+ case (False, False):
+ return []
+
+ # Only Old: Return Old
+ ## -> This is a recursive base case for the deepest field w/o deps.
+ ## -> When there are previous BLFields, this cannot be recursive root
+ ## -> Otherwise, we'd need to de-duplicate.
+ case (True, False):
+ return prev_blfields_to_clear ## Is never recursive root
+
+ # Only New: Deduplicate (from right) w/Order Preservation
+ ## -> This is the recursive root.
+ ## -> The first time there are new BLFields to clear, we dedupe.
+ ## -> This is the ONLY case where we need to dedupe.
+ ## -> Deduping deeper would be extraneous (though not damaging).
+ case (False, True):
+ return list(reversed(dict.fromkeys(reversed(blfields_to_clear))))
+
+ # New And Old: Concatenate
+ ## -> This is merely a "transport" step, sandwiched btwn base/root.
+ ## -> As such, deduplication would not be wrong, just extraneous.
+ ## -> Since invalidation is in a hot-loop, don't do such things.
+ case (True, True):
+ return blfields_to_clear
+
+ def clear_blfields_after(self, prop_name: str) -> list[str]:
+ """Clear (invalidate) all `BLField`s that have become invalid as a result of a change to `prop_name`.
+
+ Uses `self.trace_blfields_to_clear()` to deduce the names and unique ordering of `BLField`s to clear.
+ Then, update-less `bl_cache.Signal`s are written in order to invalidate each `BLField` cache without invoking `self.on_prop_changed()`.
+ Finally, the list of cleared `BLField`s is returned.
+
+ Notes:
+ Generally, this should be called from `on_prop_changed()`.
+ The resulting cleared fields can then be analyzed / used in a domain specific way as needed by the particular `BLInstance`.
+
+ Returns:
+ The topologically ordered right-de-duplicated list of BLFields that were cleared.
+ """
+ blfields_to_clear = self.trace_blfields_to_clear(prop_name)
+
+ # Invalidate BLFields
+ ## -> trace_blfields_to_clear only gave us what/how to invalidate.
+ ## -> It's the responsibility of on_prop_changed to actually do so.
+ # log.debug(
+ # '%s (NodeSocket): Clearing BLFields after "%s": "%s"',
+ # self.bl_label,
+ # prop_name,
+ # blfields_to_clear,
+ # )
+ for blfield, clear_method in blfields_to_clear:
+ # log.debug(
+ # '%s (NodeSocket): Clearing BLField: %s (%s)',
+ # self.bl_label,
+ # blfield,
+ # clear_method,
+ # )
+ setattr(
+ self,
+ blfield,
+ {
+ 'invalidate': bl_cache.Signal.InvalidateCacheNoUpdate,
+ 'reset_enum': bl_cache.Signal.ResetEnumItems, ## No updates
+ 'reset_strsearch': bl_cache.Signal.ResetStrSearch,
+ }[clear_method],
+ )
+
+ return [(prop_name, 'invalidate'), *blfields_to_clear]
def on_bl_prop_changed(self, bl_prop_name: str, _: bpy.types.Context) -> None:
"""Called when a property has been updated via the Blender UI.
diff --git a/src/blender_maxwell/utils/extra_sympy_units.py b/src/blender_maxwell/utils/extra_sympy_units.py
index b35f629..5991eea 100644
--- a/src/blender_maxwell/utils/extra_sympy_units.py
+++ b/src/blender_maxwell/utils/extra_sympy_units.py
@@ -44,6 +44,7 @@ from pydantic_core import core_schema as pyd_core_schema
from blender_maxwell import contracts as ct
from . import logger
+from .staticproperty import staticproperty
log = logger.get(__name__)
@@ -69,7 +70,7 @@ class MathType(enum.StrEnum):
Complex = enum.auto()
@staticmethod
- def combine(*mathtypes: list[typ.Self]) -> typ.Self:
+ def combine(*mathtypes: list[typ.Self], optional: bool = False) -> typ.Self | None:
if MathType.Complex in mathtypes:
return MathType.Complex
if MathType.Real in mathtypes:
@@ -79,6 +80,9 @@ class MathType(enum.StrEnum):
if MathType.Integer in mathtypes:
return MathType.Integer
+ if optional:
+ return None
+
msg = f"Can't combine mathtypes {mathtypes}"
raise ValueError(msg)
@@ -113,7 +117,7 @@ class MathType(enum.StrEnum):
return complex(pyobj, 0)
@staticmethod
- def from_expr(sp_obj: SympyType) -> type:
+ def from_expr(sp_obj: SympyType, optional: bool = False) -> type | None:
if isinstance(sp_obj, sp.MatrixBase):
return MathType.combine(
*[MathType.from_expr(v) for v in sp.flatten(sp_obj)]
@@ -134,6 +138,9 @@ class MathType(enum.StrEnum):
if sp_obj in [sp.zoo, -sp.zoo]:
return MathType.Complex
+ if optional:
+ return None
+
msg = f"Can't determine MathType from sympy object: {sp_obj}"
raise ValueError(msg)
@@ -957,6 +964,48 @@ def unit_str_to_unit(unit_str: str) -> Unit | None:
####################
# - "Physical" Type
####################
+def unit_dim_to_unit_dim_deps(
+ unit_dims: SympyType,
+) -> dict[spu.dimensions.Dimension, int] | None:
+ dimsys_SI = spu.systems.si.dimsys_SI
+
+ # Retrieve Dimensional Dependencies
+ try:
+ return dimsys_SI.get_dimensional_dependencies(unit_dims)
+
+ # Catch TypeError
+ ## -> Happens if `+` or `-` is in `unit`.
+ ## -> Generally, it doesn't make sense to add/subtract differing unit dims.
+ ## -> Thus, when trying to figure out the unit dimension, there isn't one.
+ except TypeError:
+ return None
+
+
+def unit_to_unit_dim_deps(
+ unit: SympyType,
+) -> dict[spu.dimensions.Dimension, int] | None:
+ # Retrieve Dimensional Dependencies
+ ## -> NOTE: .subs() alone seems to produce sp.Symbol atoms.
+ ## -> This is extremely problematic; `Dims` arithmetic has key properties.
+ ## -> So we have to go all the way to the dimensional dependencies.
+ ## -> This isn't really respecting the args, but it seems to work :)
+ return unit_dim_to_unit_dim_deps(
+ unit.subs({arg: arg.dimension for arg in unit.atoms(spu.Quantity)})
+ )
+
+
+def compare_unit_dims(unit_dim_l: SympyType, unit_dim_r: SympyType) -> bool:
+ return unit_dim_to_unit_dim_deps(unit_dim_l) == unit_dim_to_unit_dim_deps(
+ unit_dim_r
+ )
+
+
+def compare_unit_dim_to_unit_dim_deps(
+ unit_dim: SympyType, unit_dim_deps: dict[spu.dimensions.Dimension, int]
+) -> bool:
+ return unit_dim_to_unit_dim_deps(unit_dim) == unit_dim_deps
+
+
class PhysicalType(enum.StrEnum):
"""Type identifiers for expressions with both `MathType` and a unit, aka a "physical" type."""
@@ -1005,7 +1054,7 @@ class PhysicalType(enum.StrEnum):
Illuminance = enum.auto()
@functools.cached_property
- def unit_dim(self):
+ def unit_dim(self) -> SympyType:
PT = PhysicalType
return {
PT.NonPhysical: None,
@@ -1050,6 +1099,95 @@ class PhysicalType(enum.StrEnum):
PT.Illuminance: Dims.luminous_intensity / Dims.length**2,
}[self]
+ @staticproperty
+ def unit_dims() -> dict[typ.Self, SympyType]:
+ return {
+ physical_type: physical_type.unit_dim
+ for physical_type in list(PhysicalType)
+ }
+
+ @functools.cached_property
+ def color(self):
+ """A color corresponding to the physical type.
+
+ The color selections were initially generated using AI, as this is a rote task that's better adjusted than invented.
+ The LLM provided the following rationale for its choices:
+
+ > Non-Physical: Grey signifies neutrality and non-physical nature.
+ > Global:
+ > Time: Blue is often associated with calmness and the passage of time.
+ > Angle and Solid Angle: Different shades of blue and cyan suggest angular dimensions and spatial aspects.
+ > Frequency and Angular Frequency: Darker shades of blue to maintain the link to time.
+ > Cartesian:
+ > Length, Area, Volume: Shades of green to represent spatial dimensions, with intensity increasing with dimension.
+ > Mechanical:
+ > Velocity and Acceleration: Red signifies motion and dynamics, with lighter reds for related quantities.
+ > Mass: Dark red for the fundamental property.
+ > Force and Pressure: Shades of red indicating intensity.
+ > Energy:
+ > Work and Power: Orange signifies energy transformation, with lighter oranges for related quantities.
+ > Temperature: Yellow for heat.
+ > Electrodynamics:
+ > Current and related quantities: Cyan shades indicating flow.
+ > Voltage, Capacitance: Greenish and blueish cyan for electrical potential.
+ > Impedance, Conductance, Conductivity: Purples and magentas to signify resistance and conductance.
+ > Magnetic properties: Magenta shades for magnetism.
+ > Electric Field: Light blue.
+ > Magnetic Field: Grey, as it can be considered neutral in terms of direction.
+ > Luminal:
+ > Luminous properties: Yellows to signify light and illumination.
+ >
+ > This color mapping helps maintain intuitive connections for users interacting with these physical types.
+ """
+ PT = PhysicalType
+ return {
+ PT.NonPhysical: (0.75, 0.75, 0.75, 1.0), # Light Grey: Non-physical
+ # Global
+ PT.Time: (0.5, 0.5, 1.0, 1.0), # Light Blue: Time
+ PT.Angle: (0.5, 0.75, 1.0, 1.0), # Light Blue: Angle
+ PT.SolidAngle: (0.5, 0.75, 0.75, 1.0), # Light Cyan: Solid Angle
+ PT.Freq: (0.5, 0.5, 0.9, 1.0), # Light Blue: Frequency
+ PT.AngFreq: (0.5, 0.5, 0.8, 1.0), # Light Blue: Angular Frequency
+ # Cartesian
+ PT.Length: (0.5, 1.0, 0.5, 1.0), # Light Green: Length
+ PT.Area: (0.6, 1.0, 0.6, 1.0), # Light Green: Area
+ PT.Volume: (0.7, 1.0, 0.7, 1.0), # Light Green: Volume
+ # Mechanical
+ PT.Vel: (1.0, 0.5, 0.5, 1.0), # Light Red: Velocity
+ PT.Accel: (1.0, 0.6, 0.6, 1.0), # Light Red: Acceleration
+ PT.Mass: (0.75, 0.5, 0.5, 1.0), # Light Red: Mass
+ PT.Force: (0.9, 0.5, 0.5, 1.0), # Light Red: Force
+ PT.Pressure: (1.0, 0.7, 0.7, 1.0), # Light Red: Pressure
+ # Energy
+ PT.Work: (1.0, 0.75, 0.5, 1.0), # Light Orange: Work
+ PT.Power: (1.0, 0.85, 0.5, 1.0), # Light Orange: Power
+ PT.PowerFlux: (1.0, 0.8, 0.6, 1.0), # Light Orange: Power Flux
+ PT.Temp: (1.0, 1.0, 0.5, 1.0), # Light Yellow: Temperature
+ # Electrodynamics
+ PT.Current: (0.5, 1.0, 1.0, 1.0), # Light Cyan: Current
+ PT.CurrentDensity: (0.5, 0.9, 0.9, 1.0), # Light Cyan: Current Density
+ PT.Charge: (0.5, 0.85, 0.85, 1.0), # Light Cyan: Charge
+ PT.Voltage: (0.5, 1.0, 0.75, 1.0), # Light Greenish Cyan: Voltage
+ PT.Capacitance: (0.5, 0.75, 1.0, 1.0), # Light Blueish Cyan: Capacitance
+ PT.Impedance: (0.6, 0.5, 0.75, 1.0), # Light Purple: Impedance
+ PT.Conductance: (0.7, 0.5, 0.8, 1.0), # Light Purple: Conductance
+ PT.Conductivity: (0.8, 0.5, 0.9, 1.0), # Light Purple: Conductivity
+ PT.MFlux: (0.75, 0.5, 0.75, 1.0), # Light Magenta: Magnetic Flux
+ PT.MFluxDensity: (
+ 0.85,
+ 0.5,
+ 0.85,
+ 1.0,
+ ), # Light Magenta: Magnetic Flux Density
+ PT.Inductance: (0.8, 0.5, 0.8, 1.0), # Light Magenta: Inductance
+ PT.EField: (0.75, 0.75, 1.0, 1.0), # Light Blue: Electric Field
+ PT.HField: (0.75, 0.75, 0.75, 1.0), # Light Grey: Magnetic Field
+ # Luminal
+ PT.LumIntensity: (1.0, 0.95, 0.5, 1.0), # Light Yellow: Luminous Intensity
+ PT.LumFlux: (1.0, 0.95, 0.6, 1.0), # Light Yellow: Luminous Flux
+ PT.Illuminance: (1.0, 1.0, 0.75, 1.0), # Pale Yellow: Illuminance
+ }[self]
+
@functools.cached_property
def default_unit(self) -> list[Unit]:
PT = PhysicalType
@@ -1256,17 +1394,59 @@ class PhysicalType(enum.StrEnum):
}[self]
@staticmethod
- def from_unit(unit: Unit, optional: bool = False) -> list[Unit] | None:
- for physical_type in list(PhysicalType):
- if unit in physical_type.valid_units:
- return physical_type
- ## TODO: Optimize
+ def from_unit(unit: Unit | None, optional: bool = False) -> typ.Self | None:
+ """Attempt to determine a matching `PhysicalType` from a unit.
+
+ NOTE: It is not guaranteed that `unit` is within `valid_units`, only that it can be converted to any unit in `valid_units`.
+
+ Returns:
+ The matched `PhysicalType`.
+
+ If none could be matched, then either return `None` (if `optional` is set) or error.
+
+ Raises:
+ ValueError: If no `PhysicalType` could be matched, and `optional` is `False`.
+ """
+ if unit is None:
+ return ct.PhysicalType.NonPhysical
+
+ unit_dim_deps = unit_to_unit_dim_deps(unit)
+ if unit_dim_deps is not None:
+ for physical_type, candidate_unit_dim in PhysicalType.unit_dims.items():
+ if compare_unit_dim_to_unit_dim_deps(candidate_unit_dim, unit_dim_deps):
+ return physical_type
if optional:
return None
msg = f'Could not determine PhysicalType for {unit}'
raise ValueError(msg)
+ @staticmethod
+ def from_unit_dim(
+ unit_dim: SympyType | None, optional: bool = False
+ ) -> typ.Self | None:
+ """Attempts to match an arbitrary unit dimension expression to a corresponding `PhysicalType`.
+
+ For comparing arbitrary unit dimensions (via expressions of `spu.dimensions.Dimension`), it is critical that equivalent dimensions are also compared as equal (ex. `mass*length/time^2 == force`).
+ To do so, we employ the `SI` unit conventions, for extracting the fundamental dimensional dependencies of unit dimension expressions.
+
+ Returns:
+ The matched `PhysicalType`.
+
+ If none could be matched, then either return `None` (if `optional` is set) or error.
+
+ Raises:
+ ValueError: If no `PhysicalType` could be matched, and `optional` is `False`.
+ """
+ for physical_type, candidate_unit_dim in PhysicalType.unit_dims.items():
+ if compare_unit_dims(unit_dim, candidate_unit_dim):
+ return physical_type
+
+ if optional:
+ return None
+ msg = f'Could not determine PhysicalType for {unit_dim}'
+ raise ValueError(msg)
+
@functools.cached_property
def valid_shapes(self) -> list[typ.Literal[(3,), (2,)] | None]:
PT = PhysicalType
diff --git a/src/blender_maxwell/utils/image_ops.py b/src/blender_maxwell/utils/image_ops.py
index 93baf22..205cb71 100644
--- a/src/blender_maxwell/utils/image_ops.py
+++ b/src/blender_maxwell/utils/image_ops.py
@@ -17,7 +17,6 @@
"""Useful image processing operations for use in the addon."""
import enum
-import functools
import typing as typ
import jax
@@ -27,13 +26,13 @@ import matplotlib
import matplotlib.axis as mpl_ax
import matplotlib.backends.backend_agg
import matplotlib.figure
-import matplotlib.style as mplstyle
+import numpy as np
import seaborn as sns
from blender_maxwell import contracts as ct
+from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
-# mplstyle.use('fast') ## TODO: Does this do anything?
sns.set_theme()
log = logger.get(__name__)
@@ -139,7 +138,7 @@ def rgba_image_from_2d_map(
####################
# - MPL Helpers
####################
-@functools.lru_cache(maxsize=16)
+# @functools.lru_cache(maxsize=16)
def mpl_fig_canvas_ax(width_inches: float, height_inches: float, dpi: int):
fig = matplotlib.figure.Figure(
figsize=[width_inches, height_inches], dpi=dpi, layout='tight'
@@ -160,9 +159,9 @@ def plot_box_plot_1d(data, ax: mpl_ax.Axis) -> None:
x_sym, y_sym = list(data.keys())
ax.boxplot([data[y_sym]])
- ax.set_title(f'{x_sym.name_pretty} -> {y_sym.name_pretty}')
+ ax.set_title(f'{x_sym.name_pretty} ā {y_sym.name_pretty}')
ax.set_xlabel(x_sym.plot_label)
- ax.set_xlabel(y_sym.plot_label)
+ ax.set_ylabel(y_sym.plot_label)
def plot_bar(data, ax: mpl_ax.Axis) -> None:
@@ -173,26 +172,31 @@ def plot_bar(data, ax: mpl_ax.Axis) -> None:
ax.set_title(f'{x_sym.name_pretty} -> {heights_sym.name_pretty}')
ax.set_xlabel(x_sym.plot_label)
- ax.set_xlabel(heights_sym.plot_label)
+ ax.set_ylabel(heights_sym.plot_label)
-# (ā) -> ā
+# (ā) -> ā (| sometimes complex)
def plot_curve_2d(data, ax: mpl_ax.Axis) -> None:
x_sym, y_sym = list(data.keys())
+ if y_sym.mathtype is spux.MathType.Complex:
+ ax.plot(data[x_sym], data[y_sym].real, label='ā')
+ ax.plot(data[x_sym], data[y_sym].imag, label='š')
+ ax.legend()
+
ax.plot(data[x_sym], data[y_sym])
- ax.set_title(f'{x_sym.name_pretty} -> {y_sym.name_pretty}')
+ ax.set_title(f'{x_sym.name_pretty} ā {y_sym.name_pretty}')
ax.set_xlabel(x_sym.plot_label)
- ax.set_xlabel(y_sym.plot_label)
+ ax.set_ylabel(y_sym.plot_label)
def plot_points_2d(data, ax: mpl_ax.Axis) -> None:
x_sym, y_sym = list(data.keys())
ax.scatter(data[x_sym], data[y_sym])
- ax.set_title(f'{x_sym.name_pretty} -> {y_sym.name_pretty}')
+ ax.set_title(f'{x_sym.name_pretty} ā {y_sym.name_pretty}')
ax.set_xlabel(x_sym.plot_label)
- ax.set_xlabel(y_sym.plot_label)
+ ax.set_ylabel(y_sym.plot_label)
# (ā, ā¤) -> ā
@@ -202,34 +206,30 @@ def plot_curves_2d(data, ax: mpl_ax.Axis) -> None:
for i, label in enumerate(data[label_sym]):
ax.plot(data[x_sym], data[y_sym][:, i], label=label)
- ax.set_title(f'{x_sym.name_pretty} -> {y_sym.name_pretty}')
+ ax.set_title(f'{x_sym.name_pretty} ā {y_sym.name_pretty}')
ax.set_xlabel(x_sym.plot_label)
- ax.set_xlabel(y_sym.plot_label)
+ ax.set_ylabel(y_sym.plot_label)
ax.legend()
-def plot_filled_curves_2d(
- data: jtyp.Float32[jtyp.Array, 'x_size 2'], info, ax: mpl_ax.Axis
-) -> None:
- x_sym, _, y_sym = list(data.keys())
+def plot_filled_curves_2d(data, ax: mpl_ax.Axis) -> None:
+ x_sym, _, y_sym = list(data.keys(data))
ax.fill_between(data[x_sym], data[y_sym][:, 0], data[x_sym], data[y_sym][:, 1])
- ax.set_title(f'{x_sym.name_pretty} -> {y_sym.name_pretty}')
+ ax.set_title(f'{x_sym.name_pretty} ā {y_sym.name_pretty}')
ax.set_xlabel(x_sym.plot_label)
- ax.set_xlabel(y_sym.plot_label)
+ ax.set_ylabel(y_sym.plot_label)
ax.legend()
# (ā, ā) -> ā
-def plot_heatmap_2d(
- data: jtyp.Float32[jtyp.Array, 'x_size y_size'], info, ax: mpl_ax.Axis
-) -> None:
+def plot_heatmap_2d(data, ax: mpl_ax.Axis) -> None:
x_sym, y_sym, c_sym = list(data.keys())
heatmap = ax.imshow(data[c_sym], aspect='equal', interpolation='none')
ax.figure.colorbar(heatmap, cax=ax)
- ax.set_title(f'({x_sym.name_pretty}, {y_sym.name_pretty}) -> {c_sym.plot_label}')
+ ax.set_title(f'({x_sym.name_pretty}, {y_sym.name_pretty}) ā {c_sym.plot_label}')
ax.set_xlabel(x_sym.plot_label)
ax.set_xlabel(y_sym.plot_label)
ax.legend()
diff --git a/src/blender_maxwell/utils/sim_symbols.py b/src/blender_maxwell/utils/sim_symbols.py
index c8370f4..3be4750 100644
--- a/src/blender_maxwell/utils/sim_symbols.py
+++ b/src/blender_maxwell/utils/sim_symbols.py
@@ -24,7 +24,6 @@ from fractions import Fraction
import jaxtyping as jtyp
import pydantic as pyd
import sympy as sp
-import sympy.physics.units as spu
from . import extra_sympy_units as spux
from . import logger, serialize
@@ -88,6 +87,7 @@ class SimSymbolName(enum.StrEnum):
Wavelength = enum.auto()
Frequency = enum.auto()
+ Perm = enum.auto()
PermXX = enum.auto()
PermYY = enum.auto()
PermZZ = enum.auto()
@@ -161,6 +161,7 @@ class SimSymbolName(enum.StrEnum):
# Optics
SSN.Wavelength: 'wl',
SSN.Frequency: 'freq',
+ SSN.Perm: 'eps_r',
SSN.PermXX: 'eps_xx',
SSN.PermYY: 'eps_yy',
SSN.PermZZ: 'eps_zz',
@@ -179,6 +180,7 @@ class SimSymbolName(enum.StrEnum):
SSN.LowerTheta: 'Īø',
SSN.LowerPhi: 'Ļ',
# Fields
+ SSN.Er: 'Er',
SSN.Etheta: 'EĪø',
SSN.Ephi: 'EĻ',
SSN.Hr: 'Hr',
@@ -186,10 +188,11 @@ class SimSymbolName(enum.StrEnum):
SSN.Hphi: 'HĻ',
# Optics
SSN.Wavelength: 'Ī»',
- SSN.Frequency: 'š',
- SSN.PermXX: 'Īµ_xx',
- SSN.PermYY: 'Īµ_yy',
- SSN.PermZZ: 'Īµ_zz',
+ SSN.Frequency: 'fįµ£',
+ SSN.Perm: 'Īµįµ£',
+ SSN.PermXX: 'Īµįµ£[xx]',
+ SSN.PermYY: 'Īµįµ£[yy]',
+ SSN.PermZZ: 'Īµįµ£[zz]',
}.get(self, self.name)
@@ -248,6 +251,8 @@ class SimSymbol(pyd.BaseModel):
## -> See self.domain.
## -> We have to deconstruct symbolic interval semantics a bit for UI.
is_constant: bool = False
+ exclude_zero: bool = False
+
interval_finite_z: tuple[int, int] = (0, 1)
interval_finite_q: tuple[tuple[int, int], tuple[int, int]] = ((0, 1), (1, 1))
interval_finite_re: tuple[float, float] = (0.0, 1.0)
@@ -284,20 +289,25 @@ class SimSymbol(pyd.BaseModel):
@functools.cached_property
def unit_label(self) -> str:
"""Pretty unit label, which is an empty string when there is no unit."""
- return spux.sp_to_str(self.unit) if self.unit is not None else ''
+ return spux.sp_to_str(self.unit.n(4)) if self.unit is not None else ''
+
+ @functools.cached_property
+ def name_unit_label(self) -> str:
+ """Pretty name | unit label, which is just the name when there is no unit."""
+ if self.unit is None:
+ return self.name_pretty
+ return f'{self.name_pretty} | {self.unit_label}'
@functools.cached_property
def def_label(self) -> str:
"""Pretty definition label, exposing the symbol definition."""
- return f'{self.name_pretty} | {self.unit_label} ā {self.mathtype_size_label}'
+ return f'{self.name_unit_label} ā {self.mathtype_size_label}'
## TODO: Domain of validity from self.domain?
@functools.cached_property
def plot_label(self) -> str:
"""Pretty plot-oriented label."""
- return f'{self.name_pretty}' + (
- f'({self.unit})' if self.unit is not None else ''
- )
+ return f'{self.name_pretty} ({self.unit_label})'
####################
# - Computed Properties
@@ -307,6 +317,11 @@ class SimSymbol(pyd.BaseModel):
"""Factor corresponding to the tracked unit, which can be multiplied onto exported values without `None`-checking."""
return self.unit if self.unit is not None else sp.S(1)
+ @functools.cached_property
+ def unit_dim(self) -> spux.SympyExpr:
+ """Unit dimension factor corresponding to the tracked unit, which can be multiplied onto exported values without `None`-checking."""
+ return self.unit if self.unit is not None else sp.S(1)
+
@functools.cached_property
def size(self) -> tuple[int, ...] | None:
return {
@@ -403,6 +418,29 @@ class SimSymbol(pyd.BaseModel):
case (False, False):
return sp.S(self.mathtype.coerce_compatible_pyobj(-1))
+ @functools.cached_property
+ def is_nonzero(self) -> bool:
+ if self.exclude_zero:
+ return True
+
+ def check_real_domain(real_domain):
+ return (
+ (
+ real_domain.left == 0
+ and real_domain.left_open
+ or real_domain.right == 0
+ and real_domain.right_open
+ )
+ or real_domain.left > 0
+ or real_domain.right < 0
+ )
+
+ if self.mathtype is spux.MathType.Complex:
+ return check_real_domain(self.domain[0]) and check_real_domain(
+ self.domain[1]
+ )
+ return check_real_domain(self.domain)
+
####################
# - Properties
####################
@@ -434,23 +472,15 @@ class SimSymbol(pyd.BaseModel):
mathtype_kwargs |= {'complex': True}
# Non-Zero Assumption
- if (
- (
- self.domain.left == 0
- and self.domain.left_open
- or self.domain.right == 0
- and self.domain.right_open
- )
- or self.domain.left > 0
- or self.domain.right < 0
- ):
+ if self.is_nonzero:
mathtype_kwargs |= {'nonzero': True}
# Positive/Negative Assumption
- if self.domain.left >= 0:
- mathtype_kwargs |= {'positive': True}
- elif self.domain.right <= 0:
- mathtype_kwargs |= {'negative': True}
+ if self.mathtype is not spux.MathType.Complex:
+ if self.domain.left >= 0:
+ mathtype_kwargs |= {'positive': True}
+ elif self.domain.right <= 0:
+ mathtype_kwargs |= {'negative': True}
# Scalar: Return Symbol
if self.rows == 1 and self.cols == 1:
@@ -521,8 +551,8 @@ class SimSymbol(pyd.BaseModel):
self.valid_domain_value, strip_unit=True
),
# Defaults: FlowKind.Range
- 'default_min': self.domain.start,
- 'default_max': self.domain.end,
+ 'default_min': self.conform(self.domain.start, strip_unit=True),
+ 'default_max': self.conform(self.domain.end, strip_unit=True),
}
msg = f'Tried to generate an ExprSocket from a SymSymbol "{self.name}", but its unit ({self.unit}) is not a valid unit of its physical type ({self.physical_type}) (SimSymbol={self})'
raise NotImplementedError(msg)
@@ -671,7 +701,9 @@ class SimSymbol(pyd.BaseModel):
sym_name: SimSymbolName,
expr: spux.SympyExpr,
unit_expr: spux.SympyExpr,
- ) -> typ.Self:
+ is_constant: bool = False,
+ optional: bool = False,
+ ) -> typ.Self | None:
"""Deduce a `SimSymbol` that matches the output of a given expression (and unit expression).
This is an essential method, allowing for the ded
@@ -697,12 +729,28 @@ class SimSymbol(pyd.BaseModel):
# MathType from Expr Assumptions
## -> All input symbols have assumptions, because we are very pedantic.
## -> Therefore, we should be able to reconstruct the MathType.
- mathtype = spux.MathType.from_expr(expr)
+ mathtype = spux.MathType.from_expr(expr, optional=optional)
+ if mathtype is None:
+ return None
# PhysicalType as "NonPhysical"
## -> 'unit' still applies - but we can't guarantee a PhysicalType will.
## -> Therefore, this is what we gotta do.
- physical_type = spux.PhysicalType.NonPhysical
+ if spux.uses_units(unit_expr):
+ simplified_unit_expr = sp.simplify(unit_expr)
+ expr_physical_type = spux.PhysicalType.from_unit(
+ simplified_unit_expr, optional=True
+ )
+
+ physical_type = (
+ spux.PhysicalType.NonPhysical
+ if expr_physical_type is None
+ else expr_physical_type
+ )
+ unit = simplified_unit_expr
+ else:
+ physical_type = spux.PhysicalType.NonPhysical
+ unit = None
# Rows/Cols from Expr (if Matrix)
rows, cols = expr.shape if isinstance(expr, sp.MatrixBase) else (1, 1)
@@ -711,9 +759,11 @@ class SimSymbol(pyd.BaseModel):
sym_name=sym_name,
mathtype=mathtype,
physical_type=physical_type,
- unit=unit_expr if unit_expr != 1 else None,
+ unit=unit,
rows=rows,
cols=cols,
+ is_constant=is_constant,
+ exclude_zero=expr.is_zero is not None and not expr.is_zero,
)
####################