refactor: symbolic flow support
Massive boost. Hard to quantify everything. We're almost batch/inverse design! - All units are now correct at realization (they weren't before), and several operations were simply wrong. - Near-optimal "flow narrowing", which globally minimizes `DataChanged` even in the face of complex internal property dependencies. - Sockets now cache `FlowKind` output themselves, with invalidation provided for and flow narrowing happening before it even enters the node flow. - New FlowKind (`PreviewsFlow`) w/plotting, 3D, efficient caching, with all previewed nodes already adapted. - Drastically prettified plot output, due to seaborn theme integration, dict-like data transport w/better labelling, etc. . - Deep, reliable unit/unit dimension deduction and arithmetic for `PhysicalType` deduction using dimensional analysis on arbitrary expression operations. - Fourier transform w/Nyquist limits as boundary conditions (preserving original shift). - Vastly improved math node operation checks. - Symbol constant node, integrating domain presumptions. - Flow integration for ExprSocket, including for flow-dynamic capabilities and color. - Viewer node got a big update, incl. live-display of sympy types (even matrix) and a few Tidy3D types, as well as seperating deeper options into a "Debug" mode - Deeply streamlined symbolic flow, including - **Differentiable Box structure**, w/generic lazy parameter support - only the start!main
parent
bcba444a8b
commit
a3551c68b7
BIN
src/blender_maxwell/assets/structures/primitives/box.blend (Stored with Git LFS)
BIN
src/blender_maxwell/assets/structures/primitives/box.blend (Stored with Git LFS)
Binary file not shown.
|
@ -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',
|
||||
|
|
|
@ -230,7 +230,6 @@ class BLSocketType(enum.StrEnum):
|
|||
return {
|
||||
# Blender
|
||||
# Basic
|
||||
BLST.Bool: MT.Bool,
|
||||
# Float
|
||||
BLST.Float: MT.Real,
|
||||
BLST.FloatAngle: MT.Real,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
|
|
|
@ -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,10 +319,10 @@ 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)
|
||||
## TODO: Handle per-cell matrix units?
|
||||
|
||||
return InfoFlow(
|
||||
dims=self.dims,
|
||||
|
@ -326,16 +330,12 @@ class InfoFlow:
|
|||
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)
|
||||
|
||||
####################
|
||||
# - 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,
|
||||
|
|
|
@ -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
|
||||
####################
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -86,12 +86,14 @@ 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(
|
||||
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
|
||||
symbols = {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
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,36 +457,42 @@ 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,
|
||||
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 unit in self.dim.physical_type.valid_units
|
||||
]
|
||||
if physical_type is not None:
|
||||
valid_units = physical_type.valid_units
|
||||
else:
|
||||
valid_units = []
|
||||
|
||||
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 = []
|
||||
|
||||
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),
|
||||
|
@ -492,11 +501,9 @@ class TransformMathNode(base.MaxwellSimNode):
|
|||
'',
|
||||
i,
|
||||
)
|
||||
for i, unit in enumerate(self.new_physical_type.valid_units)
|
||||
for i, unit in enumerate(valid_units)
|
||||
]
|
||||
|
||||
return []
|
||||
|
||||
@bl_cache.cached_bl_property(depends_on={'active_new_unit'})
|
||||
def new_unit(self) -> spux.Unit:
|
||||
if self.active_new_unit is not None:
|
||||
|
@ -507,103 +514,192 @@ 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)
|
||||
|
||||
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='')
|
||||
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='')
|
||||
|
||||
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_name'], text='')
|
||||
row.prop(self, self.blfields['active_new_unit'], text='')
|
||||
|
||||
row = col.row(align=True)
|
||||
row.prop(self, self.blfields['new_physical_type'], text='')
|
||||
|
||||
case TO.FT1D | TO.InvFT1D:
|
||||
layout.prop(self, self.blfields['active_dim'], text='')
|
||||
|
||||
####################
|
||||
# - Compute: Func / Array
|
||||
####################
|
||||
@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:
|
||||
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,
|
||||
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
|
||||
|
||||
####################
|
||||
# - FlowKind.Info
|
||||
####################
|
||||
@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)
|
||||
|
||||
if has_info and operation is not None:
|
||||
# Retrieve Properties
|
||||
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:
|
||||
|
||||
# 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)
|
||||
|
||||
if has_lazy_func and has_params and not params.symbols:
|
||||
data = lazy_func.realize(params)
|
||||
if data.shape is not None and len(data.shape) == 2:
|
||||
data_col = data[:, 0]
|
||||
return operation.transform_info(info, data_col=data_col)
|
||||
# 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
|
||||
|
||||
# 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
|
||||
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,
|
||||
|
@ -611,6 +707,39 @@ class TransformMathNode(base.MaxwellSimNode):
|
|||
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: # noqa: PLR2004
|
||||
data_col = data[:, 0]
|
||||
return operation.transform_info(
|
||||
info,
|
||||
new_dim_name=new_name,
|
||||
data_col=data_col,
|
||||
unit=new_unit,
|
||||
physical_type=new_physical_type,
|
||||
)
|
||||
|
||||
# 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 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
|
||||
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
## -> 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
|
||||
}
|
||||
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}
|
||||
|
||||
# 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:
|
||||
|
|
|
@ -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:
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
|
||||
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}.'
|
||||
msg = (
|
||||
f'More than one method found for ({output_socket_name}, {kind.value!s}.'
|
||||
)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return output_socket_methods[0](self)
|
||||
|
||||
# Auxiliary Fallbacks
|
||||
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=...,
|
||||
unit_system=...,
|
||||
)
|
||||
else:
|
||||
for socket_kind in socket_kinds:
|
||||
self._compute_input.invalidate(
|
||||
input_socket_name=input_socket_name,
|
||||
kind=socket_kind,
|
||||
input_socket_name=in_sckname,
|
||||
kind=in_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)',
|
||||
# self.sim_node_name,
|
||||
# dep_out[0],
|
||||
# dep_out[1],
|
||||
# )
|
||||
altered_socket_kinds.add(dep_out[1])
|
||||
self.compute_output.invalidate(
|
||||
output_socket_name=dep_out[0],
|
||||
kind=dep_out[1],
|
||||
# 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,
|
||||
# out_sckname,
|
||||
# out_kind,
|
||||
# )
|
||||
self.compute_output.invalidate(
|
||||
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
|
||||
|
||||
# log.critical(
|
||||
# '![%s] Propagating: (%s, %s)',
|
||||
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,
|
||||
# altered_socket_kinds,
|
||||
# direc,
|
||||
# altered_socket_kinds[bl_socket.name],
|
||||
# )
|
||||
bl_socket.trigger_event(event, socket_kinds=altered_socket_kinds)
|
||||
bl_socket.trigger_event(
|
||||
event, socket_kinds=altered_socket_kinds[bl_socket.name]
|
||||
)
|
||||
|
||||
## -> 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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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_structure(self, input_sockets, unit_systems) -> td.Box:
|
||||
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=input_sockets['Center'],
|
||||
size=input_sockets['Size'],
|
||||
center=spux.scale_to_unit_system(center, ct.UNITS_TIDY3D),
|
||||
size=spux.scale_to_unit_system(size, ct.UNITS_TIDY3D),
|
||||
),
|
||||
medium=input_sockets['Medium'],
|
||||
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_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,28 +238,25 @@ 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,
|
||||
):
|
||||
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': unit_systems['BlenderUnits'],
|
||||
'unit_system': ct.UNITS_BLENDER,
|
||||
'inputs': {
|
||||
'Size': input_sockets['Size'],
|
||||
},
|
||||
},
|
||||
location=input_sockets['Center'],
|
||||
location=spux.scale_to_unit_system(center, ct.UNITS_BLENDER),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,12 +766,8 @@ 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)
|
||||
|
||||
####################
|
||||
# - UI - Color
|
||||
####################
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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_spstr,
|
||||
locals={sym.name: sym.sp_symbol_matsym for sym in self.symbols},
|
||||
strict=False,
|
||||
convert_xor=True,
|
||||
).subs(spux.UNIT_BY_SYMBOL)
|
||||
|
||||
# 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 = sp.parsing.sympy_parser.parse_expr(
|
||||
expr_spstr,
|
||||
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,
|
||||
],
|
||||
)
|
||||
else:
|
||||
return expr
|
||||
|
||||
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:
|
||||
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,
|
||||
spux.strip_unit_system(self.value),
|
||||
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:
|
||||
if self.sorted_symbols:
|
||||
output_sym = self.output_sym
|
||||
if output_sym is not None:
|
||||
return ct.ParamsFlow(
|
||||
func_args=[sym.sp_symbol_phy for sym in self.sorted_symbols],
|
||||
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:
|
||||
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=output_sym,
|
||||
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,6 +957,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
|||
Notes:
|
||||
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
|
||||
"""
|
||||
if self.active_kind is ct.FlowKind.Func:
|
||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||
has_info = not ct.FlowSignal.check(info)
|
||||
|
||||
|
@ -715,6 +981,8 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
|||
text='',
|
||||
icon=ct.Icon.ToggleSocketInfo,
|
||||
)
|
||||
else:
|
||||
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,6 +992,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
|||
Notes:
|
||||
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
|
||||
"""
|
||||
if self.active_kind is ct.FlowKind.Func:
|
||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||
has_info = not ct.FlowSignal.check(info)
|
||||
|
||||
|
@ -749,6 +1018,8 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
|||
_col.label(text='')
|
||||
else:
|
||||
_row = row
|
||||
else:
|
||||
_row = row
|
||||
|
||||
_row.label(text=text)
|
||||
|
||||
|
@ -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()
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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] = {
|
||||
allow_axes: set[ct.SimSpaceAxis] = pyd.Field(
|
||||
default={
|
||||
ct.SimSpaceAxis.X,
|
||||
ct.SimSpaceAxis.Y,
|
||||
ct.SimSpaceAxis.Z,
|
||||
}
|
||||
present_axes: set[ct.SimSpaceAxis] = {
|
||||
)
|
||||
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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
####################
|
||||
|
|
|
@ -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,7 +366,7 @@ 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)
|
||||
with self.suppress_update(bl_instance):
|
||||
self.bl_prop_enum_items.write(bl_instance, current_items)
|
||||
|
||||
# Old Item in Current Items
|
||||
|
@ -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)
|
||||
with self.suppress_update(bl_instance):
|
||||
self.bl_prop.write(bl_instance, old_item)
|
||||
## -> TODO: Don't write if not needed.
|
||||
|
||||
# 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)
|
||||
with self.suppress_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)
|
||||
|
||||
# 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:
|
||||
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)
|
||||
|
||||
####################
|
||||
|
|
|
@ -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,43 +171,58 @@ 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(
|
||||
## -> 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)
|
||||
)
|
||||
|
||||
# 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
|
||||
):
|
||||
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:
|
||||
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||
|
||||
# Call Setter
|
||||
elif self.setter_method is not None:
|
||||
if bl_instance is not None:
|
||||
# Run Setter
|
||||
## -> The user-provided setter should do any updating of partners.
|
||||
if self.setter_method is not None:
|
||||
## -> 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):
|
||||
# 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)
|
||||
)
|
||||
|
||||
# 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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,36 +238,111 @@ 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,
|
||||
# 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,
|
||||
dst_prop_name,
|
||||
invalidate_signal,
|
||||
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.
|
||||
|
||||
|
|
|
@ -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:
|
||||
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
|
||||
## TODO: Optimize
|
||||
|
||||
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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,19 +472,11 @@ 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.mathtype is not spux.MathType.Complex:
|
||||
if self.domain.left >= 0:
|
||||
mathtype_kwargs |= {'positive': True}
|
||||
elif self.domain.right <= 0:
|
||||
|
@ -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.
|
||||
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,
|
||||
)
|
||||
|
||||
####################
|
||||
|
|
Loading…
Reference in New Issue