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,
|
ArrayFlow,
|
||||||
CapabilitiesFlow,
|
CapabilitiesFlow,
|
||||||
FlowKind,
|
FlowKind,
|
||||||
InfoFlow,
|
|
||||||
RangeFlow,
|
|
||||||
FuncFlow,
|
FuncFlow,
|
||||||
|
InfoFlow,
|
||||||
ParamsFlow,
|
ParamsFlow,
|
||||||
|
PreviewsFlow,
|
||||||
|
RangeFlow,
|
||||||
ScalingMode,
|
ScalingMode,
|
||||||
ValueFlow,
|
ValueFlow,
|
||||||
)
|
)
|
||||||
|
@ -118,6 +119,7 @@ __all__ = [
|
||||||
'CapabilitiesFlow',
|
'CapabilitiesFlow',
|
||||||
'FlowKind',
|
'FlowKind',
|
||||||
'InfoFlow',
|
'InfoFlow',
|
||||||
|
'PreviewsFlow',
|
||||||
'RangeFlow',
|
'RangeFlow',
|
||||||
'FuncFlow',
|
'FuncFlow',
|
||||||
'ParamsFlow',
|
'ParamsFlow',
|
||||||
|
|
|
@ -230,7 +230,6 @@ class BLSocketType(enum.StrEnum):
|
||||||
return {
|
return {
|
||||||
# Blender
|
# Blender
|
||||||
# Basic
|
# Basic
|
||||||
BLST.Bool: MT.Bool,
|
|
||||||
# Float
|
# Float
|
||||||
BLST.Float: MT.Real,
|
BLST.Float: MT.Real,
|
||||||
BLST.FloatAngle: 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.
|
This event can lock a subset of the node tree graph.
|
||||||
DisableLock: Indicates that the node/socket should disable locking.
|
DisableLock: Indicates that the node/socket should disable locking.
|
||||||
This event can unlock part of a locked subgraph.
|
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.
|
LinkChanged: Indicates that a link to a node/socket was added/removed.
|
||||||
Is translated to `DataChanged` on sockets before propagation.
|
Is translated to `DataChanged` on sockets before propagation.
|
||||||
DataChanged: Indicates that data flowing through a node/socket was altered.
|
DataChanged: Indicates that data flowing through a node/socket was altered.
|
||||||
|
@ -50,15 +46,12 @@ class FlowEvent(enum.StrEnum):
|
||||||
EnableLock = enum.auto()
|
EnableLock = enum.auto()
|
||||||
DisableLock = enum.auto()
|
DisableLock = enum.auto()
|
||||||
|
|
||||||
# Preview Events
|
|
||||||
ShowPreview = enum.auto()
|
|
||||||
ShowPlot = enum.auto()
|
|
||||||
|
|
||||||
# Data Events
|
# Data Events
|
||||||
LinkChanged = enum.auto()
|
LinkChanged = enum.auto()
|
||||||
DataChanged = enum.auto()
|
DataChanged = enum.auto()
|
||||||
|
|
||||||
# Non-Triggered Events
|
# Non-Triggered Events
|
||||||
|
ShowPlot = enum.auto()
|
||||||
OutputRequested = enum.auto()
|
OutputRequested = enum.auto()
|
||||||
|
|
||||||
# Properties
|
# Properties
|
||||||
|
@ -79,9 +72,6 @@ class FlowEvent(enum.StrEnum):
|
||||||
# Lock Events
|
# Lock Events
|
||||||
FlowEvent.EnableLock: 'input',
|
FlowEvent.EnableLock: 'input',
|
||||||
FlowEvent.DisableLock: 'input',
|
FlowEvent.DisableLock: 'input',
|
||||||
# Preview Events
|
|
||||||
FlowEvent.ShowPreview: 'input',
|
|
||||||
FlowEvent.ShowPlot: 'input',
|
|
||||||
# Data Events
|
# Data Events
|
||||||
FlowEvent.LinkChanged: 'output',
|
FlowEvent.LinkChanged: 'output',
|
||||||
FlowEvent.DataChanged: 'output',
|
FlowEvent.DataChanged: 'output',
|
||||||
|
|
|
@ -21,6 +21,7 @@ from .info import InfoFlow
|
||||||
from .lazy_func import FuncFlow
|
from .lazy_func import FuncFlow
|
||||||
from .lazy_range import RangeFlow, ScalingMode
|
from .lazy_range import RangeFlow, ScalingMode
|
||||||
from .params import ParamsFlow
|
from .params import ParamsFlow
|
||||||
|
from .previews import PreviewsFlow
|
||||||
from .value import ValueFlow
|
from .value import ValueFlow
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
@ -32,5 +33,6 @@ __all__ = [
|
||||||
'ScalingMode',
|
'ScalingMode',
|
||||||
'FuncFlow',
|
'FuncFlow',
|
||||||
'ParamsFlow',
|
'ParamsFlow',
|
||||||
|
'PreviewsFlow',
|
||||||
'ValueFlow',
|
'ValueFlow',
|
||||||
]
|
]
|
||||||
|
|
|
@ -21,7 +21,6 @@ import typing as typ
|
||||||
import jaxtyping as jtyp
|
import jaxtyping as jtyp
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import sympy as sp
|
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 extra_sympy_units as spux
|
||||||
from blender_maxwell.utils import logger
|
from blender_maxwell.utils import logger
|
||||||
|
@ -117,13 +116,19 @@ class ArrayFlow:
|
||||||
new_unit: An (optional) new unit to scale the result to.
|
new_unit: An (optional) new unit to scale the result to.
|
||||||
"""
|
"""
|
||||||
# Compile JAX-Compatible Rescale Function
|
# 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
|
a = self.mathtype.sp_symbol_a
|
||||||
rescale_expr = (
|
rescale_expr = (
|
||||||
spux.scale_to_unit(rescale_func(a * self.unit), new_unit)
|
spux.scale_to_unit(rescale_func(a * self.unit), new_unit)
|
||||||
if self.unit is not None
|
if self.unit is not None
|
||||||
else rescale_func(a)
|
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)
|
values = _rescale_func(self.values)
|
||||||
|
|
||||||
# Return ArrayFlow
|
# Return ArrayFlow
|
||||||
|
|
|
@ -24,9 +24,30 @@ from .flow_kinds import FlowKind
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True, kw_only=True)
|
@dataclasses.dataclass(frozen=True, kw_only=True)
|
||||||
class CapabilitiesFlow:
|
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
|
socket_type: SocketType
|
||||||
active_kind: FlowKind
|
active_kind: FlowKind
|
||||||
|
|
||||||
|
# Relationships
|
||||||
allow_out_to_in: dict[FlowKind, FlowKind] = dataclasses.field(default_factory=dict)
|
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
|
is_universal: bool = False
|
||||||
|
|
||||||
|
@ -41,12 +62,19 @@ class CapabilitiesFlow:
|
||||||
|
|
||||||
def is_compatible_with(self, other: typ.Self) -> bool:
|
def is_compatible_with(self, other: typ.Self) -> bool:
|
||||||
return other.is_universal or (
|
return other.is_universal or (
|
||||||
self.socket_type == other.socket_type
|
self.socket_type is other.socket_type
|
||||||
and (
|
and (
|
||||||
self.active_kind == other.active_kind
|
self.active_kind is other.active_kind
|
||||||
or (
|
or (
|
||||||
other.active_kind in other.allow_out_to_in
|
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
|
# == Constraint
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import functools
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
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__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
_PROPERTY_NAMES = {
|
||||||
|
'capabilities',
|
||||||
|
'previews',
|
||||||
|
'value',
|
||||||
|
'array',
|
||||||
|
'lazy_range',
|
||||||
|
'lazy_func',
|
||||||
|
'params',
|
||||||
|
'info',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class FlowKind(enum.StrEnum):
|
class FlowKind(enum.StrEnum):
|
||||||
"""Defines a kind of data that can flow between nodes.
|
"""Defines a kind of data that can flow between nodes.
|
||||||
|
@ -50,14 +62,15 @@ class FlowKind(enum.StrEnum):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
Capabilities = enum.auto()
|
Capabilities = enum.auto()
|
||||||
|
Previews = enum.auto()
|
||||||
|
|
||||||
# Values
|
# Values
|
||||||
Value = enum.auto() ## 'value'
|
Value = enum.auto() ## 'value'
|
||||||
Array = enum.auto() ## 'array'
|
Array = enum.auto() ## 'array'
|
||||||
|
|
||||||
# Lazy
|
# Lazy
|
||||||
Func = enum.auto() ## 'lazy_func'
|
|
||||||
Range = enum.auto() ## 'lazy_range'
|
Range = enum.auto() ## 'lazy_range'
|
||||||
|
Func = enum.auto() ## 'lazy_func'
|
||||||
|
|
||||||
# Auxiliary
|
# Auxiliary
|
||||||
Params = enum.auto() ## 'params'
|
Params = enum.auto() ## 'params'
|
||||||
|
@ -70,12 +83,13 @@ class FlowKind(enum.StrEnum):
|
||||||
def to_name(v: typ.Self) -> str:
|
def to_name(v: typ.Self) -> str:
|
||||||
return {
|
return {
|
||||||
FlowKind.Capabilities: 'Capabilities',
|
FlowKind.Capabilities: 'Capabilities',
|
||||||
|
FlowKind.Previews: 'Previews',
|
||||||
# Values
|
# Values
|
||||||
FlowKind.Value: 'Value',
|
FlowKind.Value: 'Value',
|
||||||
FlowKind.Array: 'Array',
|
FlowKind.Array: 'Array',
|
||||||
# Lazy
|
# Lazy
|
||||||
FlowKind.Range: 'Range',
|
|
||||||
FlowKind.Func: 'Func',
|
FlowKind.Func: 'Func',
|
||||||
|
FlowKind.Range: 'Range',
|
||||||
# Auxiliary
|
# Auxiliary
|
||||||
FlowKind.Params: 'Params',
|
FlowKind.Params: 'Params',
|
||||||
FlowKind.Info: 'Info',
|
FlowKind.Info: 'Info',
|
||||||
|
@ -88,6 +102,48 @@ class FlowKind(enum.StrEnum):
|
||||||
####################
|
####################
|
||||||
# - Static Properties
|
# - 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
|
@staticproperty
|
||||||
def active_kinds() -> list[typ.Self]:
|
def active_kinds() -> list[typ.Self]:
|
||||||
"""Return a list of `FlowKind`s that are able to be considered "active".
|
"""Return a list of `FlowKind`s that are able to be considered "active".
|
||||||
|
@ -121,6 +177,7 @@ class FlowKind(enum.StrEnum):
|
||||||
####################
|
####################
|
||||||
# - Class Methods
|
# - Class Methods
|
||||||
####################
|
####################
|
||||||
|
## TODO: Remove this (only events uses it).
|
||||||
@classmethod
|
@classmethod
|
||||||
def scale_to_unit_system(
|
def scale_to_unit_system(
|
||||||
cls,
|
cls,
|
||||||
|
|
|
@ -93,7 +93,7 @@ class InfoFlow:
|
||||||
return list(self.dims.keys())[idx]
|
return list(self.dims.keys())[idx]
|
||||||
return None
|
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.
|
"""The integer axis occupied by the dimension.
|
||||||
|
|
||||||
Can be used to index `.shape` of the represented raw array.
|
Can be used to index `.shape` of the represented raw array.
|
||||||
|
@ -102,6 +102,9 @@ class InfoFlow:
|
||||||
if len(dims_with_name) == 1:
|
if len(dims_with_name) == 1:
|
||||||
return dims_with_name[0]
|
return dims_with_name[0]
|
||||||
|
|
||||||
|
if optional:
|
||||||
|
return None
|
||||||
|
|
||||||
msg = f'Dim name {dim_name} not found in InfoFlow (or >1 found)'
|
msg = f'Dim name {dim_name} not found in InfoFlow (or >1 found)'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
@ -127,14 +130,15 @@ class InfoFlow:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def is_idx_uniform(self, dim: sim_symbols.SimSymbol) -> bool:
|
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.
|
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.
|
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.
|
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:
|
def dim_axis(self, dim: sim_symbols.SimSymbol) -> int:
|
||||||
"""The integer axis occupied by the dimension.
|
"""The integer axis occupied by the dimension.
|
||||||
|
@ -194,7 +198,7 @@ class InfoFlow:
|
||||||
return {
|
return {
|
||||||
dim.name_pretty: {
|
dim.name_pretty: {
|
||||||
'length': str(len(dim_idx)) if dim_idx is not None else '∞',
|
'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,
|
'unit': dim.unit_label,
|
||||||
}
|
}
|
||||||
for dim, dim_idx in self.dims.items()
|
for dim, dim_idx in self.dims.items()
|
||||||
|
@ -315,27 +319,23 @@ class InfoFlow:
|
||||||
op: typ.Callable[[spux.SympyExpr, spux.SympyExpr], spux.SympyExpr],
|
op: typ.Callable[[spux.SympyExpr, spux.SympyExpr], spux.SympyExpr],
|
||||||
unit_op: typ.Callable[[spux.SympyExpr, spux.SympyExpr], spux.SympyExpr],
|
unit_op: typ.Callable[[spux.SympyExpr, spux.SympyExpr], spux.SympyExpr],
|
||||||
) -> spux.SympyExpr:
|
) -> spux.SympyExpr:
|
||||||
if self.dims == other.dims:
|
sym_name = sim_symbols.SimSymbolName.Expr
|
||||||
sym_name = sim_symbols.SimSymbolName.Expr
|
expr = op(self.output.sp_symbol_phy, other.output.sp_symbol_phy)
|
||||||
expr = op(self.output.sp_symbol_phy, other.output.sp_symbol_phy)
|
unit_expr = unit_op(self.output.unit_factor, other.output.unit_factor)
|
||||||
unit_expr = unit_op(self.output.unit_factor, other.output.unit_factor)
|
## TODO: Handle per-cell matrix units?
|
||||||
|
|
||||||
return InfoFlow(
|
return InfoFlow(
|
||||||
dims=self.dims,
|
dims=self.dims,
|
||||||
output=sim_symbols.SimSymbol.from_expr(sym_name, expr, unit_expr),
|
output=sim_symbols.SimSymbol.from_expr(sym_name, expr, unit_expr),
|
||||||
pinned_values=self.pinned_values,
|
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
|
# - Operations: Fold
|
||||||
####################
|
####################
|
||||||
def fold_last_input(self):
|
def fold_last_input(self):
|
||||||
"""Fold the last input dimension into the output."""
|
"""Fold the last input dimension into the output."""
|
||||||
last_key = list(self.dims.keys())[-1]
|
last_idx = self.dims[self.last_dim]
|
||||||
last_idx = list(self.dims.values())[-1]
|
|
||||||
|
|
||||||
rows = self.output.rows
|
rows = self.output.rows
|
||||||
cols = self.output.cols
|
cols = self.output.cols
|
||||||
|
@ -351,7 +351,9 @@ class InfoFlow:
|
||||||
|
|
||||||
return InfoFlow(
|
return InfoFlow(
|
||||||
dims={
|
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,
|
output=new_output,
|
||||||
pinned_values=self.pinned_values,
|
pinned_values=self.pinned_values,
|
||||||
|
|
|
@ -20,10 +20,14 @@ import typing as typ
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
|
||||||
import jax
|
import jax
|
||||||
|
import jaxtyping as jtyp
|
||||||
|
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
from blender_maxwell.utils import logger, sim_symbols
|
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
|
from .params import ParamsFlow
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
@ -314,14 +318,66 @@ class FuncFlow:
|
||||||
) -> typ.Self:
|
) -> typ.Self:
|
||||||
if self.supports_jax:
|
if self.supports_jax:
|
||||||
return self.func_jax(
|
return self.func_jax(
|
||||||
*params.scaled_func_args(self.func_args, symbol_values),
|
*params.scaled_func_args(symbol_values),
|
||||||
*params.scaled_func_kwargs(self.func_args, symbol_values),
|
**params.scaled_func_kwargs(symbol_values),
|
||||||
)
|
)
|
||||||
return self.func(
|
return self.func(
|
||||||
*params.scaled_func_args(self.func_kwargs, symbol_values),
|
*params.scaled_func_args(symbol_values),
|
||||||
*params.scaled_func_kwargs(self.func_kwargs, 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
|
# - Composition Operations
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -218,9 +218,6 @@ class RangeFlow:
|
||||||
)
|
)
|
||||||
return combined_mathtype
|
return combined_mathtype
|
||||||
|
|
||||||
####################
|
|
||||||
# - Methods
|
|
||||||
####################
|
|
||||||
@property
|
@property
|
||||||
def ideal_midpoint(self) -> spux.SympyExpr:
|
def ideal_midpoint(self) -> spux.SympyExpr:
|
||||||
return (self.stop + self.start) / 2
|
return (self.stop + self.start) / 2
|
||||||
|
@ -229,6 +226,41 @@ class RangeFlow:
|
||||||
def ideal_range(self) -> spux.SympyExpr:
|
def ideal_range(self) -> spux.SympyExpr:
|
||||||
return self.stop - self.start
|
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(
|
def rescale(
|
||||||
self, rescale_func, reverse: bool = False, new_unit: spux.Unit | None = None
|
self, rescale_func, reverse: bool = False, new_unit: spux.Unit | None = None
|
||||||
) -> typ.Self:
|
) -> typ.Self:
|
||||||
|
@ -612,8 +644,8 @@ class RangeFlow:
|
||||||
symbols=self.symbols,
|
symbols=self.symbols,
|
||||||
)
|
)
|
||||||
return RangeFlow(
|
return RangeFlow(
|
||||||
start=self.start * unit,
|
start=self.start,
|
||||||
stop=self.stop * unit,
|
stop=self.stop,
|
||||||
steps=self.steps,
|
steps=self.steps,
|
||||||
scaling=self.scaling,
|
scaling=self.scaling,
|
||||||
unit=unit,
|
unit=unit,
|
||||||
|
|
|
@ -31,8 +31,6 @@ from .expr_info import ExprInfo
|
||||||
from .flow_kinds import FlowKind
|
from .flow_kinds import FlowKind
|
||||||
from .lazy_range import RangeFlow
|
from .lazy_range import RangeFlow
|
||||||
|
|
||||||
# from .info import InfoFlow
|
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -44,11 +42,18 @@ class ParamsFlow:
|
||||||
All symbols valid for use in the expression.
|
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_args: list[spux.SympyExpr] = dataclasses.field(default_factory=list)
|
||||||
func_kwargs: dict[str, spux.SympyExpr] = dataclasses.field(default_factory=dict)
|
func_kwargs: dict[str, spux.SympyExpr] = dataclasses.field(default_factory=dict)
|
||||||
|
|
||||||
symbols: frozenset[sim_symbols.SimSymbol] = frozenset()
|
symbols: frozenset[sim_symbols.SimSymbol] = frozenset()
|
||||||
|
|
||||||
|
is_differentiable: bool = False
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Symbols
|
# - Symbols
|
||||||
####################
|
####################
|
||||||
|
@ -76,8 +81,9 @@ class ParamsFlow:
|
||||||
####################
|
####################
|
||||||
# - JIT'ed Callables for Numerical Function Arguments
|
# - JIT'ed Callables for Numerical Function Arguments
|
||||||
####################
|
####################
|
||||||
|
@functools.cached_property
|
||||||
def func_args_n(
|
def func_args_n(
|
||||||
self, target_syms: list[sim_symbols.SimSymbol]
|
self,
|
||||||
) -> list[
|
) -> list[
|
||||||
typ.Callable[
|
typ.Callable[
|
||||||
[int | float | complex | jtyp.Inexact[jtyp.Array, '...'], ...],
|
[int | float | complex | jtyp.Inexact[jtyp.Array, '...'], ...],
|
||||||
|
@ -86,15 +92,12 @@ class ParamsFlow:
|
||||||
]:
|
]:
|
||||||
"""Callable functions for evaluating each `self.func_args` entry numerically.
|
"""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:
|
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.
|
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()`.
|
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 [
|
return [
|
||||||
sp.lambdify(
|
sp.lambdify(
|
||||||
|
@ -102,11 +105,14 @@ class ParamsFlow:
|
||||||
target_sym.conform(func_arg, strip_unit=True),
|
target_sym.conform(func_arg, strip_unit=True),
|
||||||
'jax',
|
'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(
|
def func_kwargs_n(
|
||||||
self, target_syms: dict[str, sim_symbols.SimSymbol]
|
self,
|
||||||
) -> dict[
|
) -> dict[
|
||||||
str,
|
str,
|
||||||
typ.Callable[
|
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()`
|
This ensures conformance to the `SimSymbol` properties, as well as adherance to a numerical type identity compatible with `sp.lambdify()`
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
func_arg_key: sp.lambdify(
|
key: sp.lambdify(
|
||||||
self.sorted_sp_symbols,
|
self.sorted_sp_symbols,
|
||||||
target_syms[func_arg_key].scale(func_arg),
|
self.kwarg_targets[key].conform(func_arg, strip_unit=True),
|
||||||
'jax',
|
'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(
|
def scaled_func_args(
|
||||||
self,
|
self,
|
||||||
target_syms: list[sim_symbols.SimSymbol] = (),
|
|
||||||
symbol_values: dict[sim_symbols.SimSymbol, spux.SympyExpr] = MappingProxyType(
|
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.
|
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.
|
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:
|
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).
|
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`._
|
_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.
|
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:
|
Parameters:
|
||||||
target_syms: `SimSymbol`s describing how the function arguments returned by this method are intended to be used.
|
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`).
|
||||||
**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`).
|
|
||||||
"""
|
"""
|
||||||
realized_symbols = list(self.realize_symbols(symbol_values).values())
|
realized_symbols = list(self.realize_symbols(symbol_values).values())
|
||||||
return [
|
return [func_arg_n(*realized_symbols) for func_arg_n in self.func_args_n]
|
||||||
func_arg_n(*realized_symbols)
|
|
||||||
for func_arg_n in self.func_args_n(target_syms)
|
|
||||||
]
|
|
||||||
|
|
||||||
def scaled_func_kwargs(
|
def scaled_func_kwargs(
|
||||||
self,
|
self,
|
||||||
target_syms: list[sim_symbols.SimSymbol] = (),
|
|
||||||
symbol_values: dict[spux.Symbol, spux.SympyExpr] = MappingProxyType({}),
|
symbol_values: dict[spux.Symbol, spux.SympyExpr] = MappingProxyType({}),
|
||||||
) -> dict[
|
) -> dict[
|
||||||
str, int | float | Fraction | float | complex | jtyp.Shaped[jtyp.Array, '...']
|
str, int | float | Fraction | float | complex | jtyp.Shaped[jtyp.Array, '...']
|
||||||
|
@ -233,7 +230,7 @@ class ParamsFlow:
|
||||||
realized_symbols = self.realize_symbols(symbol_values)
|
realized_symbols = self.realize_symbols(symbol_values)
|
||||||
return {
|
return {
|
||||||
func_arg_name: func_arg_n(**realized_symbols)
|
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.
|
The next composed function will receive a tuple of two arrays, instead of just one, allowing binary operations to occur.
|
||||||
"""
|
"""
|
||||||
return ParamsFlow(
|
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_args=self.func_args + other.func_args,
|
||||||
func_kwargs=self.func_kwargs | other.func_kwargs,
|
func_kwargs=self.func_kwargs | other.func_kwargs,
|
||||||
symbols=self.symbols | other.symbols,
|
symbols=self.symbols | other.symbols,
|
||||||
|
is_differentiable=self.is_differentiable & other.is_differentiable,
|
||||||
)
|
)
|
||||||
|
|
||||||
def compose_within(
|
def compose_within(
|
||||||
self,
|
self,
|
||||||
|
enclosing_arg_targets: list[sim_symbols.SimSymbol] = (),
|
||||||
|
enclosing_kwarg_targets: list[sim_symbols.SimSymbol] = (),
|
||||||
enclosing_func_args: list[spux.SympyExpr] = (),
|
enclosing_func_args: list[spux.SympyExpr] = (),
|
||||||
enclosing_func_kwargs: dict[str, spux.SympyExpr] = MappingProxyType({}),
|
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:
|
) -> typ.Self:
|
||||||
return ParamsFlow(
|
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_args=self.func_args + list(enclosing_func_args),
|
||||||
func_kwargs=self.func_kwargs | dict(enclosing_func_kwargs),
|
func_kwargs=self.func_kwargs | dict(enclosing_func_kwargs),
|
||||||
symbols=self.symbols | enclosing_symbols,
|
symbols=self.symbols | enclosing_symbols,
|
||||||
|
is_differentiable=(
|
||||||
|
self.is_differentiable
|
||||||
|
if not enclosing_symbols
|
||||||
|
else (self.is_differentiable & enclosing_is_differentiable)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Generate ExprSocketDef
|
# - 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`.
|
"""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`.
|
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`)
|
The `ExprInfo`s can be directly defererenced `**expr_info`)
|
||||||
"""
|
"""
|
||||||
for sym in self.sorted_symbols:
|
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:
|
if sym.rows > 3 or sym.cols > 1:
|
||||||
msg = 'No support for >Vec3 / Matrix values in ExprInfo'
|
msg = 'No support for >Vec3 / Matrix values in ExprInfo'
|
||||||
raise NotImplementedError(msg)
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sym.name: {
|
sym: {
|
||||||
'active_kind': FlowKind.Value if not use_range else FlowKind.Range,
|
'default_steps': 25,
|
||||||
'default_steps': 50,
|
|
||||||
}
|
}
|
||||||
| sym.expr_info
|
| sym.expr_info
|
||||||
for sym in self.sorted_symbols
|
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()
|
Scene = enum.auto()
|
||||||
## Inputs / Constants
|
## Inputs / Constants
|
||||||
ExprConstant = enum.auto()
|
ExprConstant = enum.auto()
|
||||||
|
SymbolConstant = enum.auto()
|
||||||
ScientificConstant = enum.auto()
|
ScientificConstant = enum.auto()
|
||||||
UnitSystemConstant = enum.auto()
|
UnitSystemConstant = enum.auto()
|
||||||
BlenderConstant = enum.auto()
|
BlenderConstant = enum.auto()
|
||||||
|
|
|
@ -19,7 +19,7 @@ import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from blender_maxwell.utils import logger
|
from blender_maxwell.utils import logger, serialize
|
||||||
|
|
||||||
from . import contracts as ct
|
from . import contracts as ct
|
||||||
from .managed_objs.managed_bl_image import ManagedBLImage
|
from .managed_objs.managed_bl_image import ManagedBLImage
|
||||||
|
|
|
@ -86,11 +86,13 @@ def extract_info(monitor_data, monitor_attr: str) -> ct.InfoFlow | None: # noqa
|
||||||
if xarr is None:
|
if xarr is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mk_idx_array(axis: str) -> ct.ArrayFlow:
|
def mk_idx_array(axis: str) -> ct.RangeFlow | ct.ArrayFlow:
|
||||||
return ct.ArrayFlow(
|
return ct.RangeFlow.try_from_array(
|
||||||
values=xarr.get_index(axis).values,
|
ct.ArrayFlow(
|
||||||
unit=symbols[axis].unit,
|
values=xarr.get_index(axis).values,
|
||||||
is_sorted=True,
|
unit=symbols[axis].unit,
|
||||||
|
is_sorted=True,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Compute InfoFlow from XArray
|
# Compute InfoFlow from XArray
|
||||||
|
|
|
@ -124,12 +124,12 @@ class FilterOperation(enum.StrEnum):
|
||||||
# - Computed Properties
|
# - Computed Properties
|
||||||
####################
|
####################
|
||||||
@property
|
@property
|
||||||
def func_args(self) -> list[spux.MathType]:
|
def func_args(self) -> list[sim_symbols.SimSymbol]:
|
||||||
FO = FilterOperation
|
FO = FilterOperation
|
||||||
return {
|
return {
|
||||||
# Pin
|
# Pin
|
||||||
FO.Pin: [spux.MathType.Integer],
|
FO.Pin: [sim_symbols.idx(None)],
|
||||||
FO.PinIdx: [spux.MathType.Integer],
|
FO.PinIdx: [sim_symbols.idx(None)],
|
||||||
}.get(self, [])
|
}.get(self, [])
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -155,10 +155,10 @@ class FilterOperation(enum.StrEnum):
|
||||||
match self:
|
match self:
|
||||||
# Slice
|
# Slice
|
||||||
case FO.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:
|
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
|
# Pin
|
||||||
case FO.PinLen1:
|
case FO.PinLen1:
|
||||||
|
@ -272,10 +272,15 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
# - Properties: Expr InfoFlow
|
# - Properties: Expr InfoFlow
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name={'Expr'},
|
socket_name={'Expr'},
|
||||||
|
# Loaded
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
||||||
input_sockets_optional={'Expr': True},
|
input_sockets_optional={'Expr': True},
|
||||||
|
# Flow
|
||||||
|
## -> See docs in TransformMathNode
|
||||||
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
||||||
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
||||||
|
@ -593,11 +598,17 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
pinned_value, require_sorted=True
|
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
|
# Pin by-Index
|
||||||
if props['operation'] is FilterOperation.PinIdx and has_pinned_axis:
|
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
|
return params
|
||||||
|
|
||||||
|
|
|
@ -236,7 +236,7 @@ class MapOperation(enum.StrEnum):
|
||||||
MO.Sinc: lambda expr: sp.sinc(expr),
|
MO.Sinc: lambda expr: sp.sinc(expr),
|
||||||
# By Vector
|
# By Vector
|
||||||
# Vector -> Number
|
# Vector -> Number
|
||||||
MO.Norm2: lambda expr: sp.sqrt(expr.T @ expr),
|
MO.Norm2: lambda expr: sp.sqrt(expr.T @ expr)[0],
|
||||||
# By Matrix
|
# By Matrix
|
||||||
# Matrix -> Number
|
# Matrix -> Number
|
||||||
MO.Det: lambda expr: sp.det(expr),
|
MO.Det: lambda expr: sp.det(expr),
|
||||||
|
@ -467,10 +467,15 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name={'Expr'},
|
socket_name={'Expr'},
|
||||||
|
# Loaded
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
||||||
input_sockets_optional={'Expr': True},
|
input_sockets_optional={'Expr': True},
|
||||||
|
# Flow
|
||||||
|
## -> See docs in TransformMathNode
|
||||||
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
||||||
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
||||||
|
|
|
@ -210,7 +210,7 @@ class BinaryOperation(enum.StrEnum):
|
||||||
):
|
):
|
||||||
ops += [BO.Cross]
|
ops += [BO.Cross]
|
||||||
|
|
||||||
return ops
|
return ops_el_el + ops
|
||||||
|
|
||||||
## Vector | Matrix
|
## Vector | Matrix
|
||||||
case (1, 2):
|
case (1, 2):
|
||||||
|
@ -374,10 +374,15 @@ class OperateMathNode(base.MaxwellSimNode):
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name={'Expr L', 'Expr R'},
|
socket_name={'Expr L', 'Expr R'},
|
||||||
|
# Loaded
|
||||||
input_sockets={'Expr L', 'Expr R'},
|
input_sockets={'Expr L', 'Expr R'},
|
||||||
input_socket_kinds={'Expr L': ct.FlowKind.Info, 'Expr R': ct.FlowKind.Info},
|
input_socket_kinds={'Expr L': ct.FlowKind.Info, 'Expr R': ct.FlowKind.Info},
|
||||||
input_sockets_optional={'Expr L': True, 'Expr R': True},
|
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
|
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
||||||
has_info_l = not ct.FlowSignal.check(input_sockets['Expr L'])
|
has_info_l = not ct.FlowSignal.check(input_sockets['Expr L'])
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
"""Declares `TransformMathNode`."""
|
"""Declares `TransformMathNode`."""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
import functools
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
@ -39,13 +38,25 @@ log = logger.get(__name__)
|
||||||
# - Operation Enum
|
# - Operation Enum
|
||||||
####################
|
####################
|
||||||
class TransformOperation(enum.StrEnum):
|
class TransformOperation(enum.StrEnum):
|
||||||
"""Valid operations for the `MapMathNode`.
|
"""Valid operations for the `TransformMathNode`.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
FreqToVacWL: Transform frequency axes to be indexed by vacuum wavelength.
|
FreqToVacWL: Transform an frequency dimension to vacuum wavelength.
|
||||||
VacWLToFreq: Transform vacuum wavelength axes to be indexed by frequency.
|
VacWLToFreq: Transform a vacuum wavelength dimension to frequency.
|
||||||
FFT: Compute the fourier transform of the input expression.
|
ConvertIdxUnit: Convert the unit of a dimension to a compatible unit.
|
||||||
InvFFT: Compute the inverse fourier transform of the input expression.
|
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
|
# Covariant Transform
|
||||||
|
@ -79,7 +90,7 @@ class TransformOperation(enum.StrEnum):
|
||||||
TO.VacWLToFreq: 'λᵥ → 𝑓',
|
TO.VacWLToFreq: 'λᵥ → 𝑓',
|
||||||
TO.ConvertIdxUnit: 'Convert Dim',
|
TO.ConvertIdxUnit: 'Convert Dim',
|
||||||
TO.SetIdxUnit: 'Set Dim',
|
TO.SetIdxUnit: 'Set Dim',
|
||||||
TO.FirstColToFirstIdx: '1st Col → Dim',
|
TO.FirstColToFirstIdx: '1st Col → 1st Dim',
|
||||||
# Fold
|
# Fold
|
||||||
TO.IntDimToComplex: '→ ℂ',
|
TO.IntDimToComplex: '→ ℂ',
|
||||||
TO.DimToVec: '→ Vector',
|
TO.DimToVec: '→ Vector',
|
||||||
|
@ -87,10 +98,14 @@ class TransformOperation(enum.StrEnum):
|
||||||
## TODO: Vector to new last-dim integer
|
## TODO: Vector to new last-dim integer
|
||||||
## TODO: Matrix to two last-dim integers
|
## TODO: Matrix to two last-dim integers
|
||||||
# Fourier
|
# Fourier
|
||||||
TO.FT1D: '→ 𝑓',
|
TO.FT1D: 'FT',
|
||||||
TO.InvFT1D: '𝑓 →',
|
TO.InvFT1D: 'iFT',
|
||||||
}[value]
|
}[value]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return TransformOperation.to_name(self)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_icon(_: typ.Self) -> str:
|
def to_icon(_: typ.Self) -> str:
|
||||||
return ''
|
return ''
|
||||||
|
@ -108,49 +123,32 @@ class TransformOperation(enum.StrEnum):
|
||||||
####################
|
####################
|
||||||
# - Methods
|
# - 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]:
|
def valid_dims(self, info: ct.InfoFlow) -> list[typ.Self]:
|
||||||
TO = TransformOperation
|
TO = TransformOperation
|
||||||
match self:
|
match self:
|
||||||
case TO.FreqToVacWL | TO.FT1D:
|
case TO.FreqToVacWL:
|
||||||
return [
|
return [
|
||||||
dim
|
dim
|
||||||
for dim in info.dims
|
for dim in info.dims
|
||||||
if dim.physical_type is spux.PhysicalType.Freq
|
if dim.physical_type is spux.PhysicalType.Freq
|
||||||
]
|
]
|
||||||
|
|
||||||
case TO.VacWLToFreq | TO.InvFT1D:
|
case TO.VacWLToFreq:
|
||||||
return [
|
return [
|
||||||
dim
|
dim
|
||||||
for dim in info.dims
|
for dim in info.dims
|
||||||
if dim.physical_type is spux.PhysicalType.Length
|
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)]
|
return [dim for dim in info.dims if not info.has_idx_labels(dim)]
|
||||||
|
|
||||||
## ColDimToComplex: Implicit Last Dimension
|
## ColDimToComplex: Implicit Last Dimension
|
||||||
|
@ -198,13 +196,11 @@ class TransformOperation(enum.StrEnum):
|
||||||
# Fold
|
# Fold
|
||||||
## Last Dim -> Complex
|
## Last Dim -> Complex
|
||||||
if (
|
if (
|
||||||
info.dims
|
len(info.dims) >= 1
|
||||||
# Output is Int|Rat|Real
|
|
||||||
and (
|
and (
|
||||||
info.output.mathtype
|
info.output.mathtype
|
||||||
in [spux.MathType.Integer, spux.MathType.Rational, spux.MathType.Real]
|
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.last_dim.mathtype is spux.MathType.Integer
|
||||||
and info.has_idx_labels(info.last_dim)
|
and info.has_idx_labels(info.last_dim)
|
||||||
and len(info.dims[info.last_dim]) == 2 # noqa: PLR2004
|
and len(info.dims[info.last_dim]) == 2 # noqa: PLR2004
|
||||||
|
@ -231,14 +227,13 @@ class TransformOperation(enum.StrEnum):
|
||||||
####################
|
####################
|
||||||
# - Function Properties
|
# - Function Properties
|
||||||
####################
|
####################
|
||||||
@functools.cached_property
|
def jax_func(self, axis: int | None = None):
|
||||||
def jax_func(self):
|
|
||||||
TO = TransformOperation
|
TO = TransformOperation
|
||||||
return {
|
return {
|
||||||
# Covariant Transform
|
# Covariant Transform
|
||||||
## -> Freq <-> WL is a rescale (noop) AND flip (not noop).
|
## -> Freq <-> WL is a rescale (noop) AND flip (not noop).
|
||||||
TO.FreqToVacWL: lambda expr, axis: jnp.flip(expr, axis=axis),
|
TO.FreqToVacWL: lambda expr: jnp.flip(expr, axis=axis),
|
||||||
TO.VacWLToFreq: lambda expr, axis: jnp.flip(expr, axis=axis),
|
TO.VacWLToFreq: lambda expr: jnp.flip(expr, axis=axis),
|
||||||
TO.ConvertIdxUnit: lambda expr: expr,
|
TO.ConvertIdxUnit: lambda expr: expr,
|
||||||
TO.SetIdxUnit: lambda expr: expr,
|
TO.SetIdxUnit: lambda expr: expr,
|
||||||
TO.FirstColToFirstIdx: lambda expr: jnp.delete(expr, 0, axis=1),
|
TO.FirstColToFirstIdx: lambda expr: jnp.delete(expr, 0, axis=1),
|
||||||
|
@ -250,8 +245,8 @@ class TransformOperation(enum.StrEnum):
|
||||||
TO.DimToVec: lambda expr: expr,
|
TO.DimToVec: lambda expr: expr,
|
||||||
TO.DimsToMat: lambda expr: expr,
|
TO.DimsToMat: lambda expr: expr,
|
||||||
# Fourier
|
# Fourier
|
||||||
TO.FT1D: lambda expr, axis: jnp.fft(expr, axis=axis),
|
TO.FT1D: lambda expr: jnp.fft(expr, axis=axis),
|
||||||
TO.InvFT1D: lambda expr, axis: jnp.ifft(expr, axis=axis),
|
TO.InvFT1D: lambda expr: jnp.ifft(expr, axis=axis),
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
def transform_info(
|
def transform_info(
|
||||||
|
@ -268,25 +263,21 @@ class TransformOperation(enum.StrEnum):
|
||||||
# Covariant Transform
|
# Covariant Transform
|
||||||
TO.FreqToVacWL: lambda: info.replace_dim(
|
TO.FreqToVacWL: lambda: info.replace_dim(
|
||||||
(f_dim := dim),
|
(f_dim := dim),
|
||||||
[
|
sim_symbols.wl(unit),
|
||||||
sim_symbols.wl(unit),
|
info.dims[f_dim].rescale(
|
||||||
info.dims[f_dim].rescale(
|
lambda el: sci_constants.vac_speed_of_light / el,
|
||||||
lambda el: sci_constants.vac_speed_of_light / el,
|
reverse=True,
|
||||||
reverse=True,
|
new_unit=unit,
|
||||||
new_unit=unit,
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
TO.VacWLToFreq: lambda: info.replace_dim(
|
TO.VacWLToFreq: lambda: info.replace_dim(
|
||||||
(wl_dim := dim),
|
(wl_dim := dim),
|
||||||
[
|
sim_symbols.freq(unit),
|
||||||
sim_symbols.freq(unit),
|
info.dims[wl_dim].rescale(
|
||||||
info.dims[wl_dim].rescale(
|
lambda el: sci_constants.vac_speed_of_light / el,
|
||||||
lambda el: sci_constants.vac_speed_of_light / el,
|
reverse=True,
|
||||||
reverse=True,
|
new_unit=unit,
|
||||||
new_unit=unit,
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
TO.ConvertIdxUnit: lambda: info.replace_dim(
|
TO.ConvertIdxUnit: lambda: info.replace_dim(
|
||||||
dim,
|
dim,
|
||||||
|
@ -300,7 +291,9 @@ class TransformOperation(enum.StrEnum):
|
||||||
TO.SetIdxUnit: lambda: info.replace_dim(
|
TO.SetIdxUnit: lambda: info.replace_dim(
|
||||||
dim,
|
dim,
|
||||||
dim.update(
|
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)
|
info.dims[dim].correct_unit(unit)
|
||||||
|
@ -311,10 +304,12 @@ class TransformOperation(enum.StrEnum):
|
||||||
TO.FirstColToFirstIdx: lambda: info.replace_dim(
|
TO.FirstColToFirstIdx: lambda: info.replace_dim(
|
||||||
info.first_dim,
|
info.first_dim,
|
||||||
info.first_dim.update(
|
info.first_dim.update(
|
||||||
|
sym_name=new_dim_name,
|
||||||
mathtype=spux.MathType.from_jax_array(data_col),
|
mathtype=spux.MathType.from_jax_array(data_col),
|
||||||
|
physical_type=physical_type,
|
||||||
unit=unit,
|
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)),
|
).slice_dim(info.last_dim, (1, len(info.dims[info.last_dim]), 1)),
|
||||||
# Fold
|
# Fold
|
||||||
TO.IntDimToComplex: lambda: info.delete_dim(info.last_dim).update_output(
|
TO.IntDimToComplex: lambda: info.delete_dim(info.last_dim).update_output(
|
||||||
|
@ -380,10 +375,18 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
# - Properties: Expr InfoFlow
|
# - Properties: Expr InfoFlow
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name={'Expr'},
|
socket_name={'Expr'},
|
||||||
|
# Loaded
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
||||||
input_sockets_optional={'Expr': True},
|
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
|
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
||||||
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
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'})
|
@bl_cache.cached_bl_property(depends_on={'expr_info', 'active_dim'})
|
||||||
def dim(self) -> sim_symbols.SimSymbol | None:
|
def dim(self) -> sim_symbols.SimSymbol | None:
|
||||||
if self.expr_info is not None and self.active_dim is not 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
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -454,48 +457,52 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
active_new_unit: enum.StrEnum = bl_cache.BLField(
|
active_new_unit: enum.StrEnum = bl_cache.BLField(
|
||||||
enum_cb=lambda self, _: self.search_units(),
|
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]:
|
def search_units(self) -> list[ct.BLEnumElement]:
|
||||||
if self.dim is not None:
|
TO = TransformOperation
|
||||||
if self.dim.physical_type is not spux.PhysicalType.NonPhysical:
|
match self.operation:
|
||||||
unit_name = sp.sstr(self.dim.unit)
|
# Covariant Transform
|
||||||
return [
|
case TO.ConvertIdxUnit if self.dim is not None:
|
||||||
(
|
physical_type = spux.PhysicalType.from_unit(
|
||||||
sp.sstr(unit),
|
self.dim.unit, optional=True
|
||||||
spux.sp_to_str(unit),
|
|
||||||
sp.sstr(unit),
|
|
||||||
'',
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
for unit in self.dim.physical_type.valid_units
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.dim.unit is not None:
|
|
||||||
unit_name = sp.sstr(self.dim.unit)
|
|
||||||
return [
|
|
||||||
(
|
|
||||||
unit_name,
|
|
||||||
spux.sp_to_str(self.dim.unit),
|
|
||||||
unit_name,
|
|
||||||
'',
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
]
|
|
||||||
if self.new_physical_type is not spux.PhysicalType.NonPhysical:
|
|
||||||
return [
|
|
||||||
(
|
|
||||||
sp.sstr(unit),
|
|
||||||
spux.sp_to_str(unit),
|
|
||||||
sp.sstr(unit),
|
|
||||||
'',
|
|
||||||
i,
|
|
||||||
)
|
)
|
||||||
for i, unit in enumerate(self.new_physical_type.valid_units)
|
if physical_type is not None:
|
||||||
]
|
valid_units = physical_type.valid_units
|
||||||
|
else:
|
||||||
|
valid_units = []
|
||||||
|
|
||||||
return []
|
case TO.FreqToVacWL if self.dim is not None:
|
||||||
|
valid_units = spux.PhysicalType.Length.valid_units
|
||||||
|
|
||||||
|
case TO.VacWLToFreq if self.dim is not None:
|
||||||
|
valid_units = spux.PhysicalType.Freq.valid_units
|
||||||
|
|
||||||
|
case TO.SetIdxUnit if (
|
||||||
|
self.dim is not None
|
||||||
|
and self.new_physical_type is not spux.PhysicalType.NonPhysical
|
||||||
|
):
|
||||||
|
valid_units = self.new_physical_type.valid_units
|
||||||
|
|
||||||
|
case TO.FirstColToFirstIdx if (
|
||||||
|
self.new_physical_type is not spux.PhysicalType.NonPhysical
|
||||||
|
):
|
||||||
|
valid_units = self.new_physical_type.valid_units
|
||||||
|
|
||||||
|
case _:
|
||||||
|
valid_units = []
|
||||||
|
|
||||||
|
return [
|
||||||
|
(
|
||||||
|
sp.sstr(unit),
|
||||||
|
spux.sp_to_str(unit),
|
||||||
|
sp.sstr(unit),
|
||||||
|
'',
|
||||||
|
i,
|
||||||
|
)
|
||||||
|
for i, unit in enumerate(valid_units)
|
||||||
|
]
|
||||||
|
|
||||||
@bl_cache.cached_bl_property(depends_on={'active_new_unit'})
|
@bl_cache.cached_bl_property(depends_on={'active_new_unit'})
|
||||||
def new_unit(self) -> spux.Unit:
|
def new_unit(self) -> spux.Unit:
|
||||||
|
@ -507,30 +514,85 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
def draw_label(self):
|
@bl_cache.cached_bl_property(depends_on={'new_unit'})
|
||||||
if self.operation is not None:
|
def new_unit_str(self) -> str:
|
||||||
return 'T: ' + TransformOperation.to_name(self.operation)
|
if self.new_unit is None:
|
||||||
|
return ''
|
||||||
|
return spux.sp_to_str(self.new_unit)
|
||||||
|
|
||||||
return self.bl_label
|
def draw_label(self):
|
||||||
|
TO = TransformOperation
|
||||||
|
match self.operation:
|
||||||
|
case TO.FreqToVacWL if self.dim is not None:
|
||||||
|
return f'T: {self.dim.name_pretty} | 𝑓 → {self.new_unit_str}'
|
||||||
|
|
||||||
|
case TO.VacWLToFreq if self.dim is not None:
|
||||||
|
return f'T: {self.dim.name_pretty} | λᵥ → {self.new_unit_str}'
|
||||||
|
|
||||||
|
case TO.ConvertIdxUnit if self.dim is not None:
|
||||||
|
return f'T: {self.dim.name_pretty} → {self.new_unit_str}'
|
||||||
|
|
||||||
|
case TO.SetIdxUnit if self.dim is not None:
|
||||||
|
return f'T: {self.dim.name_pretty} → {self.new_name.name_pretty} | {self.new_unit_str}'
|
||||||
|
|
||||||
|
case (
|
||||||
|
TO.IntDimToComplex
|
||||||
|
| TO.DimToVec
|
||||||
|
| TO.DimsToMat
|
||||||
|
) if self.expr_info is not None and self.expr_info.dims:
|
||||||
|
return f'T: {self.expr_info.last_dim.name_unit_label} {self.operation.name}'
|
||||||
|
|
||||||
|
case TO.FT1D if self.dim is not None:
|
||||||
|
return f'T: FT[{self.dim.name_unit_label}]'
|
||||||
|
|
||||||
|
case TO.InvFT1D if self.dim is not None:
|
||||||
|
return f'T: iFT[{self.dim.name_unit_label}]'
|
||||||
|
|
||||||
|
case _:
|
||||||
|
if self.operation is not None:
|
||||||
|
return f'T: {self.operation.name}'
|
||||||
|
return self.bl_label
|
||||||
|
|
||||||
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||||
layout.prop(self, self.blfields['operation'], text='')
|
layout.prop(self, self.blfields['operation'], text='')
|
||||||
|
|
||||||
if self.operation is not None and self.operation.num_dim_inputs == 1:
|
TO = TransformOperation
|
||||||
TO = TransformOperation
|
match self.operation:
|
||||||
layout.prop(self, self.blfields['active_dim'], text='')
|
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)
|
col = layout.column(align=True)
|
||||||
if self.operation is TransformOperation.ConvertIdxUnit:
|
row = col.row(align=True)
|
||||||
col.prop(self, self.blfields['active_new_unit'], text='')
|
row.prop(self, self.blfields['new_name'], text='')
|
||||||
|
row.prop(self, self.blfields['active_new_unit'], text='')
|
||||||
|
|
||||||
if self.operation is TransformOperation.SetIdxUnit:
|
row = col.row(align=True)
|
||||||
col.prop(self, self.blfields['new_physical_type'], text='')
|
row.prop(self, self.blfields['new_physical_type'], text='')
|
||||||
|
|
||||||
row = col.row(align=True)
|
case TO.FT1D | TO.InvFT1D:
|
||||||
row.prop(self, self.blfields['new_name'], text='')
|
layout.prop(self, self.blfields['active_dim'], text='')
|
||||||
row.prop(self, self.blfields['active_new_unit'], text='')
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Compute: Func / Array
|
# - Compute: Func / Array
|
||||||
|
@ -538,23 +600,43 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Expr',
|
'Expr',
|
||||||
kind=ct.FlowKind.Func,
|
kind=ct.FlowKind.Func,
|
||||||
props={'operation'},
|
# Loaded
|
||||||
|
props={'operation', 'dim'},
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={
|
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:
|
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']
|
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)
|
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:
|
||||||
return lazy_func.compose_within(
|
# Retrieve Properties
|
||||||
operation.jax_func,
|
dim = props['dim']
|
||||||
supports_jax=True,
|
|
||||||
)
|
# Match Pattern by Operation
|
||||||
|
match operation:
|
||||||
|
case TO.FreqToVacWL | TO.VacWLToFreq | TO.FT1D | TO.InvFT1D:
|
||||||
|
if dim is not None and info.has_idx_discrete(dim):
|
||||||
|
return lazy_func.compose_within(
|
||||||
|
operation.jax_func(axis=info.dim_axis(dim)),
|
||||||
|
supports_jax=True,
|
||||||
|
)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
|
case _:
|
||||||
|
return lazy_func.compose_within(
|
||||||
|
operation.jax_func(),
|
||||||
|
supports_jax=True,
|
||||||
|
)
|
||||||
|
|
||||||
return ct.FlowSignal.FlowPending
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -563,54 +645,101 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Expr',
|
'Expr',
|
||||||
kind=ct.FlowKind.Info,
|
kind=ct.FlowKind.Info,
|
||||||
|
# Loaded
|
||||||
props={'operation', 'dim', 'new_name', 'new_unit', 'new_physical_type'},
|
props={'operation', 'dim', 'new_name', 'new_unit', 'new_physical_type'},
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={
|
input_socket_kinds={
|
||||||
'Expr': {ct.FlowKind.Func, ct.FlowKind.Info, ct.FlowKind.Params}
|
'Expr': {ct.FlowKind.Func, ct.FlowKind.Info, ct.FlowKind.Params}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def compute_info(
|
def compute_info( # noqa: PLR0911
|
||||||
self, props: dict, input_sockets: dict
|
self, props: dict, input_sockets: dict
|
||||||
) -> ct.InfoFlow | typ.Literal[ct.FlowSignal.FlowPending]:
|
) -> ct.InfoFlow | typ.Literal[ct.FlowSignal.FlowPending]:
|
||||||
|
"""Transform the input `InfoFlow` depending on the transform operation."""
|
||||||
|
TO = TransformOperation
|
||||||
operation = props['operation']
|
operation = props['operation']
|
||||||
info = input_sockets['Expr'][ct.FlowKind.Info]
|
info = input_sockets['Expr'][ct.FlowKind.Info]
|
||||||
|
|
||||||
has_info = not ct.FlowSignal.check(info)
|
has_info = not ct.FlowSignal.check(info)
|
||||||
|
|
||||||
dim = props['dim']
|
|
||||||
new_name = props['new_name']
|
|
||||||
new_unit = props['new_unit']
|
|
||||||
new_physical_type = props['new_physical_type']
|
|
||||||
if has_info and operation is not None:
|
if has_info and operation is not None:
|
||||||
# First Column to First Index
|
# Retrieve Properties
|
||||||
## -> We have to evaluate the lazy function at this point.
|
dim = props['dim']
|
||||||
## -> It's the only way to get at the column data.
|
new_name = props['new_name']
|
||||||
if operation is TransformOperation.FirstColToFirstIdx:
|
new_unit = props['new_unit']
|
||||||
lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
|
new_physical_type = props['new_physical_type']
|
||||||
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:
|
# Retrieve Expression Data
|
||||||
|
lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
|
||||||
|
params = input_sockets['Expr'][ct.FlowKind.Params]
|
||||||
|
|
||||||
|
has_lazy_func = not ct.FlowSignal.check(lazy_func)
|
||||||
|
has_params = not ct.FlowSignal.check(lazy_func)
|
||||||
|
|
||||||
|
# Match Pattern by Operation
|
||||||
|
match operation:
|
||||||
|
# Covariant Transform
|
||||||
|
## -> Needs: Dim, Unit
|
||||||
|
case TO.ConvertIdxUnit if dim is not None and new_unit is not None:
|
||||||
|
physical_type = spux.PhysicalType.from_unit(dim.unit, optional=True)
|
||||||
|
if (
|
||||||
|
physical_type is not None
|
||||||
|
and new_unit in physical_type.valid_units
|
||||||
|
):
|
||||||
|
return operation.transform_info(info, dim=dim, unit=new_unit)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
|
case TO.FreqToVacWL if dim is not None and new_unit is not None and new_unit in spux.PhysicalType.Length.valid_units:
|
||||||
|
return operation.transform_info(info, dim=dim, unit=new_unit)
|
||||||
|
|
||||||
|
case TO.VacWLToFreq if dim is not None and new_unit is not None and new_unit in spux.PhysicalType.Freq.valid_units:
|
||||||
|
return operation.transform_info(info, dim=dim, unit=new_unit)
|
||||||
|
|
||||||
|
## -> Needs: Dim, Unit, Physical Type
|
||||||
|
case TO.SetIdxUnit if (
|
||||||
|
dim is not None
|
||||||
|
and new_physical_type is not None
|
||||||
|
and new_unit in new_physical_type.valid_units
|
||||||
|
):
|
||||||
|
return operation.transform_info(
|
||||||
|
info,
|
||||||
|
dim=dim,
|
||||||
|
new_dim_name=new_name,
|
||||||
|
unit=new_unit,
|
||||||
|
physical_type=new_physical_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
## -> Needs: Data Column, Name, Unit, Physical Type
|
||||||
|
## -> We have to evaluate the lazy function at this point.
|
||||||
|
## -> It's the only way to get at the column's data.
|
||||||
|
case TO.FirstColToFirstIdx if (
|
||||||
|
has_lazy_func
|
||||||
|
and has_params
|
||||||
|
and not params.symbols
|
||||||
|
and new_name is not None
|
||||||
|
and new_physical_type is not None
|
||||||
|
and new_unit in new_physical_type.valid_units
|
||||||
|
):
|
||||||
data = lazy_func.realize(params)
|
data = lazy_func.realize(params)
|
||||||
if data.shape is not None and len(data.shape) == 2:
|
if data.shape is not None and len(data.shape) == 2: # noqa: PLR2004
|
||||||
data_col = data[:, 0]
|
data_col = data[:, 0]
|
||||||
return operation.transform_info(info, data_col=data_col)
|
return operation.transform_info(
|
||||||
return ct.FlowSignal.FlowPending
|
info,
|
||||||
|
new_dim_name=new_name,
|
||||||
|
data_col=data_col,
|
||||||
|
unit=new_unit,
|
||||||
|
physical_type=new_physical_type,
|
||||||
|
)
|
||||||
|
|
||||||
# Check Not-Yet-Updated Dimension
|
# Fold
|
||||||
## - Operation changes before dimensions.
|
## -> Needs: Nothing
|
||||||
## - If InfoFlow is requested in this interim, big problem.
|
case TO.IntDimToComplex | TO.DimToVec | TO.DimsToMat:
|
||||||
if dim is None and operation.num_dim_inputs > 0:
|
return operation.transform_info(info)
|
||||||
return ct.FlowSignal.FlowPending
|
|
||||||
|
# Fourier
|
||||||
|
## -> Needs: Dimension
|
||||||
|
case TO.FT1D | TO.InvFT1D if dim is not None:
|
||||||
|
return operation.transform_info(info, dim=dim)
|
||||||
|
|
||||||
return operation.transform_info(
|
|
||||||
info,
|
|
||||||
dim=dim,
|
|
||||||
new_dim_name=new_name,
|
|
||||||
unit=new_unit,
|
|
||||||
physical_type=new_physical_type,
|
|
||||||
)
|
|
||||||
return ct.FlowSignal.FlowPending
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -619,30 +748,19 @@ class TransformMathNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Expr',
|
'Expr',
|
||||||
kind=ct.FlowKind.Params,
|
kind=ct.FlowKind.Params,
|
||||||
props={'operation', 'dim'},
|
# Loaded
|
||||||
|
props={'operation'},
|
||||||
input_sockets={'Expr'},
|
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:
|
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']
|
operation = props['operation']
|
||||||
dim = props['dim']
|
params = input_sockets['Expr']
|
||||||
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])
|
|
||||||
|
|
||||||
|
has_params = not ct.FlowSignal.check(params)
|
||||||
|
if has_params and operation is not None:
|
||||||
return params
|
return params
|
||||||
|
|
||||||
return ct.FlowSignal.FlowPending
|
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."""
|
"""Given the input `InfoFlow`, deduce which visualization modes are valid to use with the described data."""
|
||||||
Z = spux.MathType.Integer
|
Z = spux.MathType.Integer
|
||||||
R = spux.MathType.Real
|
R = spux.MathType.Real
|
||||||
|
C = spux.MathType.Complex
|
||||||
VM = VizMode
|
VM = VizMode
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -115,6 +116,9 @@ class VizMode(enum.StrEnum):
|
||||||
VM.Points2D,
|
VM.Points2D,
|
||||||
VM.Bar,
|
VM.Bar,
|
||||||
],
|
],
|
||||||
|
((R,), (1, 1, C)): [
|
||||||
|
VM.Curve2D,
|
||||||
|
],
|
||||||
((R, Z), (1, 1, R)): [
|
((R, Z), (1, 1, R)): [
|
||||||
VM.Curves2D,
|
VM.Curves2D,
|
||||||
VM.FilledCurves2D,
|
VM.FilledCurves2D,
|
||||||
|
@ -231,10 +235,15 @@ class VizNode(base.MaxwellSimNode):
|
||||||
## - Properties
|
## - Properties
|
||||||
#####################
|
#####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name={'Expr'},
|
socket_name={'Expr'},
|
||||||
|
# Loaded
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
input_socket_kinds={'Expr': ct.FlowKind.Info},
|
||||||
input_sockets_optional={'Expr': True},
|
input_sockets_optional={'Expr': True},
|
||||||
|
# Flow
|
||||||
|
## -> See docs in TransformMathNode
|
||||||
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
||||||
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
has_info = not ct.FlowSignal.check(input_sockets['Expr'])
|
||||||
|
@ -326,7 +335,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
if self.viz_target is VizTarget.Plot2D:
|
if self.viz_target is VizTarget.Plot2D:
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.alignment = 'CENTER'
|
row.alignment = 'CENTER'
|
||||||
row.label(text='Width/Height/DPI')
|
row.label(text='Width | Height | DPI')
|
||||||
|
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(self, self.blfields['plot_width'], text='')
|
row.prop(self, self.blfields['plot_width'], text='')
|
||||||
|
@ -339,8 +348,10 @@ class VizNode(base.MaxwellSimNode):
|
||||||
# - Events
|
# - Events
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name='Expr',
|
socket_name='Expr',
|
||||||
run_on_init=True,
|
run_on_init=True,
|
||||||
|
# Loaded
|
||||||
input_sockets={'Expr'},
|
input_sockets={'Expr'},
|
||||||
input_socket_kinds={'Expr': {ct.FlowKind.Info, ct.FlowKind.Params}},
|
input_socket_kinds={'Expr': {ct.FlowKind.Info, ct.FlowKind.Params}},
|
||||||
input_sockets_optional={'Expr': True},
|
input_sockets_optional={'Expr': True},
|
||||||
|
@ -355,14 +366,19 @@ class VizNode(base.MaxwellSimNode):
|
||||||
# Declare Loose Sockets that Realize Symbols
|
# Declare Loose Sockets that Realize Symbols
|
||||||
## -> This happens if Params contains not-yet-realized symbols.
|
## -> This happens if Params contains not-yet-realized symbols.
|
||||||
if has_info and has_params and params.symbols:
|
if has_info and has_params and params.symbols:
|
||||||
if set(self.loose_input_sockets) != {
|
if set(self.loose_input_sockets) != {sym.name for sym in params.symbols}:
|
||||||
sym.name for sym in params.symbols if sym in info.dims
|
|
||||||
}:
|
|
||||||
self.loose_input_sockets = {
|
self.loose_input_sockets = {
|
||||||
dim_name: sockets.ExprSocketDef(**expr_info)
|
sym.name: sockets.ExprSocketDef(
|
||||||
for dim_name, expr_info in params.sym_expr_infos(
|
**(
|
||||||
use_range=True
|
expr_info
|
||||||
).items()
|
| {
|
||||||
|
'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:
|
elif self.loose_input_sockets:
|
||||||
|
@ -373,9 +389,10 @@ class VizNode(base.MaxwellSimNode):
|
||||||
#####################
|
#####################
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Preview',
|
'Preview',
|
||||||
kind=ct.FlowKind.Value,
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
props={
|
props={
|
||||||
|
'sim_node_name',
|
||||||
'viz_mode',
|
'viz_mode',
|
||||||
'viz_target',
|
'viz_target',
|
||||||
'colormap',
|
'colormap',
|
||||||
|
@ -391,7 +408,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
def compute_dummy_value(self, props, input_sockets, loose_input_sockets):
|
def compute_dummy_value(self, props, input_sockets, loose_input_sockets):
|
||||||
"""Needed for the plot to regenerate in the viewer."""
|
"""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
|
## - On Show Plot
|
||||||
|
@ -416,6 +433,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
def on_show_plot(
|
def on_show_plot(
|
||||||
self, managed_objs, props, input_sockets, loose_input_sockets
|
self, managed_objs, props, input_sockets, loose_input_sockets
|
||||||
) -> None:
|
) -> None:
|
||||||
|
log.critical('Show Plot (too many times)')
|
||||||
lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
|
lazy_func = input_sockets['Expr'][ct.FlowKind.Func]
|
||||||
info = input_sockets['Expr'][ct.FlowKind.Info]
|
info = input_sockets['Expr'][ct.FlowKind.Info]
|
||||||
params = input_sockets['Expr'][ct.FlowKind.Params]
|
params = input_sockets['Expr'][ct.FlowKind.Params]
|
||||||
|
@ -427,23 +445,17 @@ class VizNode(base.MaxwellSimNode):
|
||||||
viz_mode = props['viz_mode']
|
viz_mode = props['viz_mode']
|
||||||
viz_target = props['viz_target']
|
viz_target = props['viz_target']
|
||||||
if has_info and has_params and viz_mode is not None and viz_target is not None:
|
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.
|
## -> The loose input socket values are user-selected symbol values.
|
||||||
## -> These expressions are used to realize the lazy data.
|
## -> These are used to get rid of symbols in the ParamsFlow.
|
||||||
## -> `.realize()` ensures all ex. units are correctly conformed.
|
## -> What's left is a dictionary from SimSymbol -> Data
|
||||||
realized_syms = {
|
data = lazy_func.realize_as_data(
|
||||||
sym: loose_input_sockets[sym.name] for sym in params.sorted_symbols
|
info,
|
||||||
}
|
params,
|
||||||
output_data = lazy_func.realize(params, symbol_values=realized_syms)
|
symbol_values={
|
||||||
|
sym: loose_input_sockets[sym.name] for sym in params.sorted_symbols
|
||||||
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
|
# Match Viz Type & Perform Visualization
|
||||||
## -> Viz Target determines how to plot.
|
## -> Viz Target determines how to plot.
|
||||||
|
@ -459,7 +471,6 @@ class VizNode(base.MaxwellSimNode):
|
||||||
width_inches=plot_width,
|
width_inches=plot_width,
|
||||||
height_inches=plot_height,
|
height_inches=plot_height,
|
||||||
dpi=plot_dpi,
|
dpi=plot_dpi,
|
||||||
bl_select=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
case VizTarget.Pixels:
|
case VizTarget.Pixels:
|
||||||
|
@ -468,7 +479,6 @@ class VizNode(base.MaxwellSimNode):
|
||||||
plot.map_2d_to_image(
|
plot.map_2d_to_image(
|
||||||
data,
|
data,
|
||||||
colormap=colormap,
|
colormap=colormap,
|
||||||
bl_select=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
case VizTarget.PixelsPlane:
|
case VizTarget.PixelsPlane:
|
||||||
|
|
|
@ -21,6 +21,7 @@ Attributes:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
import functools
|
||||||
import typing as typ
|
import typing as typ
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from types import MappingProxyType
|
from types import MappingProxyType
|
||||||
|
@ -62,7 +63,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
Used as a node-specific cache index.
|
Used as a node-specific cache index.
|
||||||
sim_node_name: A unique human-readable name identifying the node.
|
sim_node_name: A unique human-readable name identifying the node.
|
||||||
Used when naming managed objects and exporting.
|
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.
|
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({})
|
loose_output_sockets: dict[str, sockets.base.SocketDef] = bl_cache.BLField({})
|
||||||
|
|
||||||
# UI Options
|
# UI Options
|
||||||
preview_active: bool = bl_cache.BLField(False)
|
|
||||||
locked: bool = bl_cache.BLField(False, use_prop_update=False)
|
locked: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
|
||||||
# Active Socket Set
|
# Active Socket Set
|
||||||
|
@ -264,35 +263,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
## TODO: Account for FlowKind
|
## TODO: Account for FlowKind
|
||||||
bl_socket.value = socket_value
|
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
|
# - Events: Lock
|
||||||
####################
|
####################
|
||||||
|
@ -521,14 +491,17 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
return {
|
return {
|
||||||
ct.FlowEvent.EnableLock: lambda *_: True,
|
ct.FlowEvent.EnableLock: lambda *_: True,
|
||||||
ct.FlowEvent.DisableLock: 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
|
socket_name
|
||||||
and socket_name in event_method.callback_info.on_changed_sockets
|
and socket_name in event_method.callback_info.on_changed_sockets
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
prop_name
|
prop_names
|
||||||
and prop_name in event_method.callback_info.on_changed_props
|
and any(
|
||||||
|
prop_name in event_method.callback_info.on_changed_props
|
||||||
|
for prop_name in prop_names
|
||||||
|
)
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
socket_name
|
socket_name
|
||||||
|
@ -536,6 +509,7 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
and socket_name in self.loose_input_sockets
|
and socket_name in self.loose_input_sockets
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
# Non-Triggered
|
||||||
ct.FlowEvent.OutputRequested: lambda output_socket_method,
|
ct.FlowEvent.OutputRequested: lambda output_socket_method,
|
||||||
output_socket_name,
|
output_socket_name,
|
||||||
_,
|
_,
|
||||||
|
@ -546,7 +520,6 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
== output_socket_method.callback_info.output_socket_name
|
== output_socket_method.callback_info.output_socket_name
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
ct.FlowEvent.ShowPreview: lambda *_: True,
|
|
||||||
ct.FlowEvent.ShowPlot: 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)
|
bl_socket = self.inputs.get(input_socket_name)
|
||||||
if bl_socket is not None:
|
if bl_socket is not None:
|
||||||
if bl_socket.instance_id:
|
if bl_socket.instance_id:
|
||||||
|
if kind is ct.FlowKind.Previews:
|
||||||
|
return bl_socket.compute_data(kind=kind)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
ct.FlowKind.scale_to_unit_system(
|
ct.FlowKind.scale_to_unit_system(
|
||||||
kind,
|
kind,
|
||||||
|
@ -610,12 +586,10 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
## -> Anyone needing results will need to wait on preinit().
|
## -> Anyone needing results will need to wait on preinit().
|
||||||
return ct.FlowSignal.FlowInitializing
|
return ct.FlowSignal.FlowInitializing
|
||||||
|
|
||||||
# if optional:
|
if kind is ct.FlowKind.Previews:
|
||||||
|
return ct.PreviewsFlow()
|
||||||
return ct.FlowSignal.NoFlow
|
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
|
# - 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
|
The value of the output socket, as computed by the dedicated method
|
||||||
registered using the `@computes_output_socket` decorator.
|
registered using the `@computes_output_socket` decorator.
|
||||||
"""
|
"""
|
||||||
if self.outputs.get(output_socket_name) is None:
|
# Previews: Aggregate All Input Sockets
|
||||||
if optional:
|
## -> All PreviewsFlows on all input sockets are combined.
|
||||||
return None
|
## -> 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"
|
# No Output Socket: No Flow
|
||||||
raise RuntimeError(msg)
|
## -> 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(
|
output_socket_methods = self.filtered_event_methods_by_event(
|
||||||
ct.FlowEvent.OutputRequested,
|
ct.FlowEvent.OutputRequested,
|
||||||
(output_socket_name, None, kind),
|
(output_socket_name, None, kind),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Run (=1) Method
|
# Exactly One Output Socket Method
|
||||||
if output_socket_methods:
|
## -> All PreviewsFlows on all input sockets are combined.
|
||||||
if len(output_socket_methods) > 1:
|
## -> Output Socket Methods can add additional PreviewsFlows.
|
||||||
msg = f'More than one method found for ({output_socket_name}, {kind.value!s}.'
|
if len(output_socket_methods) == 1:
|
||||||
raise RuntimeError(msg)
|
res = output_socket_methods[0](self)
|
||||||
|
|
||||||
return output_socket_methods[0](self)
|
# Res is PreviewsFlow: Concatenate
|
||||||
|
## -> This will add the elements within the returned PreviewsFluw.
|
||||||
|
if kind is ct.FlowKind.Previews and not ct.FlowSignal.check(res):
|
||||||
|
input_previews |= res
|
||||||
|
|
||||||
# Auxiliary Fallbacks
|
return res
|
||||||
|
|
||||||
|
# > One Output Socket Method: Error
|
||||||
|
if len(output_socket_methods) > 1:
|
||||||
|
msg = (
|
||||||
|
f'More than one method found for ({output_socket_name}, {kind.value!s}.'
|
||||||
|
)
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
|
||||||
|
if kind is ct.FlowKind.Previews:
|
||||||
|
return input_previews
|
||||||
return ct.FlowSignal.NoFlow
|
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
|
# - Event Trigger
|
||||||
|
@ -674,11 +679,11 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
method_info: events.InfoOutputRequested,
|
method_info: events.InfoOutputRequested,
|
||||||
input_socket_name: ct.SocketName | None,
|
input_socket_name: ct.SocketName | None,
|
||||||
input_socket_kinds: set[ct.FlowKind] | None,
|
input_socket_kinds: set[ct.FlowKind] | None,
|
||||||
prop_name: str | None,
|
prop_names: set[str] | None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return (
|
return (
|
||||||
prop_name is not None
|
prop_names is not None
|
||||||
and prop_name in method_info.depon_props
|
and any(prop_name in method_info.depon_props for prop_name in prop_names)
|
||||||
or input_socket_name is not None
|
or input_socket_name is not None
|
||||||
and (
|
and (
|
||||||
input_socket_name in method_info.depon_input_sockets
|
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()
|
@bl_cache.cached_bl_property()
|
||||||
def _dependent_outputs(
|
def output_socket_invalidates(
|
||||||
self,
|
self,
|
||||||
) -> dict[
|
) -> dict[
|
||||||
tuple[ct.SocketName, ct.FlowKind], set[tuple[ct.SocketName, ct.FlowKind]]
|
tuple[ct.SocketName, ct.FlowKind], set[tuple[ct.SocketName, ct.FlowKind]]
|
||||||
]:
|
]:
|
||||||
## TODO: Cleanup
|
"""Deduce which output socket | `FlowKind` combos are altered in response to a given output socket | `FlowKind` combo.
|
||||||
## TODO: Detect cycles?
|
|
||||||
## TODO: Networkx?
|
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)
|
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[
|
output_requested_methods = self.event_methods_by_event[
|
||||||
ct.FlowEvent.OutputRequested
|
ct.FlowEvent.OutputRequested
|
||||||
]
|
]
|
||||||
|
|
||||||
for altered_method in output_requested_methods:
|
for altered_method in output_requested_methods:
|
||||||
altered_info = altered_method.callback_info
|
altered_info = altered_method.callback_info
|
||||||
altered_key = (altered_info.output_socket_name, altered_info.kind)
|
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:
|
for invalidated_method in output_requested_methods:
|
||||||
invalidated_info = invalidated_method.callback_info
|
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 (
|
if (
|
||||||
altered_info.output_socket_name
|
altered_info.output_socket_name
|
||||||
in invalidated_info.depon_output_sockets
|
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 = (
|
is_same_kind = (
|
||||||
altered_info.kind
|
altered_info.kind
|
||||||
== (
|
is (
|
||||||
_kind := invalidated_info.depon_output_socket_kinds.get(
|
_kind := invalidated_info.depon_output_socket_kinds.get(
|
||||||
altered_info.output_socket_name
|
altered_info.output_socket_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
or (isinstance(_kind, set) and altered_info.kind in _kind)
|
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:
|
if is_same_kind:
|
||||||
invalidated_key = (
|
invalidated_key = (
|
||||||
invalidated_info.output_socket_name,
|
invalidated_info.output_socket_name,
|
||||||
|
@ -753,7 +780,7 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
event: ct.FlowEvent,
|
event: ct.FlowEvent,
|
||||||
socket_name: ct.SocketName | None = None,
|
socket_name: ct.SocketName | None = None,
|
||||||
socket_kinds: set[ct.FlowKind] | None = None,
|
socket_kinds: set[ct.FlowKind] | None = None,
|
||||||
prop_name: ct.SocketName | None = None,
|
prop_names: set[str] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Recursively triggers events forwards or backwards along the node tree, allowing nodes in the update path to react.
|
"""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.
|
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.
|
pop_name: The property that was altered, if any, in order to trigger this event.
|
||||||
"""
|
"""
|
||||||
log.debug(
|
# log.debug(
|
||||||
'%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
|
# '[%s] [%s] Triggered (socket_name=%s, socket_kinds=%s, prop_names=%s)',
|
||||||
self.sim_node_name,
|
# self.sim_node_name,
|
||||||
event,
|
# event,
|
||||||
str(socket_name),
|
# str(socket_name),
|
||||||
str(socket_kinds),
|
# str(socket_kinds),
|
||||||
str(prop_name),
|
# str(prop_names),
|
||||||
)
|
# )
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Invalidate Caches on DataChanged
|
# 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:
|
if event is ct.FlowEvent.DataChanged:
|
||||||
input_socket_name = socket_name ## Trigger direction is forwards
|
in_sckname = socket_name
|
||||||
|
|
||||||
# Invalidate Input Socket Cache
|
# Clear Input Socket Cache(s)
|
||||||
if input_socket_name is not None:
|
## -> The input socket cache for each altered FlowKinds is cleared.
|
||||||
if socket_kinds is None:
|
## -> 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(
|
self._compute_input.invalidate(
|
||||||
input_socket_name=input_socket_name,
|
input_socket_name=in_sckname,
|
||||||
kind=...,
|
kind=in_kind,
|
||||||
unit_system=...,
|
unit_system=...,
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
for socket_kind in socket_kinds:
|
|
||||||
self._compute_input.invalidate(
|
|
||||||
input_socket_name=input_socket_name,
|
|
||||||
kind=socket_kind,
|
|
||||||
unit_system=...,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Invalidate Output Socket Cache
|
# Clear Output Socket Cache(s)
|
||||||
for output_socket_method in self.event_methods_by_event[
|
for output_socket_method in self.event_methods_by_event[
|
||||||
ct.FlowEvent.OutputRequested
|
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
|
method_info = output_socket_method.callback_info
|
||||||
if self._should_recompute_output_socket(
|
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
|
out_sckname = method_info.output_socket_name
|
||||||
kind = method_info.kind
|
out_kind = method_info.kind
|
||||||
|
|
||||||
# Invalidate Output Directly
|
# log.debug(
|
||||||
# log.critical(
|
# '![%s] Clear Output Socket Cache (%s, %s)',
|
||||||
# '[%s] Invalidating: (%s, %s)',
|
|
||||||
# self.sim_node_name,
|
# self.sim_node_name,
|
||||||
# out_sckname,
|
# out_sckname,
|
||||||
# str(kind),
|
# out_kind,
|
||||||
# )
|
# )
|
||||||
altered_socket_kinds.add(kind)
|
|
||||||
self.compute_output.invalidate(
|
self.compute_output.invalidate(
|
||||||
output_socket_name=out_sckname,
|
output_socket_name=out_sckname,
|
||||||
kind=kind,
|
kind=out_kind,
|
||||||
)
|
)
|
||||||
|
altered_socket_kinds[out_sckname].add(out_kind)
|
||||||
|
|
||||||
# Invalidate Any Dependent Outputs
|
# Invalidate Dependent Output Sockets
|
||||||
if (
|
## -> Other outscks may depend on the altered outsck.
|
||||||
dep_outs := self._dependent_outputs.get((out_sckname, kind))
|
## -> The property 'output_socket_invalidates' encodes this.
|
||||||
) is not None:
|
## -> The property 'output_socket_invalidates' encodes this.
|
||||||
for dep_out in dep_outs:
|
cleared_outscks_kinds = self.output_socket_invalidates.get(
|
||||||
# log.critical(
|
(out_sckname, out_kind)
|
||||||
# '![%s] Invalidating: (%s, %s)',
|
)
|
||||||
|
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,
|
# self.sim_node_name,
|
||||||
# dep_out[0],
|
# out_sckname,
|
||||||
# dep_out[1],
|
# out_kind,
|
||||||
# )
|
# )
|
||||||
altered_socket_kinds.add(dep_out[1])
|
|
||||||
self.compute_output.invalidate(
|
self.compute_output.invalidate(
|
||||||
output_socket_name=dep_out[0],
|
output_socket_name=dep_out_sckname,
|
||||||
kind=dep_out[1],
|
kind=dep_out_kind,
|
||||||
)
|
)
|
||||||
|
altered_socket_kinds[dep_out_sckname].add(dep_out_kind)
|
||||||
|
|
||||||
# Run Triggered Event Methods
|
# 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
|
stop_propagation = False
|
||||||
triggered_event_methods = self.filtered_event_methods_by_event(
|
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:
|
for event_method in triggered_event_methods:
|
||||||
stop_propagation |= event_method.stop_propagation
|
stop_propagation |= event_method.stop_propagation
|
||||||
# log.critical(
|
# log.debug(
|
||||||
# '%s: Running %s',
|
# '![%s] Running: %s',
|
||||||
# self.sim_node_name,
|
# self.sim_node_name,
|
||||||
# str(event_method.callback_info),
|
# str(event_method.callback_info),
|
||||||
# )
|
# )
|
||||||
event_method(self)
|
event_method(self)
|
||||||
|
|
||||||
# DataChanged Propagation Stop: No Altered Socket Kinds
|
# Propagate Event
|
||||||
## -> If no FlowKinds were altered, then propagation makes no sense.
|
## -> If 'stop_propagation' was tripped, don't propagate.
|
||||||
## -> Semantically, **nothing has changed** == no DataChanged!
|
## -> If no sockets were altered during DataChanged, don't propagate.
|
||||||
if event is ct.FlowEvent.DataChanged and not altered_socket_kinds:
|
## -> Each FlowEvent decides whether to flow forwards/backwards.
|
||||||
return
|
|
||||||
|
|
||||||
# Constrain ShowPlot to First Node: Workaround
|
|
||||||
if event is ct.FlowEvent.ShowPlot:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Propagate Event to All Sockets in "Trigger Direction"
|
|
||||||
## -> The trigger chain goes node/socket/socket/node/socket/...
|
## -> The trigger chain goes node/socket/socket/node/socket/...
|
||||||
|
## -> Unlinked sockets naturally stop the propagation.
|
||||||
if not stop_propagation:
|
if not stop_propagation:
|
||||||
direc = ct.FlowEvent.flow_direction[event]
|
direc = ct.FlowEvent.flow_direction[event]
|
||||||
triggered_sockets = self._bl_sockets(direc=direc)
|
for bl_socket in self._bl_sockets(direc=direc):
|
||||||
for bl_socket in triggered_sockets:
|
# DataChanged: Propagate Altered SocketKinds
|
||||||
if direc == 'output' and not bl_socket.is_linked:
|
## -> Only altered FlowKinds for the socket will propagate.
|
||||||
continue
|
## -> In this way, we guarantee no extraneous (noop) flow.
|
||||||
|
if event is ct.FlowEvent.DataChanged:
|
||||||
|
if bl_socket.name in altered_socket_kinds:
|
||||||
|
# log.debug(
|
||||||
|
# '![%s] [%s] Propagating (direction=%s, altered_socket_kinds=%s)',
|
||||||
|
# self.sim_node_name,
|
||||||
|
# event,
|
||||||
|
# direc,
|
||||||
|
# altered_socket_kinds[bl_socket.name],
|
||||||
|
# )
|
||||||
|
bl_socket.trigger_event(
|
||||||
|
event, socket_kinds=altered_socket_kinds[bl_socket.name]
|
||||||
|
)
|
||||||
|
|
||||||
# log.critical(
|
## -> Otherwise, do nothing - guarantee no extraneous flow.
|
||||||
# '![%s] Propagating: (%s, %s)',
|
|
||||||
# self.sim_node_name,
|
# Propagate Normally
|
||||||
# event,
|
else:
|
||||||
# altered_socket_kinds,
|
# log.debug(
|
||||||
# )
|
# '![%s] [%s] Propagating (direction=%s)',
|
||||||
bl_socket.trigger_event(event, socket_kinds=altered_socket_kinds)
|
# self.sim_node_name,
|
||||||
|
# event,
|
||||||
|
# direc,
|
||||||
|
# )
|
||||||
|
bl_socket.trigger_event(event)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Property Event: On Update
|
# - Property Event: On Update
|
||||||
|
@ -903,18 +947,22 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
||||||
Parameters:
|
Parameters:
|
||||||
prop_name: The name of the property that changed.
|
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
|
# BLField Attributes: Invalidate BLField Dependents
|
||||||
## -> Dependent props will generally also trigger on_prop_changed.
|
## -> All invalidated blfields will have their caches cleared.
|
||||||
## -> The recursion ends with the depschain.
|
## -> The (topologically) ordered list of cleared blfields is returned.
|
||||||
## -> WARNING: The chain is not checked for ex. cycles.
|
## -> WARNING: The chain is not checked for ex. cycles.
|
||||||
if prop_name in self.blfields:
|
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
|
# - UI Methods
|
||||||
|
|
|
@ -18,15 +18,18 @@ from . import (
|
||||||
blender_constant,
|
blender_constant,
|
||||||
expr_constant,
|
expr_constant,
|
||||||
scientific_constant,
|
scientific_constant,
|
||||||
|
symbol_constant,
|
||||||
)
|
)
|
||||||
|
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
*expr_constant.BL_REGISTER,
|
*expr_constant.BL_REGISTER,
|
||||||
|
*symbol_constant.BL_REGISTER,
|
||||||
*scientific_constant.BL_REGISTER,
|
*scientific_constant.BL_REGISTER,
|
||||||
*blender_constant.BL_REGISTER,
|
*blender_constant.BL_REGISTER,
|
||||||
]
|
]
|
||||||
BL_NODES = {
|
BL_NODES = {
|
||||||
**expr_constant.BL_NODES,
|
**expr_constant.BL_NODES,
|
||||||
|
**symbol_constant.BL_NODES,
|
||||||
**scientific_constant.BL_NODES,
|
**scientific_constant.BL_NODES,
|
||||||
**blender_constant.BL_NODES,
|
**blender_constant.BL_NODES,
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,13 +76,13 @@ class ScientificConstantNode(base.MaxwellSimNode):
|
||||||
"""Retrieve a symbol for the scientific constant."""
|
"""Retrieve a symbol for the scientific constant."""
|
||||||
if self.sci_constant is not None and self.sci_constant_info is not None:
|
if self.sci_constant is not None and self.sci_constant_info is not None:
|
||||||
unit = self.sci_constant_info['units']
|
unit = self.sci_constant_info['units']
|
||||||
return sim_symbols.SimSymbol(
|
return sim_symbols.SimSymbol.from_expr(
|
||||||
sym_name=self.sci_constant_name,
|
self.sci_constant_name,
|
||||||
mathtype=spux.MathType.from_expr(self.sci_constant),
|
self.sci_constant,
|
||||||
# physical_type= ## TODO: Formalize unit w/o physical_type
|
unit,
|
||||||
unit=unit,
|
|
||||||
is_constant=True,
|
is_constant=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -125,7 +125,7 @@ class ScientificConstantNode(base.MaxwellSimNode):
|
||||||
if self.sci_constant_info:
|
if self.sci_constant_info:
|
||||||
row = _col.row(align=True)
|
row = _col.row(align=True)
|
||||||
# row.alignment = 'CENTER'
|
# 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 = _col.row(align=True)
|
||||||
# row.alignment = 'CENTER'
|
# row.alignment = 'CENTER'
|
||||||
|
@ -184,13 +184,18 @@ class ScientificConstantNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Expr',
|
'Expr',
|
||||||
kind=ct.FlowKind.Params,
|
kind=ct.FlowKind.Params,
|
||||||
props={'sci_constant'},
|
props={'sci_constant', 'sci_constant_sym'},
|
||||||
)
|
)
|
||||||
def compute_params(self, props: dict) -> typ.Any:
|
def compute_params(self, props: dict) -> typ.Any:
|
||||||
sci_constant = props['sci_constant']
|
sci_constant = props['sci_constant']
|
||||||
|
sci_constant_sym = props['sci_constant_sym']
|
||||||
|
|
||||||
if sci_constant is not None:
|
if sci_constant is not None and sci_constant_sym is not None:
|
||||||
return ct.ParamsFlow(func_args=[sci_constant])
|
return ct.ParamsFlow(
|
||||||
|
arg_targets=[sci_constant_sym],
|
||||||
|
func_args=[sci_constant],
|
||||||
|
is_differentiable=True,
|
||||||
|
)
|
||||||
return ct.FlowSignal.FlowPending
|
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
|
# - Properties
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
socket_name={'File Path'},
|
socket_name={'File Path'},
|
||||||
|
# Loaded
|
||||||
input_sockets={'File Path'},
|
input_sockets={'File Path'},
|
||||||
input_socket_kinds={'File Path': ct.FlowKind.Value},
|
input_socket_kinds={'File Path': ct.FlowKind.Value},
|
||||||
input_sockets_optional={'File Path': True},
|
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
|
def on_input_exprs_changed(self, input_sockets) -> None: # noqa: D102
|
||||||
has_file_path = not ct.FlowSignal.check(input_sockets['File Path'])
|
has_file_path = not ct.FlowSignal.check(input_sockets['File Path'])
|
||||||
|
@ -83,7 +88,15 @@ class DataFileImporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Output Info
|
# - 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:
|
def expr_info(self) -> ct.InfoFlow | None:
|
||||||
"""Retrieve the output expression's `InfoFlow`."""
|
"""Retrieve the output expression's `InfoFlow`."""
|
||||||
info = self.compute_output('Expr', kind=ct.FlowKind.Info)
|
info = self.compute_output('Expr', kind=ct.FlowKind.Info)
|
||||||
|
@ -184,19 +197,19 @@ class DataFileImporterNode(base.MaxwellSimNode):
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Expr',
|
'Expr',
|
||||||
kind=ct.FlowKind.Func,
|
kind=ct.FlowKind.Func,
|
||||||
|
# Loaded
|
||||||
input_sockets={'File Path'},
|
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.
|
"""Declare a lazy, composable function that returns the loaded data.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A completely empty `ParamsFlow`, ready to be composed.
|
A completely empty `ParamsFlow`, ready to be composed.
|
||||||
"""
|
"""
|
||||||
file_path = input_sockets['File Path']
|
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 and file_path is not None:
|
||||||
|
|
||||||
if has_file_path:
|
|
||||||
data_file_format = ct.DataFileFormat.from_path(file_path)
|
data_file_format = ct.DataFileFormat.from_path(file_path)
|
||||||
if data_file_format is not None:
|
if data_file_format is not None:
|
||||||
# Jax Compatibility: Lazy Data Loading
|
# Jax Compatibility: Lazy Data Loading
|
||||||
|
|
|
@ -195,18 +195,23 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Time Monitor',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews_time(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
@events.computes_output_socket(
|
||||||
managed_objs['modifier'].hide_preview()
|
'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(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -170,18 +170,23 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview - Changes to Input Sockets
|
# - Preview - Changes to Input Sockets
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Time Monitor',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews_time(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
@events.computes_output_socket(
|
||||||
managed_objs['modifier'].hide_preview()
|
'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(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -119,18 +119,14 @@ class PermittivityMonitorNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Permittivity Monitor',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews_freq(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -232,8 +232,17 @@ class DataFileExporterNode(base.MaxwellSimNode):
|
||||||
dim.name for dim in params.symbols if dim in info.dims
|
dim.name for dim in params.symbols if dim in info.dims
|
||||||
}:
|
}:
|
||||||
self.loose_input_sockets = {
|
self.loose_input_sockets = {
|
||||||
dim_name: sockets.ExprSocketDef(**expr_info)
|
sym.name: sockets.ExprSocketDef(
|
||||||
for dim_name, expr_info in params.sym_expr_infos(info).items()
|
**(
|
||||||
|
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:
|
elif self.loose_input_sockets:
|
||||||
|
|
|
@ -18,6 +18,7 @@ import typing as typ
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
|
import tidy3d as td
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
|
@ -88,32 +89,79 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
socket_name='Any',
|
socket_name='Any',
|
||||||
)
|
)
|
||||||
def on_input_changed(self) -> None:
|
def on_input_changed(self) -> None:
|
||||||
self.input_flow = bl_cache.Signal.InvalidateCache
|
"""Lightweight invalidator, which invalidates the more specific `cached_bl_property` used to determine when something ex. plot-related has changed.
|
||||||
|
|
||||||
@bl_cache.cached_bl_property()
|
|
||||||
def input_flow(self) -> dict[ct.FlowKind, typ.Any | None]:
|
|
||||||
input_flow = {}
|
|
||||||
|
|
||||||
|
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):
|
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)
|
flow = self._compute_input('Any', kind=flow_kind)
|
||||||
has_flow = not ct.FlowSignal.check(flow)
|
has_flow = not ct.FlowSignal.check(flow)
|
||||||
|
|
||||||
if has_flow:
|
if has_flow:
|
||||||
input_flow |= {flow_kind: flow}
|
return flow
|
||||||
else:
|
return None
|
||||||
input_flow |= {flow_kind: None}
|
return None
|
||||||
|
|
||||||
return input_flow
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Property: Input Expression String Lines
|
# - 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:
|
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:
|
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))
|
return spux.sp_to_str(v.n(4))
|
||||||
|
|
||||||
if isinstance(value, spux.SympyType):
|
if isinstance(value, spux.SympyType):
|
||||||
|
@ -124,6 +172,25 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
]
|
]
|
||||||
|
|
||||||
return [[sp_pretty(value)]]
|
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
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -132,12 +199,12 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
|
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
|
||||||
row = layout.row(align=True)
|
row = layout.row(align=True)
|
||||||
|
|
||||||
|
# Automatic Expression Printing
|
||||||
|
row.prop(self, self.blfields['auto_expr'], text='Live', toggle=True)
|
||||||
|
|
||||||
# Debug Mode On/Off
|
# Debug Mode On/Off
|
||||||
row.prop(self, self.blfields['debug_mode'], text='Debug', toggle=True)
|
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
|
# Debug Mode Operators
|
||||||
if self.debug_mode:
|
if self.debug_mode:
|
||||||
layout.prop(self, self.blfields['console_print_kind'], text='')
|
layout.prop(self, self.blfields['console_print_kind'], text='')
|
||||||
|
@ -210,47 +277,47 @@ class ViewerNode(base.MaxwellSimNode):
|
||||||
# - Methods
|
# - Methods
|
||||||
####################
|
####################
|
||||||
def print_data_to_console(self):
|
def print_data_to_console(self):
|
||||||
if not self.inputs['Any'].is_linked:
|
flow = self._compute_input('Any', kind=self.console_print_kind)
|
||||||
return
|
|
||||||
|
|
||||||
log.info('Printing to Console')
|
log.info('Printing to Console')
|
||||||
data = self._compute_input('Any', kind=self.console_print_kind, optional=True)
|
if isinstance(flow, spux.SympyType):
|
||||||
|
console.print(sp.pretty(flow, use_unicode=True))
|
||||||
if isinstance(data, spux.SympyType):
|
|
||||||
console.print(sp.pretty(data, use_unicode=True))
|
|
||||||
else:
|
else:
|
||||||
console.print(data)
|
console.print(flow)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Event Methods
|
# - Event Methods
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name='Any',
|
# Trigger
|
||||||
prop_name='auto_plot',
|
prop_name={'input_previews', 'auto_plot'},
|
||||||
props={'auto_plot'},
|
# Loaded
|
||||||
|
props={'input_previews', 'auto_plot'},
|
||||||
)
|
)
|
||||||
def on_changed_plot_preview(self, props):
|
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
|
previews.update_image_preview()
|
||||||
with node_tree.replot():
|
else:
|
||||||
if props['auto_plot'] and self.inputs['Any'].is_linked:
|
ct.PreviewsFlow.hide_image_preview()
|
||||||
self.inputs['Any'].links[0].from_socket.node.trigger_event(
|
|
||||||
ct.FlowEvent.ShowPlot
|
|
||||||
)
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name='Any',
|
# Trigger
|
||||||
prop_name='auto_3d_preview',
|
prop_name={'input_previews', 'auto_3d_preview'},
|
||||||
props={'auto_3d_preview'},
|
# Loaded
|
||||||
|
props={'input_previews', 'auto_3d_preview'},
|
||||||
)
|
)
|
||||||
def on_changed_3d_preview(self, props):
|
def on_changed_3d_preview(self, props):
|
||||||
node_tree = self.id_data
|
previews = props['input_previews']
|
||||||
|
if previews is not None and props['auto_3d_preview']:
|
||||||
# Remove Non-Repreviewed Previews on Close
|
previews.update_bl_object_previews()
|
||||||
with node_tree.repreview_all():
|
else:
|
||||||
if props['auto_3d_preview']:
|
ct.PreviewsFlow.hide_bl_object_previews()
|
||||||
self.trigger_event(ct.FlowEvent.ShowPreview)
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -64,20 +64,22 @@ class FDTDSimNode(base.MaxwellSimNode):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr:
|
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']
|
sim_domain = input_sockets['Domain']
|
||||||
sources = input_sockets['Sources']
|
sources = input_sockets['Sources']
|
||||||
structures = input_sockets['Structures']
|
structures = input_sockets['Structures']
|
||||||
bounds = input_sockets['BCs']
|
bounds = input_sockets['BCs']
|
||||||
monitors = input_sockets['Monitors']
|
monitors = input_sockets['Monitors']
|
||||||
|
|
||||||
return td.Simulation(
|
return td.Simulation(
|
||||||
**sim_domain, ## run_time=, size=, grid=, medium=
|
**sim_domain,
|
||||||
structures=structures,
|
structures=structures,
|
||||||
sources=sources,
|
sources=sources,
|
||||||
monitors=monitors,
|
monitors=monitors,
|
||||||
boundary_spec=bounds,
|
boundary_spec=bounds,
|
||||||
)
|
)
|
||||||
|
## TODO: Visualize the boundary conditions on top of the sim domain
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -93,18 +93,14 @@ class SimDomainNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Domain',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
## Trigger
|
## Trigger
|
||||||
|
|
|
@ -165,18 +165,14 @@ class GaussianBeamSourceNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview - Changes to Input Sockets
|
# - Preview - Changes to Input Sockets
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Angled Source',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -129,18 +129,14 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview - Changes to Input Sockets
|
# - Preview - Changes to Input Sockets
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Angled Source',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -104,18 +104,14 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Source',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name={'Center'},
|
socket_name={'Center'},
|
||||||
|
|
|
@ -132,18 +132,14 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Events: Preview
|
# - Events: Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Structure',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -16,13 +16,15 @@
|
||||||
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
|
import bpy
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
import sympy.physics.units as spu
|
import sympy.physics.units as spu
|
||||||
import tidy3d as td
|
import tidy3d as td
|
||||||
|
import tidy3d.plugins.adjoint as tdadj
|
||||||
|
|
||||||
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
|
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 extra_sympy_units as spux
|
||||||
from blender_maxwell.utils import logger
|
|
||||||
|
|
||||||
from .... import contracts as ct
|
from .... import contracts as ct
|
||||||
from .... import managed_objs, sockets
|
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(
|
@events.computes_output_socket(
|
||||||
'Structure',
|
'Structure',
|
||||||
|
kind=ct.FlowKind.Value,
|
||||||
|
# Loaded
|
||||||
|
props={'differentiable'},
|
||||||
input_sockets={'Medium', 'Center', 'Size'},
|
input_sockets={'Medium', 'Center', 'Size'},
|
||||||
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
|
output_sockets={'Structure'},
|
||||||
scale_input_sockets={
|
output_socket_kinds={'Structure': ct.FlowKind.Params},
|
||||||
'Center': 'Tidy3DUnits',
|
)
|
||||||
'Size': 'Tidy3DUnits',
|
def compute_value(self, props, input_sockets, output_sockets) -> td.Box:
|
||||||
|
output_params = output_sockets['Structure']
|
||||||
|
center = input_sockets['Center']
|
||||||
|
size = input_sockets['Size']
|
||||||
|
medium = input_sockets['Medium']
|
||||||
|
|
||||||
|
has_output_params = not ct.FlowSignal.check(output_params)
|
||||||
|
has_center = not ct.FlowSignal.check(center)
|
||||||
|
has_size = not ct.FlowSignal.check(size)
|
||||||
|
has_medium = not ct.FlowSignal.check(medium)
|
||||||
|
|
||||||
|
if (
|
||||||
|
has_center
|
||||||
|
and has_size
|
||||||
|
and has_medium
|
||||||
|
and has_output_params
|
||||||
|
and not props['differentiable']
|
||||||
|
and not output_params.symbols
|
||||||
|
):
|
||||||
|
return td.Structure(
|
||||||
|
geometry=td.Box(
|
||||||
|
center=spux.scale_to_unit_system(center, ct.UNITS_TIDY3D),
|
||||||
|
size=spux.scale_to_unit_system(size, ct.UNITS_TIDY3D),
|
||||||
|
),
|
||||||
|
medium=medium,
|
||||||
|
)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - FlowKind.Func
|
||||||
|
####################
|
||||||
|
@events.computes_output_socket(
|
||||||
|
'Structure',
|
||||||
|
kind=ct.FlowKind.Func,
|
||||||
|
# Loaded
|
||||||
|
props={'differentiable'},
|
||||||
|
input_sockets={'Medium', 'Center', 'Size'},
|
||||||
|
input_socket_kinds={
|
||||||
|
'Medium': ct.FlowKind.Func,
|
||||||
|
'Center': ct.FlowKind.Func,
|
||||||
|
'Size': ct.FlowKind.Func,
|
||||||
|
},
|
||||||
|
output_sockets={'Structure'},
|
||||||
|
output_socket_kinds={'Structure': ct.FlowKind.Params},
|
||||||
|
)
|
||||||
|
def compute_lazy_structure(self, props, input_sockets, output_sockets) -> td.Box:
|
||||||
|
output_params = output_sockets['Structure']
|
||||||
|
center = input_sockets['Center']
|
||||||
|
size = input_sockets['Size']
|
||||||
|
medium = input_sockets['Medium']
|
||||||
|
|
||||||
|
has_output_params = not ct.FlowSignal.check(output_params)
|
||||||
|
has_center = not ct.FlowSignal.check(center)
|
||||||
|
has_size = not ct.FlowSignal.check(size)
|
||||||
|
has_medium = not ct.FlowSignal.check(medium)
|
||||||
|
|
||||||
|
differentiable = props['differentiable']
|
||||||
|
if (
|
||||||
|
has_output_params
|
||||||
|
and has_center
|
||||||
|
and has_size
|
||||||
|
and has_medium
|
||||||
|
and differentiable == output_params.is_differentiable
|
||||||
|
):
|
||||||
|
if differentiable:
|
||||||
|
return (center | size | medium).compose_within(
|
||||||
|
enclosing_func=lambda els: tdadj.JaxStructure(
|
||||||
|
geometry=tdadj.JaxBox(
|
||||||
|
center=tuple(els[0][0].flatten()),
|
||||||
|
size=tuple(els[0][1].flatten()),
|
||||||
|
),
|
||||||
|
medium=els[1],
|
||||||
|
),
|
||||||
|
supports_jax=True,
|
||||||
|
)
|
||||||
|
return (center | size | medium).compose_within(
|
||||||
|
enclosing_func=lambda els: td.Structure(
|
||||||
|
geometry=td.Box(
|
||||||
|
center=tuple(els[0][0].flatten()),
|
||||||
|
size=tuple(els[0][1].flatten()),
|
||||||
|
),
|
||||||
|
medium=els[1],
|
||||||
|
),
|
||||||
|
supports_jax=False,
|
||||||
|
)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - FlowKind.Params
|
||||||
|
####################
|
||||||
|
@events.computes_output_socket(
|
||||||
|
'Structure',
|
||||||
|
kind=ct.FlowKind.Params,
|
||||||
|
# Loaded
|
||||||
|
props={'differentiable'},
|
||||||
|
input_sockets={'Medium', 'Center', 'Size'},
|
||||||
|
input_socket_kinds={
|
||||||
|
'Medium': ct.FlowKind.Params,
|
||||||
|
'Center': ct.FlowKind.Params,
|
||||||
|
'Size': ct.FlowKind.Params,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
def compute_structure(self, input_sockets, unit_systems) -> td.Box:
|
def compute_params(self, props, input_sockets) -> td.Box:
|
||||||
return td.Structure(
|
center = input_sockets['Center']
|
||||||
geometry=td.Box(
|
size = input_sockets['Size']
|
||||||
center=input_sockets['Center'],
|
medium = input_sockets['Medium']
|
||||||
size=input_sockets['Size'],
|
|
||||||
),
|
has_center = not ct.FlowSignal.check(center)
|
||||||
medium=input_sockets['Medium'],
|
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: Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Structure',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
output_sockets={'Structure'},
|
||||||
|
output_socket_kinds={'Structure': ct.FlowKind.Params},
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props, output_sockets):
|
||||||
if props['preview_active']:
|
output_params = output_sockets['Structure']
|
||||||
managed_objs['modifier'].show_preview()
|
has_output_params = not ct.FlowSignal.check(output_params)
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
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(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
@ -105,29 +238,26 @@ class BoxStructureNode(base.MaxwellSimNode):
|
||||||
# Loaded
|
# Loaded
|
||||||
input_sockets={'Center', 'Size'},
|
input_sockets={'Center', 'Size'},
|
||||||
managed_objs={'modifier'},
|
managed_objs={'modifier'},
|
||||||
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
|
output_sockets={'Structure'},
|
||||||
scale_input_sockets={
|
output_socket_kinds={'Structure': ct.FlowKind.Params},
|
||||||
'Center': 'BlenderUnits',
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
def on_inputs_changed(
|
def on_inputs_changed(self, managed_objs, input_sockets, output_sockets):
|
||||||
self,
|
output_params = output_sockets['Structure']
|
||||||
managed_objs,
|
has_output_params = not ct.FlowSignal.check(output_params)
|
||||||
input_sockets,
|
if has_output_params and not output_params.symbols:
|
||||||
unit_systems,
|
# Push Loose Input Values to GeoNodes Modifier
|
||||||
):
|
center = input_sockets['Center']
|
||||||
# Push Loose Input Values to GeoNodes Modifier
|
managed_objs['modifier'].bl_modifier(
|
||||||
managed_objs['modifier'].bl_modifier(
|
'NODES',
|
||||||
'NODES',
|
{
|
||||||
{
|
'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
|
||||||
'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
|
'unit_system': ct.UNITS_BLENDER,
|
||||||
'unit_system': unit_systems['BlenderUnits'],
|
'inputs': {
|
||||||
'inputs': {
|
'Size': input_sockets['Size'],
|
||||||
'Size': input_sockets['Size'],
|
},
|
||||||
},
|
},
|
||||||
},
|
location=spux.scale_to_unit_system(center, ct.UNITS_BLENDER),
|
||||||
location=input_sockets['Center'],
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -89,18 +89,14 @@ class CylinderStructureNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Structure',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -83,18 +83,14 @@ class SphereStructureNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Preview
|
# - Preview
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
'Structure',
|
||||||
prop_name='preview_active',
|
kind=ct.FlowKind.Previews,
|
||||||
# Loaded
|
# Loaded
|
||||||
managed_objs={'modifier'},
|
props={'sim_node_name'},
|
||||||
props={'preview_active'},
|
|
||||||
)
|
)
|
||||||
def on_preview_changed(self, managed_objs, props):
|
def compute_previews(self, props):
|
||||||
if props['preview_active']:
|
return ct.PreviewsFlow(bl_object_names={props['sim_node_name']})
|
||||||
managed_objs['modifier'].show_preview()
|
|
||||||
else:
|
|
||||||
managed_objs['modifier'].hide_preview()
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
# Trigger
|
||||||
|
|
|
@ -50,8 +50,10 @@ class SocketDef(pyd.BaseModel, abc.ABC):
|
||||||
Parameters:
|
Parameters:
|
||||||
bl_socket: The Blender node socket to alter using data from this SocketDef.
|
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.reset_instance_id()
|
||||||
bl_socket.regenerate_dynamic_field_persistance()
|
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:
|
def postinit(self, bl_socket: bpy.types.NodeSocket) -> None:
|
||||||
"""Pre-initialize a real Blender node socket from this socket definition.
|
"""Pre-initialize a real Blender node socket from this socket definition.
|
||||||
|
@ -59,8 +61,12 @@ class SocketDef(pyd.BaseModel, abc.ABC):
|
||||||
Parameters:
|
Parameters:
|
||||||
bl_socket: The Blender node socket to alter using data from this SocketDef.
|
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.is_initializing = False
|
||||||
bl_socket.on_active_kind_changed()
|
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
|
@abc.abstractmethod
|
||||||
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
|
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
|
socket_type: ct.SocketType
|
||||||
bl_label: str
|
bl_label: str
|
||||||
|
|
||||||
|
use_linked_capabilities: bool = bl_cache.BLField(False, use_prop_update=False)
|
||||||
|
|
||||||
## Computed by Subclass
|
## Computed by Subclass
|
||||||
bl_idname: str
|
bl_idname: str
|
||||||
|
|
||||||
|
@ -181,17 +189,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
"""
|
"""
|
||||||
self.display_shape = self.active_kind.socket_shape
|
self.display_shape = self.active_kind.socket_shape
|
||||||
|
|
||||||
def on_socket_prop_changed(self, prop_name: str) -> None:
|
def on_socket_props_changed(self, prop_names: set[str]) -> None:
|
||||||
"""Called when a property has been updated.
|
"""Called when a set of properties has been updated.
|
||||||
|
|
||||||
Notes:
|
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**.
|
**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.
|
Think **very carefully** before using this, and use it with the greatest of care.
|
||||||
|
|
||||||
Attributes:
|
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:
|
def on_prop_changed(self, prop_name: str) -> None:
|
||||||
|
@ -207,30 +215,49 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
Attributes:
|
Attributes:
|
||||||
prop_name: The name of the property that was changed.
|
prop_name: The name of the property that was changed.
|
||||||
"""
|
"""
|
||||||
# All Attributes: Trigger Local Event
|
# BLField Attributes: Invalidate BLField Dependents
|
||||||
## -> While initializing, only `DataChanged` won't trigger.
|
## -> All invalidated blfields will have their caches cleared.
|
||||||
if hasattr(self, prop_name):
|
## -> The (topologically) ordered list of cleared blfields is returned.
|
||||||
# Property Callbacks: Active Kind
|
## -> WARNING: The chain is not checked for ex. cycles.
|
||||||
## -> WARNING: May NOT rely on flow.
|
if not self.is_initializing and prop_name in self.blfields:
|
||||||
if prop_name == 'active_kind':
|
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()
|
self.on_active_kind_changed()
|
||||||
|
|
||||||
# Property Callbacks: Per-Socket
|
# Property Callbacks: Per-Socket
|
||||||
## -> WARNING: May NOT rely on flow.
|
## -> NOTE: User-defined handlers might recurse on_prop_changed.
|
||||||
self.on_socket_prop_changed(prop_name)
|
self.is_initializing = True
|
||||||
|
self.on_socket_props_changed(set_of_cleared_blfields)
|
||||||
|
self.is_initializing = False
|
||||||
|
|
||||||
# Not Initializing: Trigger Event
|
# Trigger Event
|
||||||
## -> This declares that the socket has changed.
|
## -> Before SocketDef.postinit(), never emit DataChanged.
|
||||||
## -> This should happen first, in case dependents need a cache.
|
## -> ONLY emit DataChanged if a FlowKind-bound prop was cleared.
|
||||||
if not self.is_initializing:
|
## -> ONLY emit a single DataChanged w/set of altered FlowKinds.
|
||||||
self.trigger_event(ct.FlowEvent.DataChanged)
|
## w/node's trigger_event, we've guaranteed a minimal action.
|
||||||
|
socket_kinds = {
|
||||||
# BLField Attributes: Invalidate BLField Dependents
|
ct.FlowKind.from_property_name(prop_name)
|
||||||
## -> Dependent props will generally also trigger on_prop_changed.
|
for prop_name in {
|
||||||
## -> The recursion ends with the depschain.
|
prop_name
|
||||||
## -> WARNING: The chain is not checked for ex. cycles.
|
for prop_name, clear_method in set_of_cleared_blfields
|
||||||
if prop_name in self.blfields:
|
if clear_method == 'invalidate'
|
||||||
self.invalidate_blfield_deps(prop_name)
|
}.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
|
# - Link Event: Consent / On Change
|
||||||
|
@ -273,11 +300,29 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Capability Check
|
# 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(
|
log.error(
|
||||||
'Attempted to link output socket "%s" (%s) to input socket "%s" (%s), but capabilities are incompatible',
|
'Attempted to link output socket "%s" (%s) to input socket "%s" (%s), but capabilities are incompatible',
|
||||||
link.from_socket.bl_label,
|
link.from_socket.bl_label,
|
||||||
link.from_socket.capabilities,
|
incoming_capabilities,
|
||||||
self.bl_label,
|
self.bl_label,
|
||||||
self.capabilities,
|
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
|
def on_link_added(self, link: bpy.types.NodeLink) -> None: # noqa: ARG002
|
||||||
"""Triggers a `ct.FlowEvent.LinkChanged` event when a link is added.
|
"""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:
|
Notes:
|
||||||
Called by the node tree, generally (but not guaranteed) after `self.allow_add_link()` has given consent to add the link.
|
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.
|
link: The node link that was added.
|
||||||
Currently unused.
|
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
|
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`.
|
"""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
|
def on_link_removed(self, from_socket: bpy.types.NodeSocket) -> None: # noqa: ARG002
|
||||||
"""Triggers a `ct.FlowEvent.LinkChanged` event when a link is removed.
|
"""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:
|
Notes:
|
||||||
Called by the node tree, generally (but not guaranteed) after `self.allow_remove_link()` has given consent to remove the link.
|
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.
|
from_socket: The node socket that was attached to before link removal.
|
||||||
Currently unused.
|
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:
|
def remove_invalidated_links(self) -> None:
|
||||||
"""Reevaluates the capabilities of all socket links, and removes any that no longer match.
|
"""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
|
# - 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(
|
def trigger_event(
|
||||||
self,
|
self,
|
||||||
event: ct.FlowEvent,
|
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 -> Input**: Trigger event on node (w/`socket_name`).
|
||||||
- **Output Socket -> Output**: Trigger event on `to_socket`s along output links.
|
- **Output Socket -> Output**: Trigger event on `to_socket`s along output links.
|
||||||
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This can be an unpredictably heavy function, depending on the node graph topology.
|
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.
|
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.
|
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]
|
flow_direction = ct.FlowEvent.flow_direction[event]
|
||||||
|
|
||||||
# Locking
|
# Locking
|
||||||
if event in [ct.FlowEvent.EnableLock, ct.FlowEvent.DisableLock]:
|
if event is ct.FlowEvent.EnableLock:
|
||||||
self.locked = event == ct.FlowEvent.EnableLock
|
self.locked = True
|
||||||
|
elif event is ct.FlowEvent.DisableLock:
|
||||||
|
self.locked = False
|
||||||
|
|
||||||
# Event by Socket Orientation | Flow Direction
|
# Event by Socket Orientation | Flow Direction
|
||||||
match (self.is_output, 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)
|
link.from_socket.trigger_event(event, socket_kinds=socket_kinds)
|
||||||
|
|
||||||
case (False, 'output'):
|
case (False, 'output'):
|
||||||
if event == ct.FlowEvent.LinkChanged:
|
if event is ct.FlowEvent.LinkChanged:
|
||||||
self.node.trigger_event(
|
self.node.trigger_event(
|
||||||
ct.FlowEvent.DataChanged,
|
ct.FlowEvent.DataChanged,
|
||||||
socket_name=self.name,
|
socket_name=self.name,
|
||||||
|
@ -432,6 +545,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
# - FlowKind: Auxiliary
|
# - FlowKind: Auxiliary
|
||||||
####################
|
####################
|
||||||
# Capabilities
|
# Capabilities
|
||||||
|
def linked_capabilities(self, info: ct.InfoFlow) -> ct.CapabilitiesFlow:
|
||||||
|
"""Try this first when `is_linked and use_linked_capabilities`."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def capabilities(self) -> None:
|
def capabilities(self) -> None:
|
||||||
"""By default, the socket is linkeable with any other socket of the same type and active kind.
|
"""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:
|
Raises:
|
||||||
ValueError: When referencing a socket that's meant to be directly referenced.
|
ValueError: When referencing a socket that's meant to be directly referenced.
|
||||||
"""
|
"""
|
||||||
kind_data_map = {
|
return {
|
||||||
ct.FlowKind.Capabilities: lambda: self.capabilities,
|
ct.FlowKind.Capabilities: lambda: self.capabilities,
|
||||||
|
ct.FlowKind.Previews: lambda: ct.PreviewsFlow(),
|
||||||
ct.FlowKind.Value: lambda: self.value,
|
ct.FlowKind.Value: lambda: self.value,
|
||||||
ct.FlowKind.Array: lambda: self.array,
|
ct.FlowKind.Array: lambda: self.array,
|
||||||
ct.FlowKind.Func: lambda: self.lazy_func,
|
ct.FlowKind.Func: lambda: self.lazy_func,
|
||||||
ct.FlowKind.Range: lambda: self.lazy_range,
|
ct.FlowKind.Range: lambda: self.lazy_range,
|
||||||
ct.FlowKind.Params: lambda: self.params,
|
ct.FlowKind.Params: lambda: self.params,
|
||||||
ct.FlowKind.Info: lambda: self.info,
|
ct.FlowKind.Info: lambda: self.info,
|
||||||
}
|
}[kind]()
|
||||||
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)
|
|
||||||
|
|
||||||
def compute_data(
|
def compute_data(
|
||||||
self,
|
self,
|
||||||
|
@ -635,7 +747,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
return self.node.compute_output(self.name, kind=kind)
|
return self.node.compute_output(self.name, kind=kind)
|
||||||
|
|
||||||
# Compute Input Socket
|
# Compute Input Socket
|
||||||
## Unlinked: Retrieve Socket Value
|
## -> Unlinked: Retrieve Socket Value
|
||||||
if not self.is_linked:
|
if not self.is_linked:
|
||||||
return self._compute_data(kind)
|
return self._compute_data(kind)
|
||||||
|
|
||||||
|
@ -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]
|
linked_values = [link.from_socket.compute_data(kind) for link in self.links]
|
||||||
|
|
||||||
# Return Single Value / List of Values
|
# Return Single Value / List of Values
|
||||||
if len(linked_values) == 1:
|
## -> Multi-input sockets are not yet supported.
|
||||||
|
if linked_values:
|
||||||
return linked_values[0]
|
return linked_values[0]
|
||||||
|
|
||||||
# Edge Case: While Dragging Link (but not yet removed)
|
# Edge Case: While Dragging Link (but not yet removed)
|
||||||
|
@ -653,11 +766,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
||||||
## - self.is_linked = True, since the user hasn't confirmed anything.
|
## - self.is_linked = True, since the user hasn't confirmed anything.
|
||||||
## - self.links will be empty, since the link object was freed.
|
## - self.links will be empty, since the link object was freed.
|
||||||
## When this particular condition is met, pretend that we're not linked.
|
## When this particular condition is met, pretend that we're not linked.
|
||||||
if len(linked_values) == 0:
|
return self._compute_data(kind)
|
||||||
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
|
# - UI - Color
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# 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 contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
@ -25,8 +27,8 @@ class AnyBLSocket(base.MaxwellSimSocket):
|
||||||
socket_type = ct.SocketType.Any
|
socket_type = ct.SocketType.Any
|
||||||
bl_label = 'Any'
|
bl_label = 'Any'
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'active_kind'})
|
||||||
def capabilities(self):
|
def capabilities(self) -> ct.CapabilitiesFlow:
|
||||||
return ct.CapabilitiesFlow(
|
return ct.CapabilitiesFlow(
|
||||||
socket_type=self.socket_type,
|
socket_type=self.socket_type,
|
||||||
active_kind=self.active_kind,
|
active_kind=self.active_kind,
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
@ -43,7 +43,7 @@ class BoolBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Computation of Default Value
|
# - Computation of Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> bool:
|
def value(self) -> bool:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ class FilePathBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - FlowKind: Value
|
# - FlowKind: Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> Path:
|
def value(self) -> Path:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
@ -30,12 +32,7 @@ class StringBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.StringProperty(
|
raw_value: str = bl_cache.BLField('')
|
||||||
name='String',
|
|
||||||
description='Represents a string',
|
|
||||||
default='',
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Socket UI
|
# - Socket UI
|
||||||
|
@ -46,7 +43,7 @@ class StringBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Computation of Default Value
|
# - Computation of Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> str:
|
def value(self) -> str:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ class BlenderGeoNodesBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Default Value
|
# - Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> bpy.types.NodeTree | ct.FlowSignal:
|
def value(self) -> bpy.types.NodeTree | ct.FlowSignal:
|
||||||
return self.raw_value if self.raw_value is not None else ct.FlowSignal.NoFlow
|
return self.raw_value if self.raw_value is not None else ct.FlowSignal.NoFlow
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
@ -30,12 +32,7 @@ class BlenderImageBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.PointerProperty(
|
raw_value: bpy.types.Image = bl_cache.BLField()
|
||||||
name='Blender Image',
|
|
||||||
description='Represents a Blender Image',
|
|
||||||
type=bpy.types.Image,
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
|
@ -46,7 +43,7 @@ class BlenderImageBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Default Value
|
# - Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> bpy.types.Image | None:
|
def value(self) -> bpy.types.Image | None:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
@ -27,12 +29,7 @@ class BlenderMaterialBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.PointerProperty(
|
raw_value: bpy.types.Material = bl_cache.BLField()
|
||||||
name='Blender Material',
|
|
||||||
description='Represents a Blender material',
|
|
||||||
type=bpy.types.Material,
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
|
@ -43,7 +40,7 @@ class BlenderMaterialBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Default Value
|
# - Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> bpy.types.Material | None:
|
def value(self) -> bpy.types.Material | None:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
|
|
@ -16,29 +16,12 @@
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
####################
|
|
||||||
# - 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'}
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -51,47 +34,18 @@ class BlenderObjectBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.PointerProperty(
|
raw_value: bpy.types.Object = bl_cache.BLField()
|
||||||
name='Blender Object',
|
|
||||||
description='Represents a Blender object',
|
|
||||||
type=bpy.types.Object,
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - 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:
|
def draw_value(self, col: bpy.types.UILayout) -> None:
|
||||||
col.prop(self, 'raw_value', text='')
|
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
|
# - Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> bpy.types.Object | None:
|
def value(self) -> bpy.types.Object | None:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
@ -114,6 +68,5 @@ class BlenderObjectSocketDef(base.SocketDef):
|
||||||
# - Blender Registration
|
# - Blender Registration
|
||||||
####################
|
####################
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
BlenderMaxwellCreateAndAssignBLObject,
|
|
||||||
BlenderObjectBLSocket,
|
BlenderObjectBLSocket,
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,9 +16,13 @@
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Blender Socket
|
# - Blender Socket
|
||||||
|
@ -30,12 +34,7 @@ class BlenderTextBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
raw_value: bpy.props.PointerProperty(
|
raw_value: bpy.types.Text = bl_cache.BLField()
|
||||||
name='Blender Text',
|
|
||||||
description='Represents a Blender text datablock',
|
|
||||||
type=bpy.types.Text,
|
|
||||||
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
|
@ -46,7 +45,7 @@ class BlenderTextBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Default Value
|
# - Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'raw_value'})
|
||||||
def value(self) -> bpy.types.Text:
|
def value(self) -> bpy.types.Text:
|
||||||
return self.raw_value
|
return self.raw_value
|
||||||
|
|
||||||
|
|
|
@ -64,21 +64,21 @@ class InfoDisplayCol(enum.StrEnum):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_name(value: typ.Self) -> str:
|
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
|
IDC = InfoDisplayCol
|
||||||
return {
|
return {
|
||||||
IDC.Length: 'L',
|
IDC.Length: 'L',
|
||||||
IDC.MathType: '∈',
|
IDC.MathType: 'M',
|
||||||
IDC.Unit: 'U',
|
IDC.Unit: 'U',
|
||||||
}[value]
|
}[value]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_icon(value: typ.Self) -> str:
|
def to_icon(_: typ.Self) -> str:
|
||||||
IDC = InfoDisplayCol
|
"""No icons."""
|
||||||
return {
|
return ''
|
||||||
IDC.Length: '',
|
|
||||||
IDC.MathType: '',
|
|
||||||
IDC.Unit: '',
|
|
||||||
}[value]
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -109,6 +109,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
|
|
||||||
socket_type = ct.SocketType.Expr
|
socket_type = ct.SocketType.Expr
|
||||||
bl_label = 'Expr'
|
bl_label = 'Expr'
|
||||||
|
use_socket_color = True
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Socket Interface
|
# - Socket Interface
|
||||||
|
@ -117,6 +118,58 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real)
|
mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real)
|
||||||
physical_type: spux.PhysicalType = bl_cache.BLField(spux.PhysicalType.NonPhysical)
|
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
|
# - Symbols
|
||||||
####################
|
####################
|
||||||
|
@ -140,6 +193,11 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
"""Computes `sympy` symbols from `self.sorted_symbols`."""
|
"""Computes `sympy` symbols from `self.sorted_symbols`."""
|
||||||
return [sym.sp_symbol_matsym for sym in 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
|
# - Units
|
||||||
####################
|
####################
|
||||||
|
@ -171,8 +229,13 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'unit'})
|
||||||
def unit_factor(self) -> spux.Unit | None:
|
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
|
return sp.Integer(1) if self.unit is None else self.unit
|
||||||
|
|
||||||
prev_unit: str | None = bl_cache.BLField(None)
|
prev_unit: str | None = bl_cache.BLField(None)
|
||||||
|
@ -228,26 +291,92 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Computed String Expressions
|
# - 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:
|
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)
|
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:
|
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)
|
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:
|
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)
|
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
|
# Conditional Unit-Conversion
|
||||||
## -> This is niche functionality, but the only way to convert units.
|
## -> 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.
|
## -> 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
|
# Check Unit Change
|
||||||
## -> self.prev_unit only updates here; "lags" behind self.unit.
|
## -> self.prev_unit only updates here; "lags" behind self.unit.
|
||||||
## -> 1. "Laggy" unit must be different than new unit.
|
## -> 1. "Laggy" unit must be different than new unit.
|
||||||
|
@ -272,37 +401,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Value Utilities
|
# - 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):
|
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`."""
|
"""Cast the given expression to the appropriate raw value, with scaling guided by `self.unit`."""
|
||||||
if self.unit is not None:
|
if self.unit is not None:
|
||||||
|
@ -324,38 +422,117 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
|
|
||||||
return pyvalue
|
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:
|
def _parse_expr_str(self, expr_spstr: str) -> spux.SympyExpr | None:
|
||||||
"""Parse an expression string by choosing opinionated options for `sp.sympify`.
|
"""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:
|
Returns:
|
||||||
The parsed expression, if it manages to validate; else None.
|
The parsed expression, if it manages to validate; else None.
|
||||||
"""
|
"""
|
||||||
expr = sp.sympify(
|
expr = sp.parsing.sympy_parser.parse_expr(
|
||||||
expr_spstr,
|
expr_spstr,
|
||||||
locals={sym.name: sym.sp_symbol_matsym for sym in self.symbols},
|
local_dict=(
|
||||||
strict=False,
|
{sym.name: sym.sp_symbol_matsym for sym in self.symbols}
|
||||||
convert_xor=True,
|
| {sym.name: unit for sym, unit in spux.UNIT_BY_SYMBOL.items()}
|
||||||
).subs(spux.UNIT_BY_SYMBOL)
|
),
|
||||||
|
transformations=[
|
||||||
|
# Lambda Notation: Symbolic Anonymous Functions
|
||||||
|
## -> Interpret 'lambda: x/8' to sp.Lambda((), x/0)
|
||||||
|
sp.parsing.sympy_parser.lambda_notation,
|
||||||
|
# Automatic Symbols
|
||||||
|
## -> Interpret known functions as their sympy equivs.
|
||||||
|
## -> Interpret unknown 'x' as sp.Symbol('x')
|
||||||
|
## -> NOTE: Must check for extraneous/unwelcome unknowns.
|
||||||
|
sp.parsing.sympy_parser.auto_symbol,
|
||||||
|
# Repeated Decimals
|
||||||
|
## -> Interpret '0.2[1]' as 0.211111...
|
||||||
|
sp.parsing.sympy_parser.repeated_decimals,
|
||||||
|
# Number Literals
|
||||||
|
## -> Interpret ints/float literals.
|
||||||
|
## -> Interpret 'I' as the imaginary number literal.
|
||||||
|
## -> TODO: Maybe special-case the variable name 'I'?
|
||||||
|
sp.parsing.sympy_parser.auto_number,
|
||||||
|
# Factorial Notation
|
||||||
|
## -> Allow 'x!' to be the factorial of x.
|
||||||
|
sp.parsing.sympy_parser.factorial_notation,
|
||||||
|
# Rationalize Float -> Rational
|
||||||
|
## -> Helps numerical stability for pure-symbolic math.
|
||||||
|
## -> AFTER auto_number
|
||||||
|
sp.parsing.sympy_parser.rationalize,
|
||||||
|
# Carrot Exponentiation
|
||||||
|
## -> Interpret '^' as power, instead of as XOR.
|
||||||
|
sp.parsing.sympy_parser.convert_xor,
|
||||||
|
# Symbol Splitting
|
||||||
|
## -> Interpret 'xyz' as 'x*y*z' for convenience.
|
||||||
|
## -> NEVER split greek character names (ex. theta).
|
||||||
|
## -> NEVER split symbol names in 'self.symbols'.
|
||||||
|
sp.parsing.sympy_parser.split_symbols_custom(
|
||||||
|
predicate=lambda sym_name: (
|
||||||
|
sp.parsing.sympy_parser._token_splittable(sym_name) # noqa: SLF001
|
||||||
|
if sym_name not in self.sorted_symbol_names
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
),
|
||||||
|
# Implicit Mult/Call
|
||||||
|
## -> Most times, allow '2x' as '2*x' / '2 x y' as '2*x*y'.
|
||||||
|
## -> Sometimes, allow 'sin 2x' as 'sin(2*x)'
|
||||||
|
## -> Allow functions to be exponentiated ex. 'sin^2 x'
|
||||||
|
sp.parsing.sympy_parser.implicit_multiplication,
|
||||||
|
sp.parsing.sympy_parser.implicit_application,
|
||||||
|
sp.parsing.sympy_parser.function_exponentiation,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
# Try Parsing and Returning the Expression
|
if self._parse_expr_symbol(expr) is not None:
|
||||||
try:
|
|
||||||
self._parse_expr_info(expr)
|
|
||||||
except ValueError:
|
|
||||||
log.exception(
|
|
||||||
'Couldn\'t parse expression "%s" in Expr socket.',
|
|
||||||
expr_spstr,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return expr
|
return expr
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - FlowKind: Value
|
# - 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:
|
def value(self) -> spux.SympyExpr:
|
||||||
"""Return the expression defined by the socket as `FlowKind.Value`.
|
"""Return the expression defined by the socket as `FlowKind.Value`.
|
||||||
|
|
||||||
|
@ -382,8 +559,8 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
## -> ExprSocket doesn't support Vec4 (yet?).
|
## -> ExprSocket doesn't support Vec4 (yet?).
|
||||||
## -> I mean, have you _seen_ that mess of attributes up top?
|
## -> I mean, have you _seen_ that mess of attributes up top?
|
||||||
NS = spux.NumberSize1D
|
NS = spux.NumberSize1D
|
||||||
if self.size == NS.Vec4:
|
if self.size is NS.Vec4:
|
||||||
return ct.Flow
|
return ct.FlowSignal.NoFlow
|
||||||
|
|
||||||
MT_Z = spux.MathType.Integer
|
MT_Z = spux.MathType.Integer
|
||||||
MT_Q = spux.MathType.Rational
|
MT_Q = spux.MathType.Rational
|
||||||
|
@ -430,7 +607,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
Notes:
|
Notes:
|
||||||
Called to set the internal `FlowKind.Value` of this socket.
|
Called to set the internal `FlowKind.Value` of this socket.
|
||||||
"""
|
"""
|
||||||
_mathtype, _size = self._parse_expr_info(expr)
|
|
||||||
if self.symbols:
|
if self.symbols:
|
||||||
self.raw_value_spstr = sp.sstr(expr)
|
self.raw_value_spstr = sp.sstr(expr)
|
||||||
else:
|
else:
|
||||||
|
@ -473,7 +649,22 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - FlowKind: Range
|
# - 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:
|
def lazy_range(self) -> ct.RangeFlow:
|
||||||
"""Return the not-yet-computed uniform array defined by the socket.
|
"""Return the not-yet-computed uniform array defined by the socket.
|
||||||
|
|
||||||
|
@ -519,18 +710,18 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
)
|
)
|
||||||
|
|
||||||
@lazy_range.setter
|
@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.
|
"""Set the not-yet-computed uniform array defined by the socket.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Called to compute the internal `FlowKind.Range` of this socket.
|
Called to compute the internal `FlowKind.Range` of this socket.
|
||||||
"""
|
"""
|
||||||
self.steps = value.steps
|
self.steps = lazy_range.steps
|
||||||
self.scaling = value.scaling
|
self.scaling = lazy_range.scaling
|
||||||
|
|
||||||
if self.symbols:
|
if self.symbols:
|
||||||
self.raw_min_spstr = sp.sstr(value.start)
|
self.raw_min_spstr = sp.sstr(lazy_range.start)
|
||||||
self.raw_max_spstr = sp.sstr(value.stop)
|
self.raw_max_spstr = sp.sstr(lazy_range.stop)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
MT_Z = spux.MathType.Integer
|
MT_Z = spux.MathType.Integer
|
||||||
|
@ -538,32 +729,40 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
MT_R = spux.MathType.Real
|
MT_R = spux.MathType.Real
|
||||||
MT_C = spux.MathType.Complex
|
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:
|
if self.mathtype == MT_Z:
|
||||||
self.raw_range_int = [
|
self.raw_range_int = [
|
||||||
self._to_raw_value(bound * unit)
|
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:
|
elif self.mathtype == MT_Q:
|
||||||
self.raw_range_rat = [
|
self.raw_range_rat = [
|
||||||
self._to_raw_value(bound * unit)
|
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:
|
elif self.mathtype == MT_R:
|
||||||
self.raw_range_float = [
|
self.raw_range_float = [
|
||||||
self._to_raw_value(bound * unit)
|
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:
|
elif self.mathtype == MT_C:
|
||||||
self.raw_range_complex = [
|
self.raw_range_complex = [
|
||||||
self._to_raw_value(bound * unit, force_complex=True)
|
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)
|
# - 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:
|
def lazy_func(self) -> ct.FuncFlow:
|
||||||
"""Returns a lazy value that computes the expression returned by `self.value`.
|
"""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.
|
## -> `self.value` is guaranteed to be an expression with unknowns.
|
||||||
## -> The function computes `self.value` with unknowns as arguments.
|
## -> The function computes `self.value` with unknowns as arguments.
|
||||||
if self.symbols:
|
if self.symbols:
|
||||||
return ct.FuncFlow(
|
value = self.value
|
||||||
func=sp.lambdify(
|
has_value = not ct.FlowSignal.check(value)
|
||||||
self.sorted_sp_symbols,
|
|
||||||
spux.strip_unit_system(self.value),
|
output_sym = self.output_sym
|
||||||
'jax',
|
if output_sym is not None and has_value:
|
||||||
),
|
return ct.FuncFlow(
|
||||||
func_args=list(self.sorted_symbols),
|
func=sp.lambdify(
|
||||||
supports_jax=True,
|
self.sorted_sp_symbols,
|
||||||
)
|
output_sym.conform(value, strip_unit=True),
|
||||||
|
'jax',
|
||||||
|
),
|
||||||
|
func_args=list(self.sorted_symbols),
|
||||||
|
supports_jax=True,
|
||||||
|
)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
# Constant
|
# Constant
|
||||||
## -> When a `self.value` has no unknowns, use a dummy function.
|
## -> 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.
|
## -> Generally only useful for operations with other expressions.
|
||||||
return ct.FuncFlow(
|
return ct.FuncFlow(
|
||||||
func=lambda v: v,
|
func=lambda v: v,
|
||||||
func_args=[
|
func_args=[self.output_sym],
|
||||||
sim_symbols.SimSymbol.from_expr(
|
|
||||||
sim_symbols.SimSymbolName.Constant, self.value, self.unit_factor
|
|
||||||
)
|
|
||||||
],
|
|
||||||
supports_jax=True,
|
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:
|
def params(self) -> ct.ParamsFlow:
|
||||||
"""Returns parameter symbols/values to accompany `self.lazy_func`.
|
"""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.
|
## -> They should be realized later, ex. in a Viz node.
|
||||||
## -> Therefore, we just dump the symbols. Easy!
|
## -> Therefore, we just dump the symbols. Easy!
|
||||||
## -> NOTE: func_args must have the same symbol order as was lambdified.
|
## -> NOTE: func_args must have the same symbol order as was lambdified.
|
||||||
if self.symbols:
|
if self.sorted_symbols:
|
||||||
return ct.ParamsFlow(
|
output_sym = self.output_sym
|
||||||
func_args=[sym.sp_symbol_phy for sym in self.sorted_symbols],
|
if output_sym is not None:
|
||||||
symbols=self.sorted_symbols,
|
return ct.ParamsFlow(
|
||||||
)
|
arg_targets=list(self.sorted_symbols),
|
||||||
|
func_args=[sym.sp_symbol for sym in self.sorted_symbols],
|
||||||
|
symbols=self.sorted_symbols,
|
||||||
|
is_differentiable=self.is_differentiable,
|
||||||
|
)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
# Constant
|
# Constant
|
||||||
## -> Simply pass self.value verbatim as a function argument.
|
## -> Simply pass self.value verbatim as a function argument.
|
||||||
## -> Easy dice, easy life!
|
## -> 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
|
@bl_cache.cached_bl_property(depends_on={'sorted_symbols', 'output_sym'})
|
||||||
def info(self) -> ct.ArrayFlow:
|
def info(self) -> ct.InfoFlow:
|
||||||
r"""Returns parameter symbols/values to accompany `self.lazy_func`.
|
r"""Returns parameter symbols/values to accompany `self.lazy_func`.
|
||||||
|
|
||||||
The output name/size/mathtype/unit corresponds directly the `ExprSocket`.
|
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.
|
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
|
# Constant
|
||||||
## -> The input SimSymbols become continuous dimensional indices.
|
## -> The input SimSymbols become continuous dimensional indices.
|
||||||
## -> All domain validity information is defined on the SimSymbol keys.
|
## -> All domain validity information is defined on the SimSymbol keys.
|
||||||
if self.symbols:
|
if self.sorted_symbols:
|
||||||
return ct.InfoFlow(
|
output_sym = self.output_sym
|
||||||
dims={sym: None for sym in self.sorted_symbols},
|
if output_sym is not None:
|
||||||
output=output_sym,
|
return ct.InfoFlow(
|
||||||
)
|
dims={sym: None for sym in self.sorted_symbols},
|
||||||
|
output=self.output_sym,
|
||||||
|
)
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
# Constant
|
# Constant
|
||||||
## -> We only need the output symbol to describe the raw data.
|
## -> 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
|
# - FlowKind: Capabilities
|
||||||
####################
|
####################
|
||||||
@property
|
def linked_capabilities(self, info: ct.InfoFlow) -> ct.CapabilitiesFlow:
|
||||||
def capabilities(self) -> None:
|
"""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(
|
return ct.CapabilitiesFlow(
|
||||||
socket_type=self.socket_type,
|
socket_type=self.socket_type,
|
||||||
active_kind=self.active_kind,
|
active_kind=self.active_kind,
|
||||||
allow_out_to_in={ct.FlowKind.Func: ct.FlowKind.Value},
|
allow_out_to_in={
|
||||||
|
ct.FlowKind.Func: ct.FlowKind.Value,
|
||||||
|
},
|
||||||
|
allow_out_to_in_if_matches={
|
||||||
|
ct.FlowKind.Value: (
|
||||||
|
ct.FlowKind.Func,
|
||||||
|
(
|
||||||
|
info.output.physical_type,
|
||||||
|
info.output.mathtype,
|
||||||
|
info.output.rows,
|
||||||
|
info.output.cols,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@bl_cache.cached_bl_property(depends_on={'active_kind', 'output_sym'})
|
||||||
|
def capabilities(self) -> ct.CapabilitiesFlow:
|
||||||
|
"""Expose capabilities for use when checking socket link compatibility.
|
||||||
|
|
||||||
|
Only used when `use_linked_capabilities == True`.
|
||||||
|
"""
|
||||||
|
output_sym = self.output_sym
|
||||||
|
if output_sym is not None:
|
||||||
|
allow_out_to_in_if_matches = {
|
||||||
|
ct.FlowKind.Value: (
|
||||||
|
ct.FlowKind.Func,
|
||||||
|
(
|
||||||
|
output_sym.physical_type,
|
||||||
|
output_sym.mathtype,
|
||||||
|
output_sym.rows,
|
||||||
|
output_sym.cols,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
allow_out_to_in_if_matches = {}
|
||||||
|
|
||||||
|
return ct.CapabilitiesFlow(
|
||||||
|
socket_type=self.socket_type,
|
||||||
|
active_kind=self.active_kind,
|
||||||
|
allow_out_to_in={
|
||||||
|
ct.FlowKind.Func: ct.FlowKind.Value,
|
||||||
|
},
|
||||||
|
allow_out_to_in_if_matches=allow_out_to_in_if_matches,
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -692,29 +957,32 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
Notes:
|
Notes:
|
||||||
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
|
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
|
||||||
"""
|
"""
|
||||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
if self.active_kind is ct.FlowKind.Func:
|
||||||
has_info = not ct.FlowSignal.check(info)
|
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||||
|
has_info = not ct.FlowSignal.check(info)
|
||||||
|
|
||||||
if has_info:
|
if has_info:
|
||||||
split = row.split(factor=0.85, align=True)
|
split = row.split(factor=0.85, align=True)
|
||||||
_row = split.row(align=False)
|
_row = split.row(align=False)
|
||||||
|
else:
|
||||||
|
_row = row
|
||||||
|
|
||||||
|
_row.label(text=text)
|
||||||
|
if has_info:
|
||||||
|
if self.show_info_columns:
|
||||||
|
_row.prop(self, self.blfields['info_columns'])
|
||||||
|
|
||||||
|
_row = split.row(align=True)
|
||||||
|
_row.alignment = 'RIGHT'
|
||||||
|
_row.prop(
|
||||||
|
self,
|
||||||
|
self.blfields['show_info_columns'],
|
||||||
|
toggle=True,
|
||||||
|
text='',
|
||||||
|
icon=ct.Icon.ToggleSocketInfo,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_row = row
|
row.label(text=text)
|
||||||
|
|
||||||
_row.label(text=text)
|
|
||||||
if has_info:
|
|
||||||
if self.show_info_columns:
|
|
||||||
_row.prop(self, self.blfields['info_columns'])
|
|
||||||
|
|
||||||
_row = split.row(align=True)
|
|
||||||
_row.alignment = 'RIGHT'
|
|
||||||
_row.prop(
|
|
||||||
self,
|
|
||||||
self.blfields['show_info_columns'],
|
|
||||||
toggle=True,
|
|
||||||
text='',
|
|
||||||
icon=ct.Icon.ToggleSocketInfo,
|
|
||||||
)
|
|
||||||
|
|
||||||
def draw_output_label_row(self, row: bpy.types.UILayout, text) -> None:
|
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.
|
"""Provide a dropdown for enabling the `InfoFlow` UI in the linked output label row.
|
||||||
|
@ -724,29 +992,32 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
Notes:
|
Notes:
|
||||||
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
|
Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`.
|
||||||
"""
|
"""
|
||||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
if self.active_kind is ct.FlowKind.Func:
|
||||||
has_info = not ct.FlowSignal.check(info)
|
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||||
|
has_info = not ct.FlowSignal.check(info)
|
||||||
|
|
||||||
if has_info:
|
if has_info:
|
||||||
split = row.split(factor=0.15, align=True)
|
split = row.split(factor=0.15, align=True)
|
||||||
|
|
||||||
_row = split.row(align=True)
|
_row = split.row(align=True)
|
||||||
_row.prop(
|
_row.prop(
|
||||||
self,
|
self,
|
||||||
self.blfields['show_info_columns'],
|
self.blfields['show_info_columns'],
|
||||||
toggle=True,
|
toggle=True,
|
||||||
text='',
|
text='',
|
||||||
icon=ct.Icon.ToggleSocketInfo,
|
icon=ct.Icon.ToggleSocketInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
_row = split.row(align=False)
|
_row = split.row(align=False)
|
||||||
_row.alignment = 'RIGHT'
|
_row.alignment = 'RIGHT'
|
||||||
if self.show_info_columns:
|
if self.show_info_columns:
|
||||||
_row.prop(self, self.blfields['info_columns'])
|
_row.prop(self, self.blfields['info_columns'])
|
||||||
|
else:
|
||||||
|
_col = _row.column()
|
||||||
|
_col.alignment = 'EXPAND'
|
||||||
|
_col.label(text='')
|
||||||
else:
|
else:
|
||||||
_col = _row.column()
|
_row = row
|
||||||
_col.alignment = 'EXPAND'
|
|
||||||
_col.label(text='')
|
|
||||||
else:
|
else:
|
||||||
_row = row
|
_row = row
|
||||||
|
|
||||||
|
@ -860,42 +1131,38 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
Uses `draw_value` to draw the base UI
|
Uses `draw_value` to draw the base UI
|
||||||
"""
|
"""
|
||||||
if self.show_func_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
|
# Output Name Selector
|
||||||
## -> The name of the output
|
## -> The name of the output
|
||||||
if self.show_name_selector:
|
if self.show_name_selector:
|
||||||
row = col.row()
|
row = col.row()
|
||||||
|
row.alignment = 'CENTER'
|
||||||
row.prop(self, self.blfields['output_name'], text='Name')
|
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
|
# - UI: InfoFlow
|
||||||
####################
|
####################
|
||||||
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
|
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
|
||||||
"""Visualize the `InfoFlow` information passing through the socket."""
|
"""Visualize the `InfoFlow` information passing through the socket."""
|
||||||
if (
|
if (
|
||||||
self.active_kind == ct.FlowKind.Func
|
self.active_kind is ct.FlowKind.Func
|
||||||
and self.show_info_columns
|
and self.show_info_columns
|
||||||
and self.is_linked
|
and (self.is_linked or self.is_output)
|
||||||
):
|
):
|
||||||
row = col.row()
|
row = col.row()
|
||||||
box = row.box()
|
box = row.box()
|
||||||
|
@ -922,7 +1189,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
|
||||||
if InfoDisplayCol.Length in self.info_columns:
|
if InfoDisplayCol.Length in self.info_columns:
|
||||||
grid.label(text='', icon=ct.Icon.DataSocketOutput)
|
grid.label(text='', icon=ct.Icon.DataSocketOutput)
|
||||||
if InfoDisplayCol.MathType in self.info_columns:
|
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:
|
if InfoDisplayCol.Unit in self.info_columns:
|
||||||
grid.label(text=info.output.unit_label)
|
grid.label(text=info.output.unit_label)
|
||||||
|
|
||||||
|
@ -935,7 +1202,6 @@ class ExprSocketDef(base.SocketDef):
|
||||||
active_kind: typ.Literal[
|
active_kind: typ.Literal[
|
||||||
ct.FlowKind.Value,
|
ct.FlowKind.Value,
|
||||||
ct.FlowKind.Range,
|
ct.FlowKind.Range,
|
||||||
ct.FlowKind.Array,
|
|
||||||
ct.FlowKind.Func,
|
ct.FlowKind.Func,
|
||||||
] = ct.FlowKind.Value
|
] = ct.FlowKind.Value
|
||||||
output_name: sim_symbols.SimSymbolName = sim_symbols.SimSymbolName.Expr
|
output_name: sim_symbols.SimSymbolName = sim_symbols.SimSymbolName.Expr
|
||||||
|
@ -1240,6 +1506,7 @@ class ExprSocketDef(base.SocketDef):
|
||||||
def init(self, bl_socket: ExprBLSocket) -> None:
|
def init(self, bl_socket: ExprBLSocket) -> None:
|
||||||
bl_socket.active_kind = self.active_kind
|
bl_socket.active_kind = self.active_kind
|
||||||
bl_socket.output_name = self.output_name
|
bl_socket.output_name = self.output_name
|
||||||
|
bl_socket.use_linked_capabilities = True
|
||||||
|
|
||||||
# Socket Interface
|
# Socket Interface
|
||||||
## -> Recall that auto-updates are turned off during init()
|
## -> 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/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
import pydantic as pyd
|
||||||
import tidy3d as td
|
import tidy3d as td
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
@ -59,7 +60,9 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - FlowKind
|
# - FlowKind
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(
|
||||||
|
depends_on={'active_kind', 'allow_axes', 'present_axes'}
|
||||||
|
)
|
||||||
def capabilities(self) -> ct.CapabilitiesFlow:
|
def capabilities(self) -> ct.CapabilitiesFlow:
|
||||||
return ct.CapabilitiesFlow(
|
return ct.CapabilitiesFlow(
|
||||||
socket_type=self.socket_type,
|
socket_type=self.socket_type,
|
||||||
|
@ -68,7 +71,7 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
|
||||||
present_any=self.present_axes,
|
present_any=self.present_axes,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'default'})
|
||||||
def value(self) -> td.BoundaryEdge:
|
def value(self) -> td.BoundaryEdge:
|
||||||
return self.default.tidy3d_boundary_edge
|
return self.default.tidy3d_boundary_edge
|
||||||
|
|
||||||
|
@ -84,16 +87,20 @@ class MaxwellBoundCondSocketDef(base.SocketDef):
|
||||||
socket_type: ct.SocketType = ct.SocketType.MaxwellBoundCond
|
socket_type: ct.SocketType = ct.SocketType.MaxwellBoundCond
|
||||||
|
|
||||||
default: ct.BoundCondType = ct.BoundCondType.Pml
|
default: ct.BoundCondType = ct.BoundCondType.Pml
|
||||||
allow_axes: set[ct.SimSpaceAxis] = {
|
allow_axes: set[ct.SimSpaceAxis] = pyd.Field(
|
||||||
ct.SimSpaceAxis.X,
|
default={
|
||||||
ct.SimSpaceAxis.Y,
|
ct.SimSpaceAxis.X,
|
||||||
ct.SimSpaceAxis.Z,
|
ct.SimSpaceAxis.Y,
|
||||||
}
|
ct.SimSpaceAxis.Z,
|
||||||
present_axes: set[ct.SimSpaceAxis] = {
|
}
|
||||||
ct.SimSpaceAxis.X,
|
)
|
||||||
ct.SimSpaceAxis.Y,
|
present_axes: set[ct.SimSpaceAxis] = pyd.Field(
|
||||||
ct.SimSpaceAxis.Z,
|
default={
|
||||||
}
|
ct.SimSpaceAxis.X,
|
||||||
|
ct.SimSpaceAxis.Y,
|
||||||
|
ct.SimSpaceAxis.Z,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
def init(self, bl_socket: MaxwellBoundCondBLSocket) -> None:
|
def init(self, bl_socket: MaxwellBoundCondBLSocket) -> None:
|
||||||
bl_socket.default = self.default
|
bl_socket.default = self.default
|
||||||
|
|
|
@ -86,7 +86,9 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Computation of Default Value
|
# - 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:
|
def value(self) -> td.BoundarySpec:
|
||||||
"""Compute a user-defined default value for simulation boundary conditions, from certain common/sensible options.
|
"""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 scipy as sc
|
||||||
import sympy.physics.units as spu
|
import sympy.physics.units as spu
|
||||||
import tidy3d as td
|
import tidy3d as td
|
||||||
|
import tidy3d.plugins.adjoint as tdadj
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
|
@ -39,12 +40,14 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - 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
|
# - FlowKinds
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'eps_rel', 'differentiable'})
|
||||||
def value(self) -> td.Medium:
|
def value(self) -> td.Medium:
|
||||||
freq = (
|
freq = (
|
||||||
spu.convert_to(
|
spu.convert_to(
|
||||||
|
@ -53,31 +56,49 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
|
||||||
)
|
)
|
||||||
/ spu.hertz
|
/ 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(
|
return td.Medium.from_nk(
|
||||||
n=self.rel_permittivity[0],
|
n=self.eps_rel[0],
|
||||||
k=self.rel_permittivity[1],
|
k=self.eps_rel[1],
|
||||||
freq=freq,
|
freq=freq,
|
||||||
)
|
)
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(
|
def value(self, eps_rel: tuple[float, float]) -> None:
|
||||||
self, value: tuple[spux.ConstrSympyExpr(allow_variables=False), complex]
|
self.eps_rel = eps_rel
|
||||||
) -> None:
|
|
||||||
rel_permittivity = value
|
|
||||||
|
|
||||||
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
|
# - UI
|
||||||
####################
|
####################
|
||||||
def draw_value(self, col: bpy.types.UILayout) -> None:
|
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 = split.column(align=True)
|
||||||
col.label(text='ϵ_r (ℂ)')
|
_col.label(text='εᵣ')
|
||||||
|
|
||||||
col = split.column(align=True)
|
_col = split.column(align=True)
|
||||||
col.prop(self, self.blfields['rel_permittivity'], text='')
|
_col.prop(self, self.blfields['eps_rel'], text='')
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -90,7 +111,7 @@ class MaxwellMediumSocketDef(base.SocketDef):
|
||||||
default_permittivity_imag: float = 0.0
|
default_permittivity_imag: float = 0.0
|
||||||
|
|
||||||
def init(self, bl_socket: MaxwellMediumBLSocket) -> None:
|
def init(self, bl_socket: MaxwellMediumBLSocket) -> None:
|
||||||
bl_socket.rel_permittivity = (
|
bl_socket.eps_rel = (
|
||||||
self.default_permittivity_real,
|
self.default_permittivity_real,
|
||||||
self.default_permittivity_imag,
|
self.default_permittivity_imag,
|
||||||
)
|
)
|
||||||
|
|
|
@ -49,7 +49,7 @@ class MaxwellSimGridBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Computation of Default Value
|
# - Computation of Default Value
|
||||||
####################
|
####################
|
||||||
@property
|
@bl_cache.cached_bl_property(depends_on={'min_steps_per_wl'})
|
||||||
def value(self) -> td.GridSpec:
|
def value(self) -> td.GridSpec:
|
||||||
return td.GridSpec.auto(
|
return td.GridSpec.auto(
|
||||||
min_steps_per_wvl=self.min_steps_per_wl,
|
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)
|
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.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.ResetEnumItems
|
||||||
|
bl_socket.existing_task_id = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -77,7 +79,9 @@ class Authenticate(bpy.types.Operator):
|
||||||
bl_socket.api_key = ''
|
bl_socket.api_key = ''
|
||||||
|
|
||||||
bl_socket.existing_folder_id = bl_cache.Signal.ResetEnumItems
|
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.ResetEnumItems
|
||||||
|
bl_socket.existing_task_id = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -102,62 +106,18 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - 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)
|
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(
|
existing_folder_id: enum.StrEnum = bl_cache.BLField(
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
|
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()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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]:
|
def search_cloud_folders(self) -> list[ct.BLEnumElement]:
|
||||||
if tdcloud.IS_AUTHENTICATED:
|
if tdcloud.IS_AUTHENTICATED:
|
||||||
return [
|
return [
|
||||||
|
@ -175,6 +135,13 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
|
|
||||||
return []
|
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]:
|
def search_cloud_tasks(self) -> list[ct.BLEnumElement]:
|
||||||
if self.existing_folder_id is None or not tdcloud.IS_AUTHENTICATED:
|
if self.existing_folder_id is None or not tdcloud.IS_AUTHENTICATED:
|
||||||
return []
|
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
|
# - UI
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
@ -166,7 +167,7 @@ class BLField:
|
||||||
self.cb_depends_on: set[str] | None = cb_depends_on
|
self.cb_depends_on: set[str] | None = cb_depends_on
|
||||||
|
|
||||||
# Update Suppressing
|
# Update Suppressing
|
||||||
self.suppress_update: dict[str, bool] = {}
|
self.suppressed_update: dict[str, bool] = {}
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Descriptor Setup
|
# - Descriptor Setup
|
||||||
|
@ -253,9 +254,38 @@ class BLField:
|
||||||
return self.bl_prop.default_value ## TODO: Good idea?
|
return self.bl_prop.default_value ## TODO: Good idea?
|
||||||
return cached_value
|
return cached_value
|
||||||
|
|
||||||
def suppress_next_update(self, bl_instance) -> None:
|
@contextlib.contextmanager
|
||||||
self.suppress_update[bl_instance.instance_id] = True
|
def suppress_update(self, bl_instance: bl_instance.BLInstance) -> None:
|
||||||
## TODO: Make it a context manager to prevent the worst of surprises
|
"""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__(
|
def __set__(
|
||||||
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
|
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
|
||||||
|
@ -263,7 +293,7 @@ class BLField:
|
||||||
"""Sets the value described by the BLField.
|
"""Sets the value described by the BLField.
|
||||||
|
|
||||||
In general, any BLField modified in the UI will set `InvalidateCache` on this descriptor.
|
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:
|
Notes:
|
||||||
Run by Python when the attribute described by the descriptor is set.
|
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.
|
bl_instance: Instance that is accessing the attribute.
|
||||||
owner: The class that owns the instance.
|
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
|
# Invalidate Cache
|
||||||
## -> This empties the non-persistent cache.
|
## -> This empties the non-persistent cache.
|
||||||
## -> As a result, the value must be reloaded from the property.
|
## -> As a result, the value must be reloaded from the property.
|
||||||
## The 'on_prop_changed' method on the bl_instance might also be called.
|
## 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)
|
self.bl_prop.invalidate_nonpersist(bl_instance)
|
||||||
|
|
||||||
# Update Suppression
|
# Trigger Update Chain
|
||||||
if self.suppress_update.get(bl_instance.instance_id):
|
## -> User can disable w/'use_prop_update=False'.
|
||||||
self.suppress_update[bl_instance.instance_id] = False
|
## -> Use InvalidateCacheNoUpdate to explicitly disable update.
|
||||||
|
## -> If 'suppressed_update' context manager is active, don't update.
|
||||||
# ELSE: Trigger Update Chain
|
if (
|
||||||
elif self.prop_info['use_prop_update'] and value is Signal.InvalidateCache:
|
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)
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
# Reset Enum Items
|
# 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:
|
elif value is Signal.ResetEnumItems:
|
||||||
if self.bl_prop_enum_items is None:
|
if self.bl_prop_enum_items is None:
|
||||||
return
|
return
|
||||||
|
@ -335,8 +366,8 @@ class BLField:
|
||||||
# Swap Enum Items
|
# Swap Enum Items
|
||||||
## -> This is the hot stuff - the enum elements are overwritten.
|
## -> This is the hot stuff - the enum elements are overwritten.
|
||||||
## -> The safe_enum_cb will pick up on this immediately.
|
## -> 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)
|
self.bl_prop_enum_items.write(bl_instance, current_items)
|
||||||
|
|
||||||
# Old Item in Current Items
|
# Old Item in Current Items
|
||||||
## -> It's possible that the old enum key is in the new enum.
|
## -> It's possible that the old enum key is in the new enum.
|
||||||
|
@ -344,9 +375,8 @@ class BLField:
|
||||||
## -> Thus, we set it - Blender sees a change, user doesn't.
|
## -> Thus, we set it - Blender sees a change, user doesn't.
|
||||||
## -> DO NOT trigger on_prop_changed (since "nothing changed").
|
## -> DO NOT trigger on_prop_changed (since "nothing changed").
|
||||||
if any(raw_old_item == item[0] for item in current_items):
|
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)
|
self.bl_prop.write(bl_instance, old_item)
|
||||||
## -> TODO: Don't write if not needed.
|
|
||||||
|
|
||||||
# Old Item Not in Current Items
|
# Old Item Not in Current Items
|
||||||
## -> In this case, fallback to the first current item.
|
## -> In this case, fallback to the first current item.
|
||||||
|
@ -355,28 +385,27 @@ class BLField:
|
||||||
raw_first_current_item = current_items[0][0]
|
raw_first_current_item = current_items[0][0]
|
||||||
first_current_item = self.bl_prop.decode(raw_first_current_item)
|
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)
|
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
|
# Reset Str Search
|
||||||
|
## -> If there is no string search method, do nothing.
|
||||||
|
## -> Simply invalidate the non-persistent cache
|
||||||
elif value is Signal.ResetStrSearch:
|
elif value is Signal.ResetStrSearch:
|
||||||
if self.bl_prop_str_search is None:
|
if self.bl_prop_str_search is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
|
self.bl_prop_str_search.invalidate_nonpersist(bl_instance)
|
||||||
|
|
||||||
# General __set__
|
# Default __set__
|
||||||
else:
|
else:
|
||||||
self.bl_prop.write(bl_instance, value)
|
with self.suppress_update(bl_instance):
|
||||||
|
self.bl_prop.write(bl_instance, value)
|
||||||
|
|
||||||
# Update Semantics
|
# Update Semantics
|
||||||
if self.suppress_update.get(bl_instance.instance_id):
|
if self.prop_info['use_prop_update'] and not self.suppressed_update.get(
|
||||||
self.suppress_update[bl_instance.instance_id] = False
|
bl_instance.instance_id, False
|
||||||
|
):
|
||||||
elif self.prop_info['use_prop_update']:
|
|
||||||
bl_instance.on_prop_changed(self.bl_prop.name)
|
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."""
|
"""Implements various key caches on instances of Blender objects, especially nodes and sockets."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import inspect
|
import inspect
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ class CachedBLProperty:
|
||||||
self.decode_type: type = inspect.signature(getter_method).return_annotation
|
self.decode_type: type = inspect.signature(getter_method).return_annotation
|
||||||
|
|
||||||
# Write Suppressing
|
# Write Suppressing
|
||||||
self.suppress_write: dict[str, bool] = {}
|
self.suppressed_update: dict[str, bool] = {}
|
||||||
|
|
||||||
# Check Non-Empty Type Annotation
|
# Check Non-Empty Type Annotation
|
||||||
## For now, just presume that all types can be encoded/decoded.
|
## For now, just presume that all types can be encoded/decoded.
|
||||||
|
@ -125,9 +126,38 @@ class CachedBLProperty:
|
||||||
return Signal.CacheNotReady
|
return Signal.CacheNotReady
|
||||||
return cached_value
|
return cached_value
|
||||||
|
|
||||||
def suppress_next_write(self, bl_instance) -> None:
|
@contextlib.contextmanager
|
||||||
self.suppress_write[bl_instance.instance_id] = True
|
def suppress_update(self, bl_instance: bl_instance.BLInstance) -> None:
|
||||||
## TODO: Make it a context manager to prevent the worst of surprises
|
"""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__(
|
def __set__(
|
||||||
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
|
self, bl_instance: bl_instance.BLInstance | None, value: typ.Any
|
||||||
|
@ -141,44 +171,59 @@ class CachedBLProperty:
|
||||||
Parameters:
|
Parameters:
|
||||||
bl_instance: The Blender object this prop
|
bl_instance: The Blender object this prop
|
||||||
"""
|
"""
|
||||||
if value is Signal.DoUpdate:
|
# Invalidate Cache
|
||||||
bl_instance.on_prop_changed(self.bl_prop.name)
|
## -> This empties the non-persistent cache.
|
||||||
|
## -> If persist=True, this also writes the persistent cache (no update).
|
||||||
elif value is Signal.InvalidateCache or value is Signal.InvalidateCacheNoUpdate:
|
## 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
|
# Invalidate Partner Non-Persistent Caches
|
||||||
## -> Only for the invalidation case do we also invalidate partners.
|
## -> Only for the invalidation case do we also invalidate partners.
|
||||||
if bl_instance is not None:
|
if bl_instance is not None:
|
||||||
# Fill Caches
|
# Fill Caches
|
||||||
## -> persist: Fill Persist and Non-Persist Cache
|
## -> persist=True: Fill Persist and Non-Persist Cache
|
||||||
## -> else: Fill Non-Persist Cache
|
## -> persist=False: Fill Non-Persist Cache
|
||||||
if self.persist and not self.suppress_write.get(
|
if self.persist:
|
||||||
bl_instance.instance_id
|
with self.suppress_update(bl_instance):
|
||||||
):
|
self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
|
||||||
self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.bl_prop.write_nonpersist(
|
self.bl_prop.write_nonpersist(
|
||||||
bl_instance, self.getter_method(bl_instance)
|
bl_instance, self.getter_method(bl_instance)
|
||||||
)
|
)
|
||||||
|
|
||||||
if value == Signal.InvalidateCache:
|
# Trigger Update
|
||||||
|
## -> Use InvalidateCacheNoUpdate to explicitly disable update.
|
||||||
|
## -> If 'suppress_update' context manager is active, don't update.
|
||||||
|
if value is Signal.InvalidateCache and not self.suppressed_update.get(
|
||||||
|
bl_instance.instance_id
|
||||||
|
):
|
||||||
bl_instance.on_prop_changed(self.bl_prop.name)
|
bl_instance.on_prop_changed(self.bl_prop.name)
|
||||||
|
|
||||||
|
# Call Setter
|
||||||
elif self.setter_method is not None:
|
elif self.setter_method is not None:
|
||||||
# Run Setter
|
if bl_instance is not None:
|
||||||
## -> The user-provided setter should do any updating of partners.
|
# Run Setter
|
||||||
if self.setter_method is not None:
|
## -> The user-provided setter can set values as it sees fit.
|
||||||
self.setter_method(bl_instance, value)
|
## -> 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
|
# Fill Caches
|
||||||
if self.persist and not self.suppress_write.get(bl_instance.instance_id):
|
## -> persist=True: Fill Persist and Non-Persist Cache
|
||||||
self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
|
## -> 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:
|
else:
|
||||||
self.bl_prop.write_nonpersist(
|
self.bl_prop.write_nonpersist(
|
||||||
bl_instance, self.getter_method(bl_instance)
|
bl_instance, self.getter_method(bl_instance)
|
||||||
)
|
)
|
||||||
bl_instance.on_prop_changed(self.bl_prop.name)
|
|
||||||
|
# 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:
|
else:
|
||||||
msg = f'Tried to set "{value}" to "{self.prop_name}" on "{bl_instance.bl_label}", but a setter was not defined'
|
msg = f'Tried to set "{value}" to "{self.prop_name}" on "{bl_instance.bl_label}", but a setter was not defined'
|
||||||
|
|
|
@ -39,8 +39,6 @@ class Signal(enum.StrEnum):
|
||||||
|
|
||||||
InvalidateCache: The cache should be invalidated.
|
InvalidateCache: The cache should be invalidated.
|
||||||
InvalidateCacheNoUpdate: The cache should be invalidated, but no update method should be run.
|
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.
|
ResetEnumItems: Cached dynamic enum items should be recomputed on next use.
|
||||||
ResetStrSearch: Cached string-search 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
|
# Invalidation
|
||||||
InvalidateCache: str = str(uuid.uuid4())
|
InvalidateCache: str = str(uuid.uuid4())
|
||||||
InvalidateCacheNoUpdate: str = str(uuid.uuid4())
|
InvalidateCacheNoUpdate: str = str(uuid.uuid4())
|
||||||
DoUpdate: str = str(uuid.uuid4())
|
|
||||||
|
|
||||||
# Reset Signals
|
# Reset Signals
|
||||||
## -> Invalidates data adjascent to fields.
|
## -> Invalidates data adjascent to fields.
|
||||||
|
|
|
@ -220,7 +220,13 @@ class BLInstance:
|
||||||
for str_search_prop_name in self.blfields_str_search:
|
for str_search_prop_name in self.blfields_str_search:
|
||||||
setattr(self, str_search_prop_name, bl_cache.Signal.ResetStrSearch)
|
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`.
|
"""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.
|
A property can recursively depend on other properties, including specificity as to whether the cache should be invalidated, the enum items be recomputed, or the string search items be recomputed.
|
||||||
|
@ -232,35 +238,110 @@ class BLInstance:
|
||||||
The dictionaries governing exactly what invalidates what, and how, are encoded as `self.blfield_deps`, `self.blfield_dynamic_enum_deps`, and `self.blfield_str_search_deps`.
|
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`).
|
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)
|
# Invalidate Dependent Properties (incl. DynEnums and StrSearch)
|
||||||
## -> NOTE: Dependent props may also trigger `on_prop_changed`.
|
## -> InvalidateCacheNoUpdate: Exactly what it sounds like.
|
||||||
## -> Don't abuse dependencies :)
|
## -> ResetEnumItems: Won't trigger on_prop_changed.
|
||||||
for deps, invalidate_signal in zip(
|
## -> -- 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_deps,
|
||||||
self.blfield_dynamic_enum_deps,
|
self.blfield_dynamic_enum_deps,
|
||||||
self.blfield_str_search_deps,
|
self.blfield_str_search_deps,
|
||||||
],
|
],
|
||||||
[
|
['invalidate', 'reset_enum', 'reset_strsearch'],
|
||||||
bl_cache.Signal.InvalidateCache,
|
|
||||||
bl_cache.Signal.ResetEnumItems,
|
|
||||||
bl_cache.Signal.ResetStrSearch,
|
|
||||||
],
|
|
||||||
strict=True,
|
strict=True,
|
||||||
):
|
):
|
||||||
if prop_name in deps:
|
if prop_name in deps:
|
||||||
for dst_prop_name in deps[prop_name]:
|
for dst_prop_name in deps[prop_name]:
|
||||||
log.debug(
|
# Mark Dependency for Clearance
|
||||||
'%s: "%s" is invalidating "%s"',
|
## -> Duplicates are OK for now, we'll clear them later.
|
||||||
self.bl_label,
|
blfields_to_clear.append((dst_prop_name, clear_method))
|
||||||
prop_name,
|
|
||||||
dst_prop_name,
|
# Compute Recursive Dependencies for Clearance
|
||||||
)
|
## -> As we go deeper, 'previous fields' is set.
|
||||||
setattr(
|
if dst_prop_name in self.blfields:
|
||||||
self,
|
blfields_to_clear += self.trace_blfields_to_clear(
|
||||||
dst_prop_name,
|
dst_prop_name,
|
||||||
invalidate_signal,
|
prev_blfields_to_clear=blfields_to_clear,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
match (bool(prev_blfields_to_clear), bool(blfields_to_clear)):
|
||||||
|
# Nothing to Clear
|
||||||
|
## -> This is a recursive base case for no-dependency BLFields.
|
||||||
|
case (False, False):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Only Old: Return Old
|
||||||
|
## -> This is a recursive base case for the deepest field w/o deps.
|
||||||
|
## -> When there are previous BLFields, this cannot be recursive root
|
||||||
|
## -> Otherwise, we'd need to de-duplicate.
|
||||||
|
case (True, False):
|
||||||
|
return prev_blfields_to_clear ## Is never recursive root
|
||||||
|
|
||||||
|
# Only New: Deduplicate (from right) w/Order Preservation
|
||||||
|
## -> This is the recursive root.
|
||||||
|
## -> The first time there are new BLFields to clear, we dedupe.
|
||||||
|
## -> This is the ONLY case where we need to dedupe.
|
||||||
|
## -> Deduping deeper would be extraneous (though not damaging).
|
||||||
|
case (False, True):
|
||||||
|
return list(reversed(dict.fromkeys(reversed(blfields_to_clear))))
|
||||||
|
|
||||||
|
# New And Old: Concatenate
|
||||||
|
## -> This is merely a "transport" step, sandwiched btwn base/root.
|
||||||
|
## -> As such, deduplication would not be wrong, just extraneous.
|
||||||
|
## -> Since invalidation is in a hot-loop, don't do such things.
|
||||||
|
case (True, True):
|
||||||
|
return blfields_to_clear
|
||||||
|
|
||||||
|
def clear_blfields_after(self, prop_name: str) -> list[str]:
|
||||||
|
"""Clear (invalidate) all `BLField`s that have become invalid as a result of a change to `prop_name`.
|
||||||
|
|
||||||
|
Uses `self.trace_blfields_to_clear()` to deduce the names and unique ordering of `BLField`s to clear.
|
||||||
|
Then, update-less `bl_cache.Signal`s are written in order to invalidate each `BLField` cache without invoking `self.on_prop_changed()`.
|
||||||
|
Finally, the list of cleared `BLField`s is returned.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Generally, this should be called from `on_prop_changed()`.
|
||||||
|
The resulting cleared fields can then be analyzed / used in a domain specific way as needed by the particular `BLInstance`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The topologically ordered right-de-duplicated list of BLFields that were cleared.
|
||||||
|
"""
|
||||||
|
blfields_to_clear = self.trace_blfields_to_clear(prop_name)
|
||||||
|
|
||||||
|
# Invalidate BLFields
|
||||||
|
## -> trace_blfields_to_clear only gave us what/how to invalidate.
|
||||||
|
## -> It's the responsibility of on_prop_changed to actually do so.
|
||||||
|
# log.debug(
|
||||||
|
# '%s (NodeSocket): Clearing BLFields after "%s": "%s"',
|
||||||
|
# self.bl_label,
|
||||||
|
# prop_name,
|
||||||
|
# blfields_to_clear,
|
||||||
|
# )
|
||||||
|
for blfield, clear_method in blfields_to_clear:
|
||||||
|
# log.debug(
|
||||||
|
# '%s (NodeSocket): Clearing BLField: %s (%s)',
|
||||||
|
# self.bl_label,
|
||||||
|
# blfield,
|
||||||
|
# clear_method,
|
||||||
|
# )
|
||||||
|
setattr(
|
||||||
|
self,
|
||||||
|
blfield,
|
||||||
|
{
|
||||||
|
'invalidate': bl_cache.Signal.InvalidateCacheNoUpdate,
|
||||||
|
'reset_enum': bl_cache.Signal.ResetEnumItems, ## No updates
|
||||||
|
'reset_strsearch': bl_cache.Signal.ResetStrSearch,
|
||||||
|
}[clear_method],
|
||||||
|
)
|
||||||
|
|
||||||
|
return [(prop_name, 'invalidate'), *blfields_to_clear]
|
||||||
|
|
||||||
def on_bl_prop_changed(self, bl_prop_name: str, _: bpy.types.Context) -> None:
|
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.
|
"""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 blender_maxwell import contracts as ct
|
||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
|
from .staticproperty import staticproperty
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@ class MathType(enum.StrEnum):
|
||||||
Complex = enum.auto()
|
Complex = enum.auto()
|
||||||
|
|
||||||
@staticmethod
|
@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:
|
if MathType.Complex in mathtypes:
|
||||||
return MathType.Complex
|
return MathType.Complex
|
||||||
if MathType.Real in mathtypes:
|
if MathType.Real in mathtypes:
|
||||||
|
@ -79,6 +80,9 @@ class MathType(enum.StrEnum):
|
||||||
if MathType.Integer in mathtypes:
|
if MathType.Integer in mathtypes:
|
||||||
return MathType.Integer
|
return MathType.Integer
|
||||||
|
|
||||||
|
if optional:
|
||||||
|
return None
|
||||||
|
|
||||||
msg = f"Can't combine mathtypes {mathtypes}"
|
msg = f"Can't combine mathtypes {mathtypes}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
@ -113,7 +117,7 @@ class MathType(enum.StrEnum):
|
||||||
return complex(pyobj, 0)
|
return complex(pyobj, 0)
|
||||||
|
|
||||||
@staticmethod
|
@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):
|
if isinstance(sp_obj, sp.MatrixBase):
|
||||||
return MathType.combine(
|
return MathType.combine(
|
||||||
*[MathType.from_expr(v) for v in sp.flatten(sp_obj)]
|
*[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]:
|
if sp_obj in [sp.zoo, -sp.zoo]:
|
||||||
return MathType.Complex
|
return MathType.Complex
|
||||||
|
|
||||||
|
if optional:
|
||||||
|
return None
|
||||||
|
|
||||||
msg = f"Can't determine MathType from sympy object: {sp_obj}"
|
msg = f"Can't determine MathType from sympy object: {sp_obj}"
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
@ -957,6 +964,48 @@ def unit_str_to_unit(unit_str: str) -> Unit | None:
|
||||||
####################
|
####################
|
||||||
# - "Physical" Type
|
# - "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):
|
class PhysicalType(enum.StrEnum):
|
||||||
"""Type identifiers for expressions with both `MathType` and a unit, aka a "physical" type."""
|
"""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()
|
Illuminance = enum.auto()
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def unit_dim(self):
|
def unit_dim(self) -> SympyType:
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
return {
|
return {
|
||||||
PT.NonPhysical: None,
|
PT.NonPhysical: None,
|
||||||
|
@ -1050,6 +1099,95 @@ class PhysicalType(enum.StrEnum):
|
||||||
PT.Illuminance: Dims.luminous_intensity / Dims.length**2,
|
PT.Illuminance: Dims.luminous_intensity / Dims.length**2,
|
||||||
}[self]
|
}[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
|
@functools.cached_property
|
||||||
def default_unit(self) -> list[Unit]:
|
def default_unit(self) -> list[Unit]:
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
|
@ -1256,17 +1394,59 @@ class PhysicalType(enum.StrEnum):
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_unit(unit: Unit, optional: bool = False) -> list[Unit] | None:
|
def from_unit(unit: Unit | None, optional: bool = False) -> typ.Self | None:
|
||||||
for physical_type in list(PhysicalType):
|
"""Attempt to determine a matching `PhysicalType` from a unit.
|
||||||
if unit in physical_type.valid_units:
|
|
||||||
return physical_type
|
NOTE: It is not guaranteed that `unit` is within `valid_units`, only that it can be converted to any unit in `valid_units`.
|
||||||
## TODO: Optimize
|
|
||||||
|
Returns:
|
||||||
|
The matched `PhysicalType`.
|
||||||
|
|
||||||
|
If none could be matched, then either return `None` (if `optional` is set) or error.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If no `PhysicalType` could be matched, and `optional` is `False`.
|
||||||
|
"""
|
||||||
|
if unit is None:
|
||||||
|
return ct.PhysicalType.NonPhysical
|
||||||
|
|
||||||
|
unit_dim_deps = unit_to_unit_dim_deps(unit)
|
||||||
|
if unit_dim_deps is not None:
|
||||||
|
for physical_type, candidate_unit_dim in PhysicalType.unit_dims.items():
|
||||||
|
if compare_unit_dim_to_unit_dim_deps(candidate_unit_dim, unit_dim_deps):
|
||||||
|
return physical_type
|
||||||
|
|
||||||
if optional:
|
if optional:
|
||||||
return None
|
return None
|
||||||
msg = f'Could not determine PhysicalType for {unit}'
|
msg = f'Could not determine PhysicalType for {unit}'
|
||||||
raise ValueError(msg)
|
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
|
@functools.cached_property
|
||||||
def valid_shapes(self) -> list[typ.Literal[(3,), (2,)] | None]:
|
def valid_shapes(self) -> list[typ.Literal[(3,), (2,)] | None]:
|
||||||
PT = PhysicalType
|
PT = PhysicalType
|
||||||
|
|
|
@ -17,7 +17,6 @@
|
||||||
"""Useful image processing operations for use in the addon."""
|
"""Useful image processing operations for use in the addon."""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
import functools
|
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import jax
|
import jax
|
||||||
|
@ -27,13 +26,13 @@ import matplotlib
|
||||||
import matplotlib.axis as mpl_ax
|
import matplotlib.axis as mpl_ax
|
||||||
import matplotlib.backends.backend_agg
|
import matplotlib.backends.backend_agg
|
||||||
import matplotlib.figure
|
import matplotlib.figure
|
||||||
import matplotlib.style as mplstyle
|
import numpy as np
|
||||||
import seaborn as sns
|
import seaborn as sns
|
||||||
|
|
||||||
from blender_maxwell import contracts as ct
|
from blender_maxwell import contracts as ct
|
||||||
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
from blender_maxwell.utils import logger
|
from blender_maxwell.utils import logger
|
||||||
|
|
||||||
# mplstyle.use('fast') ## TODO: Does this do anything?
|
|
||||||
sns.set_theme()
|
sns.set_theme()
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
@ -139,7 +138,7 @@ def rgba_image_from_2d_map(
|
||||||
####################
|
####################
|
||||||
# - MPL Helpers
|
# - 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):
|
def mpl_fig_canvas_ax(width_inches: float, height_inches: float, dpi: int):
|
||||||
fig = matplotlib.figure.Figure(
|
fig = matplotlib.figure.Figure(
|
||||||
figsize=[width_inches, height_inches], dpi=dpi, layout='tight'
|
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())
|
x_sym, y_sym = list(data.keys())
|
||||||
|
|
||||||
ax.boxplot([data[y_sym]])
|
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(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:
|
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_title(f'{x_sym.name_pretty} -> {heights_sym.name_pretty}')
|
||||||
ax.set_xlabel(x_sym.plot_label)
|
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:
|
def plot_curve_2d(data, ax: mpl_ax.Axis) -> None:
|
||||||
x_sym, y_sym = list(data.keys())
|
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.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(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:
|
def plot_points_2d(data, ax: mpl_ax.Axis) -> None:
|
||||||
x_sym, y_sym = list(data.keys())
|
x_sym, y_sym = list(data.keys())
|
||||||
|
|
||||||
ax.scatter(data[x_sym], data[y_sym])
|
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(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]):
|
for i, label in enumerate(data[label_sym]):
|
||||||
ax.plot(data[x_sym], data[y_sym][:, i], label=label)
|
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(x_sym.plot_label)
|
||||||
ax.set_xlabel(y_sym.plot_label)
|
ax.set_ylabel(y_sym.plot_label)
|
||||||
ax.legend()
|
ax.legend()
|
||||||
|
|
||||||
|
|
||||||
def plot_filled_curves_2d(
|
def plot_filled_curves_2d(data, ax: mpl_ax.Axis) -> None:
|
||||||
data: jtyp.Float32[jtyp.Array, 'x_size 2'], info, ax: mpl_ax.Axis
|
x_sym, _, y_sym = list(data.keys(data))
|
||||||
) -> None:
|
|
||||||
x_sym, _, y_sym = list(data.keys())
|
|
||||||
|
|
||||||
ax.fill_between(data[x_sym], data[y_sym][:, 0], data[x_sym], data[y_sym][:, 1])
|
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(x_sym.plot_label)
|
||||||
ax.set_xlabel(y_sym.plot_label)
|
ax.set_ylabel(y_sym.plot_label)
|
||||||
ax.legend()
|
ax.legend()
|
||||||
|
|
||||||
|
|
||||||
# (ℝ, ℝ) -> ℝ
|
# (ℝ, ℝ) -> ℝ
|
||||||
def plot_heatmap_2d(
|
def plot_heatmap_2d(data, ax: mpl_ax.Axis) -> None:
|
||||||
data: jtyp.Float32[jtyp.Array, 'x_size y_size'], info, ax: mpl_ax.Axis
|
|
||||||
) -> None:
|
|
||||||
x_sym, y_sym, c_sym = list(data.keys())
|
x_sym, y_sym, c_sym = list(data.keys())
|
||||||
|
|
||||||
heatmap = ax.imshow(data[c_sym], aspect='equal', interpolation='none')
|
heatmap = ax.imshow(data[c_sym], aspect='equal', interpolation='none')
|
||||||
ax.figure.colorbar(heatmap, cax=ax)
|
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(x_sym.plot_label)
|
||||||
ax.set_xlabel(y_sym.plot_label)
|
ax.set_xlabel(y_sym.plot_label)
|
||||||
ax.legend()
|
ax.legend()
|
||||||
|
|
|
@ -24,7 +24,6 @@ from fractions import Fraction
|
||||||
import jaxtyping as jtyp
|
import jaxtyping as jtyp
|
||||||
import pydantic as pyd
|
import pydantic as pyd
|
||||||
import sympy as sp
|
import sympy as sp
|
||||||
import sympy.physics.units as spu
|
|
||||||
|
|
||||||
from . import extra_sympy_units as spux
|
from . import extra_sympy_units as spux
|
||||||
from . import logger, serialize
|
from . import logger, serialize
|
||||||
|
@ -88,6 +87,7 @@ class SimSymbolName(enum.StrEnum):
|
||||||
Wavelength = enum.auto()
|
Wavelength = enum.auto()
|
||||||
Frequency = enum.auto()
|
Frequency = enum.auto()
|
||||||
|
|
||||||
|
Perm = enum.auto()
|
||||||
PermXX = enum.auto()
|
PermXX = enum.auto()
|
||||||
PermYY = enum.auto()
|
PermYY = enum.auto()
|
||||||
PermZZ = enum.auto()
|
PermZZ = enum.auto()
|
||||||
|
@ -161,6 +161,7 @@ class SimSymbolName(enum.StrEnum):
|
||||||
# Optics
|
# Optics
|
||||||
SSN.Wavelength: 'wl',
|
SSN.Wavelength: 'wl',
|
||||||
SSN.Frequency: 'freq',
|
SSN.Frequency: 'freq',
|
||||||
|
SSN.Perm: 'eps_r',
|
||||||
SSN.PermXX: 'eps_xx',
|
SSN.PermXX: 'eps_xx',
|
||||||
SSN.PermYY: 'eps_yy',
|
SSN.PermYY: 'eps_yy',
|
||||||
SSN.PermZZ: 'eps_zz',
|
SSN.PermZZ: 'eps_zz',
|
||||||
|
@ -179,6 +180,7 @@ class SimSymbolName(enum.StrEnum):
|
||||||
SSN.LowerTheta: 'θ',
|
SSN.LowerTheta: 'θ',
|
||||||
SSN.LowerPhi: 'φ',
|
SSN.LowerPhi: 'φ',
|
||||||
# Fields
|
# Fields
|
||||||
|
SSN.Er: 'Er',
|
||||||
SSN.Etheta: 'Eθ',
|
SSN.Etheta: 'Eθ',
|
||||||
SSN.Ephi: 'Eφ',
|
SSN.Ephi: 'Eφ',
|
||||||
SSN.Hr: 'Hr',
|
SSN.Hr: 'Hr',
|
||||||
|
@ -186,10 +188,11 @@ class SimSymbolName(enum.StrEnum):
|
||||||
SSN.Hphi: 'Hφ',
|
SSN.Hphi: 'Hφ',
|
||||||
# Optics
|
# Optics
|
||||||
SSN.Wavelength: 'λ',
|
SSN.Wavelength: 'λ',
|
||||||
SSN.Frequency: '𝑓',
|
SSN.Frequency: 'fᵣ',
|
||||||
SSN.PermXX: 'ε_xx',
|
SSN.Perm: 'εᵣ',
|
||||||
SSN.PermYY: 'ε_yy',
|
SSN.PermXX: 'εᵣ[xx]',
|
||||||
SSN.PermZZ: 'ε_zz',
|
SSN.PermYY: 'εᵣ[yy]',
|
||||||
|
SSN.PermZZ: 'εᵣ[zz]',
|
||||||
}.get(self, self.name)
|
}.get(self, self.name)
|
||||||
|
|
||||||
|
|
||||||
|
@ -248,6 +251,8 @@ class SimSymbol(pyd.BaseModel):
|
||||||
## -> See self.domain.
|
## -> See self.domain.
|
||||||
## -> We have to deconstruct symbolic interval semantics a bit for UI.
|
## -> We have to deconstruct symbolic interval semantics a bit for UI.
|
||||||
is_constant: bool = False
|
is_constant: bool = False
|
||||||
|
exclude_zero: bool = False
|
||||||
|
|
||||||
interval_finite_z: tuple[int, int] = (0, 1)
|
interval_finite_z: tuple[int, int] = (0, 1)
|
||||||
interval_finite_q: tuple[tuple[int, int], tuple[int, int]] = ((0, 1), (1, 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)
|
interval_finite_re: tuple[float, float] = (0.0, 1.0)
|
||||||
|
@ -284,20 +289,25 @@ class SimSymbol(pyd.BaseModel):
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def unit_label(self) -> str:
|
def unit_label(self) -> str:
|
||||||
"""Pretty unit label, which is an empty string when there is no unit."""
|
"""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
|
@functools.cached_property
|
||||||
def def_label(self) -> str:
|
def def_label(self) -> str:
|
||||||
"""Pretty definition label, exposing the symbol definition."""
|
"""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?
|
## TODO: Domain of validity from self.domain?
|
||||||
|
|
||||||
@functools.cached_property
|
@functools.cached_property
|
||||||
def plot_label(self) -> str:
|
def plot_label(self) -> str:
|
||||||
"""Pretty plot-oriented label."""
|
"""Pretty plot-oriented label."""
|
||||||
return f'{self.name_pretty}' + (
|
return f'{self.name_pretty} ({self.unit_label})'
|
||||||
f'({self.unit})' if self.unit is not None else ''
|
|
||||||
)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Computed Properties
|
# - 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."""
|
"""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)
|
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
|
@functools.cached_property
|
||||||
def size(self) -> tuple[int, ...] | None:
|
def size(self) -> tuple[int, ...] | None:
|
||||||
return {
|
return {
|
||||||
|
@ -403,6 +418,29 @@ class SimSymbol(pyd.BaseModel):
|
||||||
case (False, False):
|
case (False, False):
|
||||||
return sp.S(self.mathtype.coerce_compatible_pyobj(-1))
|
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
|
# - Properties
|
||||||
####################
|
####################
|
||||||
|
@ -434,23 +472,15 @@ class SimSymbol(pyd.BaseModel):
|
||||||
mathtype_kwargs |= {'complex': True}
|
mathtype_kwargs |= {'complex': True}
|
||||||
|
|
||||||
# Non-Zero Assumption
|
# Non-Zero Assumption
|
||||||
if (
|
if self.is_nonzero:
|
||||||
(
|
|
||||||
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
|
|
||||||
):
|
|
||||||
mathtype_kwargs |= {'nonzero': True}
|
mathtype_kwargs |= {'nonzero': True}
|
||||||
|
|
||||||
# Positive/Negative Assumption
|
# Positive/Negative Assumption
|
||||||
if self.domain.left >= 0:
|
if self.mathtype is not spux.MathType.Complex:
|
||||||
mathtype_kwargs |= {'positive': True}
|
if self.domain.left >= 0:
|
||||||
elif self.domain.right <= 0:
|
mathtype_kwargs |= {'positive': True}
|
||||||
mathtype_kwargs |= {'negative': True}
|
elif self.domain.right <= 0:
|
||||||
|
mathtype_kwargs |= {'negative': True}
|
||||||
|
|
||||||
# Scalar: Return Symbol
|
# Scalar: Return Symbol
|
||||||
if self.rows == 1 and self.cols == 1:
|
if self.rows == 1 and self.cols == 1:
|
||||||
|
@ -521,8 +551,8 @@ class SimSymbol(pyd.BaseModel):
|
||||||
self.valid_domain_value, strip_unit=True
|
self.valid_domain_value, strip_unit=True
|
||||||
),
|
),
|
||||||
# Defaults: FlowKind.Range
|
# Defaults: FlowKind.Range
|
||||||
'default_min': self.domain.start,
|
'default_min': self.conform(self.domain.start, strip_unit=True),
|
||||||
'default_max': self.domain.end,
|
'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})'
|
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)
|
raise NotImplementedError(msg)
|
||||||
|
@ -671,7 +701,9 @@ class SimSymbol(pyd.BaseModel):
|
||||||
sym_name: SimSymbolName,
|
sym_name: SimSymbolName,
|
||||||
expr: spux.SympyExpr,
|
expr: spux.SympyExpr,
|
||||||
unit_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).
|
"""Deduce a `SimSymbol` that matches the output of a given expression (and unit expression).
|
||||||
|
|
||||||
This is an essential method, allowing for the ded
|
This is an essential method, allowing for the ded
|
||||||
|
@ -697,12 +729,28 @@ class SimSymbol(pyd.BaseModel):
|
||||||
# MathType from Expr Assumptions
|
# MathType from Expr Assumptions
|
||||||
## -> All input symbols have assumptions, because we are very pedantic.
|
## -> All input symbols have assumptions, because we are very pedantic.
|
||||||
## -> Therefore, we should be able to reconstruct the MathType.
|
## -> 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"
|
# PhysicalType as "NonPhysical"
|
||||||
## -> 'unit' still applies - but we can't guarantee a PhysicalType will.
|
## -> 'unit' still applies - but we can't guarantee a PhysicalType will.
|
||||||
## -> Therefore, this is what we gotta do.
|
## -> Therefore, this is what we gotta do.
|
||||||
physical_type = spux.PhysicalType.NonPhysical
|
if spux.uses_units(unit_expr):
|
||||||
|
simplified_unit_expr = sp.simplify(unit_expr)
|
||||||
|
expr_physical_type = spux.PhysicalType.from_unit(
|
||||||
|
simplified_unit_expr, optional=True
|
||||||
|
)
|
||||||
|
|
||||||
|
physical_type = (
|
||||||
|
spux.PhysicalType.NonPhysical
|
||||||
|
if expr_physical_type is None
|
||||||
|
else expr_physical_type
|
||||||
|
)
|
||||||
|
unit = simplified_unit_expr
|
||||||
|
else:
|
||||||
|
physical_type = spux.PhysicalType.NonPhysical
|
||||||
|
unit = None
|
||||||
|
|
||||||
# Rows/Cols from Expr (if Matrix)
|
# Rows/Cols from Expr (if Matrix)
|
||||||
rows, cols = expr.shape if isinstance(expr, sp.MatrixBase) else (1, 1)
|
rows, cols = expr.shape if isinstance(expr, sp.MatrixBase) else (1, 1)
|
||||||
|
@ -711,9 +759,11 @@ class SimSymbol(pyd.BaseModel):
|
||||||
sym_name=sym_name,
|
sym_name=sym_name,
|
||||||
mathtype=mathtype,
|
mathtype=mathtype,
|
||||||
physical_type=physical_type,
|
physical_type=physical_type,
|
||||||
unit=unit_expr if unit_expr != 1 else None,
|
unit=unit,
|
||||||
rows=rows,
|
rows=rows,
|
||||||
cols=cols,
|
cols=cols,
|
||||||
|
is_constant=is_constant,
|
||||||
|
exclude_zero=expr.is_zero is not None and not expr.is_zero,
|
||||||
)
|
)
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
Loading…
Reference in New Issue