Compare commits

...

4 Commits

Author SHA1 Message Date
Sofus Albert Høgsbro Rose 339ee0226d
feat: Added the Bloch boundary condition.
A very healthy amount of research on how to choose the Bloch vector was
performed.
It is encapsulated not only in the documentation, but also in the modes
available for how to derive one that fits a given simulation.

The theory of the Bloch boundaries can really bite you, and the hope is
that by focusing on such invalid-usage-prevention, a lot of time can be
saved in the sim design stages.
2024-05-02 11:12:33 +02:00
Sofus Albert Høgsbro Rose 2f42c9d91b
feat: Added adiabatic absorber.
It's useful for scenarios where we need to intersect geometry with the
simulation boundary.
Generally, not preferred, which we make clear in the docs :)

Also fixed a bug in `events.py` that was misjudging `FlowInitializing`,
and causing `BoundConds` to fail.
Weird.
Anyway, fixed!
2024-05-01 16:38:11 +02:00
Sofus Albert Høgsbro Rose f60b736584
feat: Added BoundConds Node & Fancy PML Node
We now have a solid category (w/accompanying sockets) for defining
boundary conditions.
We also have a single-boundary-condition node for fully configuring the
all-important PML condition.

Some insights:
- PEC/PMC are so dead-simple that giving them their own nodes doesn't
  even make sense.
- StablePML and PML are the same, just with differing layers. Chose
  to require the user add layers to the PML node for the same effect.
- "Periodic" is a special case of "Bloch", so we only need "Bloch".
- "Absorber" vs "PML" is an important choice for the user, which we
  must ensure shines through.
2024-05-01 16:06:23 +02:00
Sofus Albert Høgsbro Rose 7263d585e5
fix: Inching closer.
I'm of the belief that the correct abstractions are now actually
available, and that most-to-all of the required functionality actually
already exists within the code base.
The art is bringing it together!
2024-05-01 13:54:16 +02:00
51 changed files with 1737 additions and 623 deletions

47
TODO.md
View File

@ -1,12 +1,5 @@
# Working TODO # Working TODO
- [ ] Wave Constant - [x] Wave Constant
- Bounds
- [ ] Boundary Conds
- [ ] PML
- [ ] PEC
- [ ] PMC
- [ ] Bloch
- [ ] Absorbing
- Sources - Sources
- [ ] Temporal Shapes / Continuous Wave Temporal Shape - [ ] Temporal Shapes / Continuous Wave Temporal Shape
- [ ] Temporal Shapes / Symbolic Temporal Shape - [ ] Temporal Shapes / Symbolic Temporal Shape
@ -18,8 +11,8 @@
- [ ] Data File Import - [ ] Data File Import
- [ ] DataFit Medium - [ ] DataFit Medium
- Monitors - Monitors
- [ ] EH Field - [x] EH Field
- [ ] Power Flux - [x] Power Flux
- [ ] Permittivity - [ ] Permittivity
- [ ] Diffraction - [ ] Diffraction
- Structures - Structures
@ -49,9 +42,9 @@
- Integration - Integration
- [ ] Simulation and Analysis of Maxim's Cavity - [ ] Simulation and Analysis of Maxim's Cavity
- Constants - Constants
- [ ] Number Constant - [x] Number Constant
- [ ] Vector Constant - [x] Vector Constant
- [ ] Physical Constant - [x] Physical Constant
- [ ] Fix many problems by persisting `_enum_cb_cache` and `_str_cb_cache`. - [ ] Fix many problems by persisting `_enum_cb_cache` and `_str_cb_cache`.
@ -70,7 +63,7 @@
- [ ] Pol SocketType: 3D Poincare sphere visualization of Stokes vectors. - [ ] Pol SocketType: 3D Poincare sphere visualization of Stokes vectors.
- [x] Math / Map Math - [x] Math / Map Math
- [ ] Remove "By x" socket set let socket sets only be "Function"/"Expr"; then add a dynamic enum underneath to select "By x" based on data support. - [x] Remove "By x" socket set let socket sets only be "Function"/"Expr"; then add a dynamic enum underneath to select "By x" based on data support.
- [ ] Filter the operations based on data support, ex. use positive-definiteness to guide cholesky. - [ ] Filter the operations based on data support, ex. use positive-definiteness to guide cholesky.
- [ ] Implement support for additional symbols via `Expr`. - [ ] Implement support for additional symbols via `Expr`.
- [x] Math / Filter Math - [x] Math / Filter Math
@ -81,8 +74,6 @@
## Inputs ## Inputs
- [x] Wave Constant - [x] Wave Constant
- [ ] Fix the LazyValueRange (again!)
- [ ] Document
- [x] Scene - [x] Scene
- [ ] Implement export of scene time via. Blender unit system. - [ ] Implement export of scene time via. Blender unit system.
- [ ] Implement optional scene-synced time exporting, so that the simulation definition and scene definition match for analysis needs. - [ ] Implement optional scene-synced time exporting, so that the simulation definition and scene definition match for analysis needs.
@ -90,14 +81,14 @@
- [x] Constants / Expr Constant - [x] Constants / Expr Constant
- See IDEAS. - See IDEAS.
- [x] Constants / Number Constant - [x] Constants / Number Constant
- [ ] Fix non-integer sockets - [x] Constants / Vector Constant
- [ ] Constants / Vector Constant - [x] Constants / Physical Constant
- [ ] Constants / Physical Constant
- [x] Constants / Scientific Constant - [x] Constants / Scientific Constant
- [ ] Nicer (boxed?) node information, maybe centered headers, in a box, etc. . - [ ] Nicer (boxed?) node information, maybe centered headers, in a box, etc. .
- [x] Constants / Unit System Constant - [ ] Constants / Unit System Constant
- [ ] Re-implement with `PhysicalType`.
- [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row. - [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row.
- [x] Constants / Blender Constant - [ ] Constants / Blender Constant
- [ ] Fix it! - [ ] Fix it!
- [ ] Web / Tidy3D Web Importer - [ ] Web / Tidy3D Web Importer
@ -200,13 +191,13 @@
## Bounds ## Bounds
- [x] Boundary Conds - [x] Boundary Conds
- [ ] Boundary Cond / PML Bound Face - [x] Boundary Cond / PML Bound Cond
- [ ] Dropdown for "Normal" and "Stable" - [ ] 1D plot visualizing the effect of parameters on a 1D wave function
- [ ] Boundary Cond / PEC Bound Face - [x] Boundary Cond / Bloch Bound Cond
- [ ] Boundary Cond / PMC Bound Face - [x] Implement "simple" mode aka "periodic" mode in Tidy3D
- [ ] Boundary Cond / Bloch Bound Face - [ ] 1D plot visualizing the effect of parameters on a 1D wave function
- [ ] Implement "simple" mode aka "periodic" mode in Tidy3D - [x] Boundary Cond / Absorbing Bound Cond
- [ ] Boundary Cond / Absorbing Bound Face - [ ] 1D plot visualizing the effect of parameters on a 1D wave function
## Monitors ## Monitors
- [x] EH Field Monitor - [x] EH Field Monitor

View File

@ -168,13 +168,13 @@ class GeoNodes(enum.StrEnum):
GN.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH, GN.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH,
GN.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH, GN.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH,
## Monitor ## Monitor
GN.MonitorEHField: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorPowerFlux: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorPowerFlux: GN_INTERNAL_MONITORS_PATH,
GN.MonitorEpsTensor: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorEpsTensor: GN_INTERNAL_MONITORS_PATH,
GN.MonitorDiffraction: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorDiffraction: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjCartEHField: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorProjCartEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjAngEHField: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorProjAngEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjKSpaceEHField: GN_INTERNAL_STRUCTURES_PATH, GN.MonitorProjKSpaceEHField: GN_INTERNAL_MONITORS_PATH,
## Simulation ## Simulation
GN.SimulationSimDomain: GN_INTERNAL_SIMULATIONS_PATH, GN.SimulationSimDomain: GN_INTERNAL_SIMULATIONS_PATH,
GN.SimulationBoundConds: GN_INTERNAL_SIMULATIONS_PATH, GN.SimulationBoundConds: GN_INTERNAL_SIMULATIONS_PATH,
@ -225,7 +225,7 @@ def import_geonodes(
if import_method == 'link' and geonodes in bpy.data.node_groups: if import_method == 'link' and geonodes in bpy.data.node_groups:
return bpy.data.node_groups[geonodes] return bpy.data.node_groups[geonodes]
filename = geonodes filename = str(geonodes)
filepath = str(geonodes.parent_path / (geonodes + '.blend') / 'NodeTree' / geonodes) filepath = str(geonodes.parent_path / (geonodes + '.blend') / 'NodeTree' / geonodes)
directory = filepath.removesuffix(geonodes) directory = filepath.removesuffix(geonodes)
log.info( log.info(

View File

@ -1,25 +1,25 @@
from blender_maxwell.contracts import ( from blender_maxwell.contracts import (
BLClass, BLClass,
BLColorRGBA, BLColorRGBA,
BLEnumElement, BLEnumElement,
BLEnumID, BLEnumID,
BLIcon, BLIcon,
BLIconSet, BLIconSet,
BLIDStruct, BLIDStruct,
BLKeymapItem, BLKeymapItem,
BLModifierType, BLModifierType,
BLNodeTreeInterfaceID, BLNodeTreeInterfaceID,
BLOperatorStatus, BLOperatorStatus,
BLPropFlag, BLPropFlag,
BLRegionType, BLRegionType,
BLSpaceType, BLSpaceType,
KeymapItemDef, KeymapItemDef,
ManagedObjName, ManagedObjName,
OperatorType, OperatorType,
PanelType, PanelType,
PresetName, PresetName,
SocketName, SocketName,
addon, addon,
) )
from .bl_socket_types import BLSocketInfo, BLSocketType from .bl_socket_types import BLSocketInfo, BLSocketType
@ -27,19 +27,20 @@ from .category_labels import NODE_CAT_LABELS
from .category_types import NodeCategory from .category_types import NodeCategory
from .flow_events import FlowEvent from .flow_events import FlowEvent
from .flow_kinds import ( from .flow_kinds import (
ArrayFlow, ArrayFlow,
CapabilitiesFlow, CapabilitiesFlow,
FlowKind, FlowKind,
InfoFlow, InfoFlow,
LazyArrayRangeFlow, LazyArrayRangeFlow,
LazyValueFuncFlow, LazyValueFuncFlow,
ParamsFlow, ParamsFlow,
ValueFlow, ValueFlow,
) )
from .flow_signals import FlowSignal from .flow_signals import FlowSignal
from .icons import Icon from .icons import Icon
from .mobj_types import ManagedObjType from .mobj_types import ManagedObjType
from .node_types import NodeType from .node_types import NodeType
from .sim_types import BoundCondType, SimSpaceAxis
from .socket_colors import SOCKET_COLORS from .socket_colors import SOCKET_COLORS
from .socket_types import SocketType from .socket_types import SocketType
from .tree_types import TreeType from .tree_types import TreeType
@ -77,6 +78,8 @@ __all__ = [
'BLSocketInfo', 'BLSocketInfo',
'BLSocketType', 'BLSocketType',
'NodeType', 'NodeType',
'BoundCondType',
'SimSpaceAxis',
'NodeCategory', 'NodeCategory',
'NODE_CAT_LABELS', 'NODE_CAT_LABELS',
'ManagedObjType', 'ManagedObjType',

View File

@ -25,58 +25,60 @@ class BLSocketInfo:
bl_isocket_identifier: spux.ScalarUnitlessRealExpr bl_isocket_identifier: spux.ScalarUnitlessRealExpr
@blender_type_enum.prefix_values_with('NodeSocket')
class BLSocketType(enum.StrEnum): class BLSocketType(enum.StrEnum):
Virtual = 'Virtual' Virtual = 'NodeSocketVirtual'
# Blender # Blender
Image = 'Image' Image = 'NodeSocketImage'
Shader = 'Shader' Shader = 'NodeSocketShader'
Material = 'Material' Material = 'NodeSocketMaterial'
Geometry = 'Material' Geometry = 'NodeSocketGeometry'
Object = 'Object' Object = 'NodeSocketObject'
Collection = 'Collection' Collection = 'NodeSocketCollection'
# Basic # Basic
Bool = 'Bool' Bool = 'NodeSocketBool'
String = 'String' String = 'NodeSocketString'
Menu = 'Menu' Menu = 'NodeSocketMenu'
# Float # Float
Float = 'Float' Float = 'NodeSocketFloat'
FloatUnsigned = 'FloatUnsigned' FloatUnsigned = 'NodeSocketFloatUnsigned'
FloatAngle = 'FloatAngle' FloatAngle = 'NodeSocketFloatAngle'
FloatDistance = 'FloatDistance' FloatDistance = 'NodeSocketFloatDistance'
FloatFactor = 'FloatFactor' FloatFactor = 'NodeSocketFloatFactor'
FloatPercentage = 'FloatPercentage' FloatPercentage = 'NodeSocketFloatPercentage'
FloatTime = 'FloatTime' FloatTime = 'NodeSocketFloatTime'
FloatTimeAbsolute = 'FloatTimeAbsolute' FloatTimeAbsolute = 'NodeSocketFloatTimeAbsolute'
# Int # Int
Int = 'Int' Int = 'NodeSocketInt'
IntFactor = 'IntFactor' IntFactor = 'NodeSocketIntFactor'
IntPercentage = 'IntPercentage' IntPercentage = 'NodeSocketIntPercentage'
IntUnsigned = 'IntUnsigned' IntUnsigned = 'NodeSocketIntUnsigned'
# Vector # Vector
Color = 'Color' Color = 'NodeSocketColor'
Rotation = 'Rotation' Rotation = 'NodeSocketRotation'
Vector = 'Vector' Vector = 'NodeSocketVector'
VectorAcceleration = 'Acceleration' VectorAcceleration = 'NodeSocketAcceleration'
VectorDirection = 'Direction' VectorDirection = 'NodeSocketDirection'
VectorEuler = 'Euler' VectorEuler = 'NodeSocketEuler'
VectorTranslation = 'Translation' VectorTranslation = 'NodeSocketTranslation'
VectorVelocity = 'Velocity' VectorVelocity = 'NodeSocketVelocity'
VectorXYZ = 'XYZ' VectorXYZ = 'NodeSocketXYZ'
@staticmethod @staticmethod
def from_bl_isocket( def from_bl_isocket(
bl_isocket: bpy.types.NodeTreeInterfaceSocket, bl_isocket: bpy.types.NodeTreeInterfaceSocket,
) -> typ.Self: ) -> typ.Self:
return BLSocketType[bl_isocket.bl_socket_idname] return BLSocketType(bl_isocket.bl_socket_idname)
@staticmethod @staticmethod
def info_from_bl_isocket( def info_from_bl_isocket(
bl_isocket: bpy.types.NodeTreeInterfaceSocket, bl_isocket: bpy.types.NodeTreeInterfaceSocket,
) -> typ.Self: ) -> typ.Self:
return BLSocketType.from_bl_isocket(bl_isocket).parse( bl_socket_type = BLSocketType.from_bl_isocket(bl_isocket)
bl_isocket.default_value, bl_isocket.description, bl_isocket.identifier if bl_socket_type.has_support:
) return bl_socket_type.parse(
bl_isocket.default_value, bl_isocket.description, bl_isocket.identifier
)
return bl_socket_type.parse(None, bl_isocket.description, bl_isocket.identifier)
#################### ####################
# - Direct Properties # - Direct Properties
@ -288,7 +290,7 @@ class BLSocketType(enum.StrEnum):
) )
# Parse the Default Value # Parse the Default Value
if self.mathtype is not None: if self.mathtype is not None and bl_default_value is not None:
if self.size == spux.NumberSize1D.Scalar: if self.size == spux.NumberSize1D.Scalar:
default_value = self.mathtype.pytype(bl_default_value) default_value = self.mathtype.pytype(bl_default_value)
elif description.startswith('2D'): elif description.startswith('2D'):

View File

@ -25,7 +25,7 @@ NODE_CAT_LABELS = {
NC.MAXWELLSIM_STRUCTURES_PRIMITIVES: 'Primitives', NC.MAXWELLSIM_STRUCTURES_PRIMITIVES: 'Primitives',
# Bounds/ # Bounds/
NC.MAXWELLSIM_BOUNDS: 'Bounds', NC.MAXWELLSIM_BOUNDS: 'Bounds',
NC.MAXWELLSIM_BOUNDS_BOUNDCONDS: 'Bound Conds', NC.MAXWELLSIM_BOUNDS_BOUNDCONDS: 'Conds',
# Monitors/ # Monitors/
NC.MAXWELLSIM_MONITORS: 'Monitors', NC.MAXWELLSIM_MONITORS: 'Monitors',
NC.MAXWELLSIM_MONITORS_PROJECTED: 'Projected', NC.MAXWELLSIM_MONITORS_PROJECTED: 'Projected',

View File

@ -13,9 +13,12 @@ import sympy as sp
import sympy.physics.units as spu 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 .socket_types import SocketType from .socket_types import SocketType
log = logger.get(__name__)
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.
@ -57,19 +60,22 @@ class FlowKind(enum.StrEnum):
Info = enum.auto() Info = enum.auto()
@classmethod @classmethod
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system): def scale_to_unit_system(
cls,
kind: typ.Self,
value,
unit_system: spux.UnitSystem,
):
if kind == cls.Value: if kind == cls.Value:
return spux.sympy_to_python( return spux.scale_to_unit_system(
spux.scale_to_unit( value,
value, unit_system,
unit_system[socket_type],
)
) )
if kind == cls.LazyArrayRange: if kind == cls.LazyArrayRange:
return value.rescale_to_unit(unit_system[socket_type]) return value.rescale_to_unit_system(unit_system)
if kind == cls.Params: if kind == cls.Params:
return value.rescale_to_unit(unit_system[socket_type]) return value.rescale_to_unit_system(unit_system)
msg = 'Tried to scale unknown kind' msg = 'Tried to scale unknown kind'
raise ValueError(msg) raise ValueError(msg)
@ -84,17 +90,28 @@ class CapabilitiesFlow:
active_kind: FlowKind active_kind: FlowKind
is_universal: bool = False is_universal: bool = False
# == Constraint
must_match: dict[str, typ.Any] = dataclasses.field(default_factory=dict) must_match: dict[str, typ.Any] = dataclasses.field(default_factory=dict)
# ∀b (b ∈ A) Constraint
## A: allow_any
## b∈B: present_any
allow_any: set[typ.Any] = dataclasses.field(default_factory=set)
present_any: set[typ.Any] = dataclasses.field(default_factory=set)
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 == other.socket_type
and self.active_kind == other.active_kind and self.active_kind == other.active_kind
# == Constraint
and all( and all(
name in other.must_match name in other.must_match
and self.must_match[name] == other.must_match[name] and self.must_match[name] == other.must_match[name]
for name in self.must_match for name in self.must_match
) )
# ∀b (b ∈ A) Constraint
and self.present_any.issubset(other.allow_any)
) )
@ -187,6 +204,9 @@ class ArrayFlow:
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg) raise ValueError(msg)
def rescale_to_unit_system(self, unit: spu.Quantity) -> typ.Self:
raise NotImplementedError
#################### ####################
# - Lazy Value Func # - Lazy Value Func
@ -469,14 +489,13 @@ class LazyArrayRangeFlow:
# Get Stop Mathtype # Get Stop Mathtype
if isinstance(self.stop, spux.SympyType): if isinstance(self.stop, spux.SympyType):
stop_mathtype = spux.MathType.from_expr(type(self.stop)) stop_mathtype = spux.MathType.from_expr(self.stop)
else: else:
stop_mathtype = spux.MathType.from_pytype(type(self.stop)) stop_mathtype = spux.MathType.from_pytype(self.stop)
# Check Equal # Check Equal
if start_mathtype != stop_mathtype: if start_mathtype != stop_mathtype:
msg = "Mathtypes of start and stop don't agree. Please fix!" return spux.MathType.combine(start_mathtype, stop_mathtype)
raise ValueError(msg)
return start_mathtype return start_mathtype
@ -525,8 +544,8 @@ class LazyArrayRangeFlow:
""" """
if self.unit is not None: if self.unit is not None:
return LazyArrayRangeFlow( return LazyArrayRangeFlow(
start=spu.scale_to_unit(self.start * self.unit, unit), start=spux.scale_to_unit(self.start * self.unit, unit),
stop=spu.scale_to_unit(self.stop * self.unit, unit), stop=spux.scale_to_unit(self.stop * self.unit, unit),
steps=self.steps, steps=self.steps,
scaling=self.scaling, scaling=self.scaling,
unit=unit, unit=unit,
@ -536,6 +555,39 @@ class LazyArrayRangeFlow:
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg) raise ValueError(msg)
def rescale_to_unit_system(self, unit_system: spux.Unit) -> typ.Self:
"""Replaces the units, **with** rescaling of the bounds.
Parameters:
unit: The unit to convert the bounds to.
Returns:
A new `LazyArrayRangeFlow` with replaced unit.
Raises:
ValueError: If the existing unit is `None`, indicating that there is no unit to correct.
"""
if self.unit is not None:
return LazyArrayRangeFlow(
start=spux.strip_unit_system(
spux.convert_to_unit_system(self.start * self.unit, unit_system),
unit_system,
),
stop=spux.strip_unit_system(
spux.convert_to_unit_system(self.start * self.unit, unit_system),
unit_system,
),
steps=self.steps,
scaling=self.scaling,
unit=unit_system[spux.PhysicalType.from_unit(self.unit)],
symbols=self.symbols,
)
msg = (
f'Tried to rescale unitless LazyDataValueRange to unit system {unit_system}'
)
raise ValueError(msg)
#################### ####################
# - Bound Operations # - Bound Operations
#################### ####################

View File

@ -16,6 +16,7 @@ class FlowSignal(enum.StrEnum):
""" """
FlowInitializing = enum.auto()
FlowPending = enum.auto() FlowPending = enum.auto()
NoFlow = enum.auto() NoFlow = enum.auto()

View File

@ -90,10 +90,8 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
BoundConds = enum.auto() BoundConds = enum.auto()
## Bounds / Bound Conds ## Bounds / Bound Conds
PMLBoundCond = enum.auto() PMLBoundCond = enum.auto()
PECBoundCond = enum.auto()
PMCBoundCond = enum.auto()
BlochBoundCond = enum.auto() BlochBoundCond = enum.auto()
AbsorbingBoundCond = enum.auto() AdiabAbsorbBoundCond = enum.auto()
# Monitors # Monitors
EHFieldMonitor = enum.auto() EHFieldMonitor = enum.auto()

View File

@ -0,0 +1,119 @@
"""Declares various simulation types for use by nodes and sockets."""
import enum
import typing as typ
import tidy3d as td
## TODO: Sim Domain type, w/pydantic checks!
class SimSpaceAxis(enum.StrEnum):
"""The axis labels of the global simulation coordinate system."""
X = enum.auto()
Y = enum.auto()
Z = enum.auto()
@staticmethod
def to_name(v: typ.Self) -> str:
"""Convert the enum value to a human-friendly name.
Notes:
Used to print names in `EnumProperty`s based on this enum.
Returns:
A human-friendly name corresponding to the enum value.
"""
SSA = SimSpaceAxis
return {
SSA.X: 'x',
SSA.Y: 'y',
SSA.Z: 'z',
}[v]
@staticmethod
def to_icon(_: typ.Self) -> str:
"""Convert the enum value to a Blender icon.
Notes:
Used to print icons in `EnumProperty`s based on this enum.
Returns:
A human-friendly name corresponding to the enum value.
"""
return ''
@property
def axis(self) -> int:
"""Deduce the integer index of the axis.
Returns:
The integer index of the axis.
"""
SSA = SimSpaceAxis
return {SSA.X: 0, SSA.Y: 1, SSA.Z: 2}[self]
class BoundCondType(enum.StrEnum):
r"""A type of boundary condition, applied to a half-axis of a simulation domain.
Attributes:
Pml: "Perfectly Matched Layer" models infinite free space.
**Should be placed sufficiently far** (ex. $\frac{\lambda}{2}) from any active structures to mitigate divergence.
Periodic: Denotes Bloch-basedrepetition
Pec: "Perfect Electrical Conductor" models a surface that perfectly reflects electric fields.
Pmc: "Perfect Magnetic Conductor" models a surface that perfectly reflects the magnetic fields.
"""
Pml = enum.auto()
Periodic = enum.auto()
Pec = enum.auto()
Pmc = enum.auto()
@staticmethod
def to_name(v: typ.Self) -> str:
"""Convert the enum value to a human-friendly name.
Notes:
Used to print names in `EnumProperty`s based on this enum.
Returns:
A human-friendly name corresponding to the enum value.
"""
BCT = BoundCondType
return {
BCT.Pml: 'PML',
BCT.Pec: 'PEC',
BCT.Pmc: 'PMC',
BCT.Periodic: 'Periodic',
}[v]
@staticmethod
def to_icon(_: typ.Self) -> str:
"""Convert the enum value to a Blender icon.
Notes:
Used to print icons in `EnumProperty`s based on this enum.
Returns:
A human-friendly name corresponding to the enum value.
"""
return ''
@property
def tidy3d_boundary_edge(self) -> td.BoundaryEdge:
"""Convert the boundary condition specifier to a corresponding, sensible `tidy3d` boundary edge.
`td.BoundaryEdge` can be used to declare a half-axis in a `td.BoundarySpec`, which attaches directly to a simulation object.
Returns:
A sensible choice of `tidy3d` object representing the boundary condition.
"""
BCT = BoundCondType
return {
BCT.Pml: td.PML(),
BCT.Pec: td.PECBoundary(),
BCT.Pmc: td.PMCBoundary(),
BCT.Periodic: td.Periodic(),
}[self]

View File

@ -110,26 +110,23 @@ class ManagedBLMesh(base.ManagedObj):
If it's already included, do nothing. If it's already included, do nothing.
""" """
if (bl_object := bpy.data.objects.get(self.name)) is not None: bl_object = bpy.data.objects.get(self.name)
if bl_object.name not in preview_collection().objects: if bl_object is None:
log.info('Moving "%s" to Preview Collection', bl_object.name) bl_object = self.bl_object()
preview_collection().objects.link(bl_object)
else: if bl_object.name not in preview_collection().objects:
msg = 'Managed BLMesh does not exist' log.info('Moving "%s" to Preview Collection', bl_object.name)
raise ValueError(msg) preview_collection().objects.link(bl_object)
def hide_preview(self) -> None: def hide_preview(self) -> None:
"""Removes the managed Blender object from the preview collection. """Removes the managed Blender object from the preview collection.
If it's already removed, do nothing. If it's already removed, do nothing.
""" """
if (bl_object := bpy.data.objects.get(self.name)) is not None: bl_object = bpy.data.objects.get(self.name)
if bl_object.name in preview_collection().objects: if bl_object is not None and bl_object.name in preview_collection().objects:
log.info('Removing "%s" from Preview Collection', bl_object.name) log.info('Removing "%s" from Preview Collection', bl_object.name)
preview_collection().objects.unlink(bl_object) preview_collection().objects.unlink(bl_object)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
def bl_select(self) -> None: def bl_select(self) -> None:
"""Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport.""" """Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport."""

View File

@ -206,6 +206,8 @@ class MaxwellSimTree(bpy.types.NodeTree):
"""Unlock all nodes in the node tree, making them editable.""" """Unlock all nodes in the node tree, making them editable."""
log.info('Unlocking All Nodes in NodeTree "%s"', self.bl_label) log.info('Unlocking All Nodes in NodeTree "%s"', self.bl_label)
for node in self.nodes: for node in self.nodes:
if node.type in ['REROUTE', 'FRAME']:
continue
node.locked = False node.locked = False
for bl_socket in [*node.inputs, *node.outputs]: for bl_socket in [*node.inputs, *node.outputs]:
bl_socket.locked = False bl_socket.locked = False
@ -229,7 +231,9 @@ class MaxwellSimTree(bpy.types.NodeTree):
@contextlib.contextmanager @contextlib.contextmanager
def repreview_all(self) -> None: def repreview_all(self) -> None:
all_nodes_with_preview_active = { all_nodes_with_preview_active = {
node.instance_id: node for node in self.nodes if node.preview_active node.instance_id: node
for node in self.nodes
if node.type not in ['REROUTE', 'FRAME'] and node.preview_active
} }
self.is_currently_repreviewing = True self.is_currently_repreviewing = True
self.newly_previewed_nodes = {} self.newly_previewed_nodes = {}

View File

@ -1,40 +1,37 @@
# from . import kitchen_sink
# from . import bounds
from . import ( from . import (
analysis, analysis,
bounds,
inputs, inputs,
mediums, # mediums,
monitors, monitors,
outputs, outputs,
simulations, # simulations,
sources, # sources,
structures, # structures,
utilities, # utilities,
) )
BL_REGISTER = [ BL_REGISTER = [
# *kitchen_sink.BL_REGISTER,
*analysis.BL_REGISTER, *analysis.BL_REGISTER,
*inputs.BL_REGISTER, *inputs.BL_REGISTER,
*outputs.BL_REGISTER, *outputs.BL_REGISTER,
*sources.BL_REGISTER, # *sources.BL_REGISTER,
*mediums.BL_REGISTER, # *mediums.BL_REGISTER,
*structures.BL_REGISTER, # *structures.BL_REGISTER,
# *bounds.BL_REGISTER, *bounds.BL_REGISTER,
*monitors.BL_REGISTER, *monitors.BL_REGISTER,
*simulations.BL_REGISTER, # *simulations.BL_REGISTER,
*utilities.BL_REGISTER, # *utilities.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
# **kitchen_sink.BL_NODES,
**analysis.BL_NODES, **analysis.BL_NODES,
**inputs.BL_NODES, **inputs.BL_NODES,
**outputs.BL_NODES, **outputs.BL_NODES,
**sources.BL_NODES, # **sources.BL_NODES,
**mediums.BL_NODES, # **mediums.BL_NODES,
**structures.BL_NODES, # **structures.BL_NODES,
# **bounds.BL_NODES, **bounds.BL_NODES,
**monitors.BL_NODES, **monitors.BL_NODES,
**simulations.BL_NODES, # **simulations.BL_NODES,
**utilities.BL_NODES, # **utilities.BL_NODES,
} }

View File

@ -401,8 +401,8 @@ class MapMathNode(base.MaxwellSimNode):
run_on_init=True, run_on_init=True,
) )
def on_input_changed(self): def on_input_changed(self):
if self.operation not in MapOperation.by_element_shape(self.expr_output_shape): # if self.operation not in MapOperation.by_element_shape(self.expr_output_shape):
self.operation = bl_cache.Signal.ResetEnumItems self.operation = bl_cache.Signal.ResetEnumItems
@events.on_value_changed( @events.on_value_changed(
# Trigger # Trigger

View File

@ -20,6 +20,10 @@ FUNCS = {
'MUL': lambda exprs: exprs[0] * exprs[1], 'MUL': lambda exprs: exprs[0] * exprs[1],
'DIV': lambda exprs: exprs[0] / exprs[1], 'DIV': lambda exprs: exprs[0] / exprs[1],
'POW': lambda exprs: exprs[0] ** exprs[1], 'POW': lambda exprs: exprs[0] ** exprs[1],
'ATAN2': lambda exprs: sp.atan2(exprs[1], exprs[0]),
# Vector | Vector
'VEC_VEC_DOT': lambda exprs: exprs[0].dot(exprs[1]),
'CROSS': lambda exprs: exprs[0].cross(exprs[1]),
} }
SP_FUNCS = FUNCS SP_FUNCS = FUNCS
@ -52,8 +56,8 @@ class OperateMathNode(base.MaxwellSimNode):
bl_label = 'Operate Math' bl_label = 'Operate Math'
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr L': sockets.ExprSocketDef(show_info_columns=False), 'Expr L': sockets.ExprSocketDef(),
'Expr R': sockets.ExprSocketDef(show_info_columns=False), 'Expr R': sockets.ExprSocketDef(),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(),
@ -73,10 +77,12 @@ class OperateMathNode(base.MaxwellSimNode):
def search_categories(self) -> list[ct.BLEnumElement]: def search_categories(self) -> list[ct.BLEnumElement]:
"""Deduce and return a list of valid categories for the current socket set and input data.""" """Deduce and return a list of valid categories for the current socket set and input data."""
expr_l_info = self._compute_input( expr_l_info = self._compute_input(
'Expr L', kind=ct.FlowKind.Info, optional=True 'Expr L',
kind=ct.FlowKind.Info,
) )
expr_r_info = self._compute_input( expr_r_info = self._compute_input(
'Expr R', kind=ct.FlowKind.Info, optional=True 'Expr R',
kind=ct.FlowKind.Info,
) )
has_expr_l_info = not ct.FlowSignal.check(expr_l_info) has_expr_l_info = not ct.FlowSignal.check(expr_l_info)
@ -121,6 +127,10 @@ class OperateMathNode(base.MaxwellSimNode):
if expr_l_info.output_shape is None and expr_r_info.output_shape is None: if expr_l_info.output_shape is None and expr_r_info.output_shape is None:
categories = [NUMBER_NUMBER] categories = [NUMBER_NUMBER]
## * | Number
elif expr_r_info.output_shape is None:
categories = []
## Number | Vector ## Number | Vector
elif ( elif (
expr_l_info.output_shape is None and len(expr_r_info.output_shape) == 1 expr_l_info.output_shape is None and len(expr_r_info.output_shape) == 1
@ -170,13 +180,12 @@ class OperateMathNode(base.MaxwellSimNode):
('POW', 'L^R', 'Power'), ('POW', 'L^R', 'Power'),
('ATAN2', 'atan2(L,R)', 'atan2(L,R)'), ('ATAN2', 'atan2(L,R)', 'atan2(L,R)'),
] ]
if self.category in 'Vector | Vector': if self.category == 'Vector | Vector':
if items: if items:
items += [None] items += [None]
items += [ items += [
('VEC_VEC_DOT', 'L · R', 'Vector-Vector Product'), ('VEC_VEC_DOT', 'L · R', 'Vector-Vector Product'),
('CROSS', 'L x R', 'Cross Product'), ('CROSS', 'L x R', 'Cross Product'),
('PROJ', 'proj(L, R)', 'Projection'),
] ]
if self.category == 'Matrix | Vector': if self.category == 'Matrix | Vector':
if items: if items:
@ -364,9 +373,7 @@ class OperateMathNode(base.MaxwellSimNode):
'Expr R': ct.FlowKind.Params, 'Expr R': ct.FlowKind.Params,
}, },
) )
def compute_params( def compute_params(self, props, input_sockets) -> ct.ParamsFlow | ct.FlowSignal:
self, props, input_sockets
) -> ct.ParamsFlow | ct.FlowSignal:
operation = props['operation'] operation = props['operation']
params_l = input_sockets['Expr L'] params_l = input_sockets['Expr L']
params_r = input_sockets['Expr R'] params_r = input_sockets['Expr R']

View File

@ -2,8 +2,11 @@ import enum
import typing as typ import typing as typ
import bpy import bpy
import jax
import jax.numpy as jnp
import jaxtyping as jtyp import jaxtyping as jtyp
import matplotlib.axis as mpl_ax import matplotlib.axis as mpl_ax
import sympy as sp
from blender_maxwell.utils import bl_cache, image_ops, logger from blender_maxwell.utils import bl_cache, image_ops, logger
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
@ -192,7 +195,10 @@ class VizNode(base.MaxwellSimNode):
# - Sockets # - Sockets
#################### ####################
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(
symbols={_x := sp.Symbol('x', real=True)},
default_value=2 * _x,
),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Preview': sockets.AnySocketDef(), 'Preview': sockets.AnySocketDef(),
@ -221,8 +227,12 @@ class VizNode(base.MaxwellSimNode):
## - Mode Searcher ## - Mode Searcher
##################### #####################
@property @property
def data_info(self) -> ct.InfoFlow: def data_info(self) -> ct.InfoFlow | None:
return self._compute_input('Expr', kind=ct.FlowKind.Info) info = self._compute_input('Expr', kind=ct.FlowKind.Info)
if not ct.FlowSignal.check(info):
return info
return None
def search_modes(self) -> list[ct.BLEnumElement]: def search_modes(self) -> list[ct.BLEnumElement]:
if not ct.FlowSignal.check(self.data_info): if not ct.FlowSignal.check(self.data_info):
@ -298,7 +308,9 @@ class VizNode(base.MaxwellSimNode):
managed_objs={'plot'}, managed_objs={'plot'},
props={'viz_mode', 'viz_target', 'colormap'}, props={'viz_mode', 'viz_target', 'colormap'},
input_sockets={'Expr'}, input_sockets={'Expr'},
input_socket_kinds={'Expr': {ct.FlowKind.Array, ct.FlowKind.Info}}, input_socket_kinds={
'Expr': {ct.FlowKind.Array, ct.FlowKind.LazyValueFunc, ct.FlowKind.Info}
},
stop_propagation=True, stop_propagation=True,
) )
def on_show_plot( def on_show_plot(

View File

@ -599,22 +599,28 @@ class MaxwellSimNode(bpy.types.Node):
It must be currently active. It must be currently active.
kind: The data flow kind to compute. kind: The data flow kind to compute.
""" """
if (bl_socket := self.inputs.get(input_socket_name)) is not None: bl_socket = self.inputs.get(input_socket_name)
return ( if bl_socket is not None:
ct.FlowKind.scale_to_unit_system( if bl_socket.instance_id:
kind, return (
bl_socket.compute_data(kind=kind), ct.FlowKind.scale_to_unit_system(
bl_socket.socket_type, kind,
unit_system, bl_socket.compute_data(kind=kind),
unit_system,
)
if unit_system is not None
else bl_socket.compute_data(kind=kind)
) )
if unit_system is not None
else bl_socket.compute_data(kind=kind) # No Socket Instance ID
) ## -> Indicates that socket_def.preinit() has not yet run.
## -> Anyone needing results will need to wait on preinit().
return ct.FlowSignal.FlowInitializing
if optional: if optional:
return ct.FlowSignal.NoFlow return ct.FlowSignal.NoFlow
msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket' 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) raise ValueError(msg)
#################### ####################

View File

@ -1,10 +1,10 @@
from . import bound_box, bound_faces from . import bound_cond_nodes, bound_conds
BL_REGISTER = [ BL_REGISTER = [
*bound_box.BL_REGISTER, *bound_conds.BL_REGISTER,
*bound_faces.BL_REGISTER, *bound_cond_nodes.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**bound_box.BL_NODES, **bound_conds.BL_NODES,
**bound_faces.BL_NODES, **bound_cond_nodes.BL_NODES,
} }

View File

@ -1,64 +0,0 @@
import tidy3d as td
from ... import contracts as ct
from ... import sockets
from .. import base, events
class BoundCondsNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoundConds
bl_label = 'Bound Box'
# bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
'+X': sockets.MaxwellBoundCondSocketDef(),
'-X': sockets.MaxwellBoundCondSocketDef(),
'+Y': sockets.MaxwellBoundCondSocketDef(),
'-Y': sockets.MaxwellBoundCondSocketDef(),
'+Z': sockets.MaxwellBoundCondSocketDef(),
'-Z': sockets.MaxwellBoundCondSocketDef(),
}
output_sockets = {
'BCs': sockets.MaxwellBoundCondsSocketDef(),
}
####################
# - Output Socket Computation
####################
@events.computes_output_socket(
'BCs', input_sockets={'+X', '-X', '+Y', '-Y', '+Z', '-Z'}
)
def compute_simulation(self, input_sockets) -> td.BoundarySpec:
x_pos = input_sockets['+X']
x_neg = input_sockets['-X']
y_pos = input_sockets['+Y']
y_neg = input_sockets['-Y']
z_pos = input_sockets['+Z']
z_neg = input_sockets['-Z']
return td.BoundarySpec(
x=td.Boundary(
plus=x_pos,
minus=x_neg,
),
y=td.Boundary(
plus=y_pos,
minus=y_neg,
),
z=td.Boundary(
plus=z_pos,
minus=z_neg,
),
)
####################
# - Blender Registration
####################
BL_REGISTER = [
BoundCondsNode,
]
BL_NODES = {ct.NodeType.BoundConds: (ct.NodeCategory.MAXWELLSIM_BOUNDS)}

View File

@ -0,0 +1,16 @@
from . import (
absorbing_bound_cond,
bloch_bound_cond,
pml_bound_cond,
)
BL_REGISTER = [
*pml_bound_cond.BL_REGISTER,
*bloch_bound_cond.BL_REGISTER,
*absorbing_bound_cond.BL_REGISTER,
]
BL_NODES = {
**pml_bound_cond.BL_NODES,
**bloch_bound_cond.BL_NODES,
**absorbing_bound_cond.BL_NODES,
}

View File

@ -0,0 +1,149 @@
"""Implements `AdiabAbsorbBoundCondNode`."""
import typing as typ
import bpy
import sympy as sp
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
class AdiabAbsorbBoundCondNode(base.MaxwellSimNode):
r"""A boundary condition that generically (adiabatically) absorbs outgoing energy, by gradually ramping up the strength of the conductor over many layers, until a final PEC layer.
Compared to PML, this boundary is more computationally expensive, and may result in higher reflectivity (and thus lower accuracy).
The general reason to use it is **to fix divergence in cases where dispersive materials intersect the simulation boundary**.
For more theoretical details, please refer to the `tidy3d` resource: <https://docs.flexcompute.com/projects/tidy3d/en/latest/api/_autosummary/tidy3d.Absorber.html>
Notes:
**Ensure** that all simulation structures are $\approx \frac{\lambda}{2}$ from any PML boundary.
This helps avoid the amplification of stray evanescent waves.
Socket Sets:
Simple: Only specify the number of absorption layers.
$40$ should generally be sufficient, but in the case of divergence issues, bumping up the number of layers should be the go-to remedy to try.
Full: Specify the conductivity min/max that makes up the absorption up the PML, as well as the order of the polynomial used to scale the effect through the layers.
You should probably leave this alone.
Since the value units are sim-relative, we've opted to show the scaling information in the node's UI, instead of coercing the values into any particular unit.
"""
node_type = ct.NodeType.AdiabAbsorbBoundCond
bl_label = 'Absorber Bound Cond'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'Layers': sockets.ExprSocketDef(
shape=None,
mathtype=spux.MathType.Integer,
abs_min=1,
default_value=40,
),
}
input_socket_sets: typ.ClassVar = {
'Simple': {},
'Full': {
'σ Order': sockets.ExprSocketDef(
shape=None,
mathtype=spux.MathType.Integer,
abs_min=1,
default_value=3,
),
'σ Range': sockets.ExprSocketDef(
shape=(2,),
mathtype=spux.MathType.Real,
default_value=sp.Matrix([0, 1.5]),
abs_min=0,
),
},
}
output_sockets: typ.ClassVar = {
'BC': sockets.MaxwellBoundCondSocketDef(),
}
####################
# - UI
####################
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Full':
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.4, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='σ:')
## RHS: Parameter Units
col = split.column()
col.label(text='2ε₀/Δt')
####################
# - Output
####################
@events.computes_output_socket(
'BC',
props={'active_socket_set'},
input_sockets={
'Layers',
'σ Order',
'σ Range',
},
input_sockets_optional={
'σ Order': True,
'σ Range': True,
},
)
def compute_adiab_absorber_bound_cond(self, props, input_sockets) -> td.Absorber:
r"""Computes the adiabatic absorber boundary condition based on the active socket set.
- **Simple**: Use `tidy3d`'s default parameters for defining the absorber parameters (apart from number of layers).
- **Full**: Use the user-defined $\sigma$ parameters, specifically polynomial order and sim-relative min/max conductivity values.
"""
log.debug(
'%s: Computing "%s" Adiabatic Absorber Boundary Condition (Input Sockets = %s)',
self.sim_node_name,
props['active_socket_set'],
input_sockets,
)
# Simple PML
if props['active_socket_set'] == 'Simple':
return td.Absorber(num_layers=input_sockets['Layers'])
# Full PML
return td.Absorber(
num_layers=input_sockets['Layers'],
parameters=td.AbsorberParams(
sigma_order=input_sockets['σ Order'],
sigma_min=input_sockets['σ Range'][0],
sigma_max=input_sockets['σ Range'][1],
),
)
####################
# - Blender Registration
####################
BL_REGISTER = [
AdiabAbsorbBoundCondNode,
]
BL_NODES = {
ct.NodeType.AdiabAbsorbBoundCond: (ct.NodeCategory.MAXWELLSIM_BOUNDS)
}

View File

@ -0,0 +1,236 @@
"""Implements `BlochBoundCondNode`."""
import typing as typ
import bpy
import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
class BlochBoundCondNode(base.MaxwellSimNode):
r"""A boundary condition that declares an "infinitely repeating" window, by applying Bloch's theorem to accurately describe how a boundary would behave if it were interacting with an infinitely repeating simulation structure.
# Theory
In the simplest case, aka. a normal-incident plane wave, the symmetries of electromagnetic wave propagation behave exactly as expected: Copy-paste the wavevector, but at opposite corners, as part of the FDTD neighbor-cell-update.
The moment this plane wave becomes angled, however, this "naive" method will cause **the phase of the periodically propagated fields to diverge from reality**.
With a bit of hand-waving, this is a natural thing: Fundamentally, the distance from each point on an angled plane wave to the boundary must vary, and if the phase is distance-dependent, then the phase must vary across the boundary.
Unfortunately, all of the explicitly-defined ways of describing how exactly to correct for this phenomenon depend on not only on what is being simulated, but on what is being studied.
The good news is, there are options.
## A Bloch of a Thing
A physicist named Felix Bloch came up with a theorem to help constrain how "wave-like things in periodic stuff" can be thought about, and it looks like
$$
\psi(\mathbf{r}) = u(\mathbf{r}) \cdot \exp_{\mathbb{C}}(\mathbf{k} \cdot \mathbf{r})
$$
for:
- $\psi$: A wave function (in general, satisfying the Schrödinger equation, but in this context, satisfying Maxwell's equations)
- $\mathbf{r}$: A position in 3D space.
- $\u$: Some periodic function mapping 3D space to a value. In this context, this might be a 3D function representing our simulation structures.
- $\mathbf{k}$: The "Bloch vector", of which there is guaranteed to be at least one, **but of which there may be many**.
At this point, it becomes interesting to note that pretty much _everything_ is, in fact, a "wave-like thing", so long as "the thing" is small enough.
Many such "periodically structured things", which form entire fields of study, can indeed be modelled using this single function:
- **Photonic Crystals**: The optical properties of many materials can be quite concisely encapsulated by placing regularly placed structures (of sub-wavelength size) within lattice-like structures.
- **Phononic Crystals**: A class of metamaterial that can be parameterized and optimized for its acoustic properties, purely by analyzing its periodic behavior, with applications ranging from interesting acoustic devices to seismic modelling.
## Modes of an Excited Nature
For a choice of $u$ (representing the simulation structure), there may be _many continuous_ choices of $\mathbf{k}$ that satisfy the Bloch theorem.
Similarly, for a particular choice of $\mathbf{k}$, there may be _several discrete_ particular solutions of the given wave function.
Thus, we come full circle: **Fully encapsulating** the wave-interactions of a periodic structure requires knowing its behavior at **all valid wave vectors**.
It is a sort of deeper truth, that any particular simulation of a unit cell cannot elicit the full story of _how a structure behaves_, since a particular choice of $\mathbf{k}$ must always inevitably be made as part of defining the simulation.
## Designing Periodically
With this insight in mind, we can now design simulations of periodic structures that properly account for the modalities imposed by particular $\mathbf{k}$ choices:
- **Only Rely on Real Fields**: If only the real parts of the fields are important, then the choice of $\mathbf{k}$ might not matter.
Remember, the symptom of needing to understand $\mathbf{k}$ is the phase-shift; if the phase-shift does not matter, then altering the Bloch vector won't change anything.
**Be careful**, though, and make sure to validate that the Bloch vector truly doesn't change anything.
- **Normal-Injected Plane Waves**: If fields generally only propagate in the normal direction, then again, choices of $\mathbf{k}$ might not matter.
Again, phase-shifting due to periodic behavior mainly happens when propagation occurs at grazing angles.
Again, **be careful**, and make sure to validate that ex. the Poynting vector truly isn't hitting the boundaries at too-grazing angles.
- **Angularly Injected Plane Waves**: If the injected plane wave is known, then we can directly compute a reasonable Bloch vector from the angle and boundary-axis-projected size of the plane wave source.
This selection of $\mathbf{k}$
- **Brute-Force Bloch-Vector Sweep**: If the nature of a periodic structure needs to be uncovered, and there's no special constraints to rely on, then it would be rightfully tempting to just sweep over all $\mathbf{k}$s, and run a complete simluation for each.
By going a step further, and plotting the energy of resonance frequencies noticed at each wave vector (just place point dipoles at random), one might stumble into a "band diagram" describing the possible energy states of electrons at each wave vector.
In general, these form a very sensible starting point for how to select Bloch vectors for productive use in the simulation.
NOTE: The Bloch vector is generally represented not as a vector, but as a single phase-shift per boundary axis unit length, mainly for convenience.
## Further Reading
- <https://optics.ansys.com/hc/en-us/articles/360041566614-Rectangular-Photonic-Crystal-Bandstructure>
- <https://docs.flexcompute.com/projects/tidy3d/en/v2.1.0/notebooks/Bandstructure.html>
- <https://en.wikipedia.org/wiki/Electronic_band_structure>
- <https://en.wikipedia.org/wiki/Brillouin_zone>
- <https://en.wikipedia.org/wiki/Bloch%27s_theorem>
Notes:
In the naive case, it is presumed that the choice of Bloch vector doesn't matter; therefore it is set to 0.
Socket Sets:
Naive: Specify a Bloch boundary condition where phase shift doesn't matter, and is thus set such that no phase-shift occurs.
This is the simplest (and cheapest) mechanism, which merely copy-pastes propagating waves at opposing sides of the simulation.
However, **this should not be used for angled plane waves**, as the phase-shift of a propagating angled plane wave **will be wrong**.
Source-Derived: Derive a Bloch vector that will be generally correct for a directed source, within a particular choice of axis on a particular simulation domain.
**Phase shift correctness is only guaranteed valid for the central frequency of the source**.
Thus, a narrow-band source is strongly recommended.
Bloch Vector: Specify a true Bloch boundary condition, including the **phase shift per unit length** (aka. the magnitude of the Bloch vector).
While the most flexible, **the appropriate choice for this value source of this value depends entirely on what is being simulated**.
"""
node_type = ct.NodeType.BlochBoundCond
bl_label = 'Bloch Bound Cond'
####################
# - Sockets
####################
input_socket_sets: typ.ClassVar = {
'Naive': {},
'Source-Derived': {
'Angled Source': sockets.MaxwellSourceSocketDef(),
## TODO: Constrain to gaussian beam, plane wafe, and tfsf
'Sim Domain': sockets.MaxwellSimDomainSocketDef(),
},
'Manual': {
'Bloch Vector': sockets.ExprSocketDef(),
},
}
output_sockets: typ.ClassVar = {
'BC': sockets.MaxwellBoundCondSocketDef(),
}
####################
# - Properties
####################
valid_sim_axis: ct.SimSpaceAxis = bl_cache.BLField(ct.SimSpaceAxis.X, prop_ui=True)
####################
# - UI
####################
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Source-Derived':
layout.prop(self, self.blfields['valid_sim_axis'], expand=True)
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Manual':
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Interpretation')
# Split
split = box.split(factor=0.6, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Bloch Vec:')
## RHS: Parameter Units
col = split.column()
col.label(text='2π/Δℓ')
####################
# - Events
####################
@events.on_value_changed(
prop_name={'active_socket_set', 'valid_sim_axis'},
run_on_init=True,
props={'active_socket_set', 'valid_sim_axis'},
)
def on_valid_sim_axis_changed(self, props):
"""For the source-derived socket set, synchronized the output socket's axis compatibility with the axis onto which the Bloch vector is computed.
The net result should be that invalid use of the Bloch boundary condition in a particular axis should be rejected.
- **Source-Derived**: Since the Bloch vector is computed between the source and the axis that this boundary is applied to, the output socket must be altered to **only** declare compatibility with that axis.
- **`*`**: Normalize the output socket axis validity to ensure that the boundary condition can be applied to any axis.
"""
if props['active_socket_set'] == 'Source-Derived':
self.outputs['BC'].present_axes = {props['valid_sim_axis']}
self.outputs['BC'].remove_invalidated_links()
else:
self.outputs['BC'].present_axes = {
ct.SimSpaceAxis.X,
ct.SimSpaceAxis.Y,
ct.SimSpaceAxis.Z,
}
####################
# - Output
####################
@events.computes_output_socket(
'BC',
props={'active_socket_set', 'valid_sim_axis'},
input_sockets={
'Angled Source',
'Sim Domain',
'Bloch Vector',
},
input_sockets_optional={
'Angled Source': True,
'Sim Domain': True,
'Bloch Vector': True,
},
)
def compute_bloch_bound_cond(
self, props, input_sockets
) -> td.Periodic | td.BlochBoundary:
r"""Computes the Bloch boundary condition.
- **Naive**: Set the Bloch vector to 0 by returning a `td.Periodic`.
- **Source-Derived**: Derive the Bloch vector from the source, simulation domain, and choice of axis.
The Bloch boundary axis **must** be orthogonal to the source's injection axis.
- **Manual**: Set the Bloch vector to the user-specified value.
"""
log.debug(
'%s: Computing Bloch Boundary Condition (Socket Set = %s)',
self.sim_node_name,
props['active_socket_set'],
)
# Naive
if props['active_socket_set'] == 'Naive':
return td.Periodic()
# Source-Derived
if props['active_socket_set'] == 'Naive':
sim_domain = input_sockets['Sim Domain']
valid_sim_axis = props['valid_sim_axis']
has_sim_domain = not ct.FlowSignal.check(sim_domain)
if has_sim_domain:
return td.BlochBoundary.from_source(
source=input_sockets['Angled Source'],
domain_size=sim_domain['size'][valid_sim_axis.axis],
axis=valid_sim_axis.axis,
medium=sim_domain['medium'],
)
return ct.FlowSignal.FlowPending
# Manual
return td.BlochBoundary(bloch_vec=input_sockets['Bloch Vector'])
####################
# - Blender Registration
####################
BL_REGISTER = [
BlochBoundCondNode,
]
BL_NODES = {ct.NodeType.BlochBoundCond: (ct.NodeCategory.MAXWELLSIM_BOUNDS)}

View File

@ -0,0 +1,190 @@
"""Implements `PMLBoundCondNode`."""
import typing as typ
import bpy
import sympy as sp
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
class PMLBoundCondNode(base.MaxwellSimNode):
r"""A "Perfectly Matched Layer" boundary condition, which is a theoretical medium that attempts to _perfectly_ absorb all outgoing waves, so as to represent "infinite space" in FDTD simulations.
PML boundary conditions do so by inducing a **frequency-dependent attenuation** on all waves that are outside of the boundary, over the course of several layers.
It is critical to note that a PML boundary can only absorb **propagating** waves.
_Evanescent_ waves oscillating w/o any power flux, ex. close to structures, may actually be **amplified** by a PML boundary.
This is the reasoning behind the $\frac{\lambda}{2}$-distance rule of thumb.
For more theoretical details, please refer to the `tidy3d` resource: <https://docs.flexcompute.com/projects/tidy3d/en/latest/api/_autosummary/tidy3d.PML.html>
Notes:
**Ensure** that all simulation structures are $\approx \frac{\lambda}{2}$ from any PML boundary.
This helps avoid the amplification of stray evanescent waves.
Socket Sets:
Simple: Only specify the number of PML layers.
$12$ should cover the most common cases; $40$ should be extremely stable.
Full: Specify the conductivity min/max that make up the PML, as well as the order of approximating polynomials.
The meaning of the parameters are rooted in the mathematics that underlie the PML function - if that doesn't mean anything to you, then you should probably leave it alone!
Since the value units are sim-relative, we've opted to show the scaling information in the node's UI, instead of coercing the values into any particular unit.
"""
node_type = ct.NodeType.PMLBoundCond
bl_label = 'PML Bound Cond'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'Layers': sockets.ExprSocketDef(
shape=None,
mathtype=spux.MathType.Integer,
abs_min=1,
default_value=12,
),
}
input_socket_sets: typ.ClassVar = {
'Simple': {},
'Full': {
'σ Order': sockets.ExprSocketDef(
shape=None,
mathtype=spux.MathType.Integer,
abs_min=1,
default_value=3,
),
'σ Range': sockets.ExprSocketDef(
shape=(2,),
mathtype=spux.MathType.Real,
default_value=sp.Matrix([0, 1.5]),
abs_min=0,
),
'κ Order': sockets.ExprSocketDef(
shape=None,
mathtype=spux.MathType.Integer,
abs_min=1,
default_value=3,
),
'κ Range': sockets.ExprSocketDef(
shape=(2,),
mathtype=spux.MathType.Real,
default_value=sp.Matrix([0, 1.5]),
abs_min=0,
),
'α Order': sockets.ExprSocketDef(
shape=None,
mathtype=spux.MathType.Integer,
abs_min=1,
default_value=3,
),
'α Range': sockets.ExprSocketDef(
shape=(2,),
mathtype=spux.MathType.Real,
default_value=sp.Matrix([0, 1.5]),
abs_min=0,
),
},
}
output_sockets: typ.ClassVar = {
'BC': sockets.MaxwellBoundCondSocketDef(),
}
####################
# - UI
####################
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set == 'Full':
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.4, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
for param in ['σ', 'κ', 'α']:
col.label(text=param + ':')
## RHS: Parameter Units
col = split.column()
for _ in range(3):
col.label(text='2ε₀/Δt')
####################
# - Output
####################
@events.computes_output_socket(
'BC',
props={'active_socket_set'},
input_sockets={
'Layers',
'σ Order',
'σ Range',
'κ Order',
'κ Range',
'α Order',
'α Range',
},
input_sockets_optional={
'σ Order': True,
'σ Range': True,
'κ Order': True,
'κ Range': True,
'α Order': True,
'α Range': True,
},
)
def compute_pml_boundary_cond(self, props, input_sockets) -> td.PML:
r"""Computes the PML boundary condition based on the active socket set.
- **Simple**: Use `tidy3d`'s default parameters for defining the PML conductor (apart from number of layers).
- **Full**: Use the user-defined $\sigma$, $\kappa$, and $\alpha$ parameters, specifically polynomial order and sim-relative min/max conductivity values.
"""
log.debug(
'%s: Computing "%s" PML Boundary Condition (Input Sockets = %s)',
self.sim_node_name,
props['active_socket_set'],
input_sockets,
)
# Simple PML
if props['active_socket_set'] == 'Simple':
return td.PML(num_layers=input_sockets['Layers'])
# Full PML
return td.PML(
num_layers=input_sockets['Layers'],
parameters=td.PMLParams(
sigma_order=input_sockets['σ Order'],
sigma_min=input_sockets['σ Range'][0],
sigma_max=input_sockets['σ Range'][1],
kappa_order=input_sockets['κ Order'],
kappa_min=input_sockets['κ Range'][0],
kappa_max=input_sockets['κ Range'][1],
alpha_order=input_sockets['α Order'],
alpha_min=input_sockets['α Range'][0],
alpha_max=input_sockets['α Range'][1],
),
)
####################
# - Blender Registration
####################
BL_REGISTER = [
PMLBoundCondNode,
]
BL_NODES = {ct.NodeType.PMLBoundCond: (ct.NodeCategory.MAXWELLSIM_BOUNDS)}

View File

@ -0,0 +1,158 @@
"""Implements `BoundCondsNode`."""
import typing as typ
import tidy3d as td
from blender_maxwell.utils import logger
from ... import contracts as ct
from ... import sockets
from .. import base, events
log = logger.get(__name__)
SSA = ct.SimSpaceAxis
class BoundCondsNode(base.MaxwellSimNode):
"""Provides a hub for joining custom simulation domain boundary conditions by-axis."""
node_type = ct.NodeType.BoundConds
bl_label = 'Bound Conds'
####################
# - Sockets
####################
input_socket_sets: typ.ClassVar = {
'XYZ': {
'X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
'±X | YZ': {
'+X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'-X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
'X | ±Y | Z': {
'X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'+Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'-Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
'XY | ±Z': {
'X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'+Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
'-Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
'±XY | Z': {
'+X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'-X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'+Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'-Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
'X | ±YZ': {
'X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'+Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'-Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'+Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
'-Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
'±XYZ': {
'+X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'-X': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.X}),
'+Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'-Y': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Y}),
'+Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
'-Z': sockets.MaxwellBoundCondSocketDef(allow_axes={SSA.Z}),
},
}
output_sockets: typ.ClassVar = {
'BCs': sockets.MaxwellBoundCondsSocketDef(),
}
####################
# - Output Socket Computation
####################
@events.computes_output_socket(
'BCs',
input_sockets={'X', 'Y', 'Z', '+X', '-X', '+Y', '-Y', '+Z', '-Z'},
input_sockets_optional={
'X': True,
'Y': True,
'Z': True,
'+X': True,
'-X': True,
'+Y': True,
'-Y': True,
'+Z': True,
'-Z': True,
},
)
def compute_boundary_conds(self, input_sockets) -> td.BoundarySpec:
"""Compute the simulation boundary conditions, by combining the individual input by specified half axis."""
log.debug(
'%s: Computing Boundary Conditions (Input Sockets = %s)',
self.sim_node_name,
str(input_sockets),
)
# Deduce "Doubledness"
## -> A "doubled" axis defines the same bound cond both ways
has_doubled_x = not ct.FlowSignal.check(input_sockets['X'])
has_doubled_y = not ct.FlowSignal.check(input_sockets['Y'])
has_doubled_z = not ct.FlowSignal.check(input_sockets['Z'])
# Deduce +/- of Each Axis
## +/- X
if has_doubled_x:
x_pos = input_sockets['X']
x_neg = input_sockets['X']
else:
x_pos = input_sockets['+X']
x_neg = input_sockets['-X']
## +/- Y
if has_doubled_y:
y_pos = input_sockets['Y']
y_neg = input_sockets['Y']
else:
y_pos = input_sockets['+Y']
y_neg = input_sockets['-Y']
## +/- Z
if has_doubled_z:
z_pos = input_sockets['Z']
z_neg = input_sockets['Z']
else:
z_pos = input_sockets['+Z']
z_neg = input_sockets['-Z']
return td.BoundarySpec(
x=td.Boundary(
plus=x_pos,
minus=x_neg,
),
y=td.Boundary(
plus=y_pos,
minus=y_neg,
),
z=td.Boundary(
plus=z_pos,
minus=z_neg,
),
)
####################
# - Blender Registration
####################
BL_REGISTER = [
BoundCondsNode,
]
BL_NODES = {ct.NodeType.BoundConds: (ct.NodeCategory.MAXWELLSIM_BOUNDS)}

View File

@ -1,25 +0,0 @@
from . import (
absorbing_bound_face,
bloch_bound_face,
pec_bound_face,
periodic_bound_face,
pmc_bound_face,
pml_bound_face,
)
BL_REGISTER = [
*pml_bound_face.BL_REGISTER,
*pec_bound_face.BL_REGISTER,
*pmc_bound_face.BL_REGISTER,
*bloch_bound_face.BL_REGISTER,
*periodic_bound_face.BL_REGISTER,
*absorbing_bound_face.BL_REGISTER,
]
BL_NODES = {
**pml_bound_face.BL_NODES,
**pec_bound_face.BL_NODES,
**pmc_bound_face.BL_NODES,
**bloch_bound_face.BL_NODES,
**periodic_bound_face.BL_NODES,
**absorbing_bound_face.BL_NODES,
}

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -3,6 +3,7 @@ import inspect
import typing as typ import typing as typ
from types import MappingProxyType from types import MappingProxyType
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger from blender_maxwell.utils import logger
from .. import contracts as ct from .. import contracts as ct
@ -10,7 +11,6 @@ from .. import contracts as ct
log = logger.get(__name__) log = logger.get(__name__)
UnitSystemID = str UnitSystemID = str
UnitSystem = dict[ct.SocketType, typ.Any]
#################### ####################
@ -70,7 +70,7 @@ def event_decorator(
all_loose_input_sockets: bool = False, all_loose_input_sockets: bool = False,
all_loose_output_sockets: bool = False, all_loose_output_sockets: bool = False,
# Request Unit System Scaling # Request Unit System Scaling
unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}), unit_systems: dict[UnitSystemID, spux.UnitSystem] = MappingProxyType({}),
scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}), scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}), scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
): ):
@ -213,7 +213,6 @@ def event_decorator(
kind=kind, kind=kind,
optional=output_sockets_optional.get(output_socket_name, False), optional=output_sockets_optional.get(output_socket_name, False),
), ),
node.outputs[output_socket_name].socket_type,
unit_systems.get(scale_output_sockets.get(output_socket_name)), unit_systems.get(scale_output_sockets.get(output_socket_name)),
) )
@ -269,9 +268,22 @@ def event_decorator(
else {} else {}
) )
# Propagate Initialization
## If there is a FlowInitializing, then the method would fail.
## Therefore, propagate FlowInitializing if found.
if any(
ct.FlowSignal.check_single(value, ct.FlowSignal.FlowInitializing)
for sockets in [
method_kw_args.get('input_sockets', {}),
method_kw_args.get('loose_input_sockets', {}),
method_kw_args.get('output_sockets', {}),
method_kw_args.get('loose_output_sockets', {}),
]
for value in sockets.values()
):
return ct.FlowSignal.FlowInitializing
# Call Method # Call Method
## If there is a FlowPending, then the method would fail.
## Therefore, propagate FlowPending if found.
return method( return method(
node, node,
**method_kw_args, **method_kw_args,

View File

@ -1,18 +1,22 @@
# from . import scientific_constant from . import (
# from . import physical_constant blender_constant,
from . import blender_constant, expr_constant, number_constant, scientific_constant expr_constant,
number_constant,
physical_constant,
scientific_constant,
)
BL_REGISTER = [ BL_REGISTER = [
*expr_constant.BL_REGISTER, *expr_constant.BL_REGISTER,
*scientific_constant.BL_REGISTER, *scientific_constant.BL_REGISTER,
*number_constant.BL_REGISTER, *number_constant.BL_REGISTER,
# *physical_constant.BL_REGISTER, *physical_constant.BL_REGISTER,
*blender_constant.BL_REGISTER, *blender_constant.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**expr_constant.BL_NODES, **expr_constant.BL_NODES,
**scientific_constant.BL_NODES, **scientific_constant.BL_NODES,
**number_constant.BL_NODES, **number_constant.BL_NODES,
# **physical_constant.BL_NODES, **physical_constant.BL_NODES,
**blender_constant.BL_NODES, **blender_constant.BL_NODES,
} }

View File

@ -44,7 +44,7 @@ class NumberConstantNode(base.MaxwellSimNode):
#################### ####################
# - UI # - UI
#################### ####################
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_props(self, _, col: bpy.types.UILayout) -> None:
row = col.row(align=True) row = col.row(align=True)
row.prop(self, self.blfields['mathtype'], text='') row.prop(self, self.blfields['mathtype'], text='')
row.prop(self, self.blfields['size'], text='') row.prop(self, self.blfields['size'], text='')
@ -56,7 +56,7 @@ class NumberConstantNode(base.MaxwellSimNode):
def on_mathtype_size_changed(self, props) -> None: def on_mathtype_size_changed(self, props) -> None:
"""Change the input/output expression sockets to match the mathtype declared in the node.""" """Change the input/output expression sockets to match the mathtype declared in the node."""
self.inputs['Value'].mathtype = props['mathtype'] self.inputs['Value'].mathtype = props['mathtype']
self.inputs['Value'].shape = props['mathtype'].shape self.inputs['Value'].shape = props['size'].shape
#################### ####################
# - FlowKind # - FlowKind

View File

@ -1,6 +1,6 @@
import enum
import typing as typ import typing as typ
import bpy
import sympy as sp import sympy as sp
from blender_maxwell.utils import bl_cache from blender_maxwell.utils import bl_cache
@ -10,7 +10,7 @@ from .... import contracts, sockets
from ... import base, events from ... import base, events
class PhysicalConstantNode(base.MaxwellSimTreeNode): class PhysicalConstantNode(base.MaxwellSimNode):
"""A number of configurable unit dimension, ex. time, length, etc. . """A number of configurable unit dimension, ex. time, length, etc. .
Attributes: Attributes:
@ -36,12 +36,12 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
prop_ui=True, prop_ui=True,
) )
mathtype: enum.Enum = bl_cache.BLField( mathtype: spux.MathType = bl_cache.BLField(
enum_cb=lambda self, _: self.search_mathtypes(), enum_cb=lambda self, _: self.search_mathtypes(),
prop_ui=True, prop_ui=True,
) )
size: enum.Enum = bl_cache.BLField( size: spux.NumberSize1D = bl_cache.BLField(
enum_cb=lambda self, _: self.search_sizes(), enum_cb=lambda self, _: self.search_sizes(),
prop_ui=True, prop_ui=True,
) )
@ -62,16 +62,25 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
if spux.NumberSize1D.supports_shape(shape) if spux.NumberSize1D.supports_shape(shape)
] ]
####################
# - UI
####################
def draw_props(self, _, col: bpy.types.UILayout) -> None:
row = col.row(align=True)
row.prop(self, self.blfields['mathtype'], text='')
row.prop(self, self.blfields['size'], text='')
#################### ####################
# - Events # - Events
#################### ####################
@events.on_value_changed( @events.on_value_changed(
prop_name={'physical_type', 'mathtype', 'size'}, prop_name={'physical_type', 'mathtype', 'size'},
run_on_init=True,
props={'physical_type', 'mathtype', 'size'}, props={'physical_type', 'mathtype', 'size'},
) )
def on_mathtype_or_size_changed(self, props) -> None: def on_mathtype_or_size_changed(self, props) -> None:
"""Change the input/output expression sockets to match the mathtype and size declared in the node.""" """Change the input/output expression sockets to match the mathtype and size declared in the node."""
shape = spux.NumberSize1D(props['size']).shape shape = props['size'].shape
# Set Input Socket Physical Type # Set Input Socket Physical Type
if self.inputs['Value'].physical_type != props['physical_type']: if self.inputs['Value'].physical_type != props['physical_type']:
@ -90,9 +99,9 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
#################### ####################
# - Callbacks # - Callbacks
#################### ####################
@events.computes_output_socket('value') @events.computes_output_socket('Value', input_sockets={'Value'})
def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr: def compute_value(self, input_sockets) -> sp.Expr:
return self.compute_input('value') return input_sockets['Value']
#################### ####################

View File

@ -2,7 +2,7 @@ import typing as typ
import bpy import bpy
from blender_maxwell.utils import sci_constants as constants from blender_maxwell.utils import bl_cache, sci_constants
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
@ -20,63 +20,43 @@ class ScientificConstantNode(base.MaxwellSimNode):
#################### ####################
# - Properties # - Properties
#################### ####################
sci_constant: bpy.props.StringProperty( sci_constant: str = bl_cache.BLField(
name='Sci Constant', '',
description='The name of a scientific constant', prop_ui=True,
default='', str_cb=lambda self, _, edit_text: self.search_sci_constants(edit_text),
search=lambda self, _, edit_text: self.search_sci_constants(edit_text),
update=lambda self, context: self.on_update_sci_constant(context),
) )
cache__units: bpy.props.StringProperty(default='')
cache__uncertainty: bpy.props.StringProperty(default='')
def search_sci_constants( def search_sci_constants(
self, self,
edit_text: str, edit_text: str,
): ):
return [ return [
name name
for name in constants.SCI_CONSTANTS for name in sci_constants.SCI_CONSTANTS
if edit_text.lower() in name.lower() if edit_text.lower() in name.lower()
] ]
def on_update_sci_constant(
self,
context: bpy.types.Context,
):
if self.sci_constant:
self.cache__units = str(
constants.SCI_CONSTANTS_INFO[self.sci_constant]['units']
)
self.cache__uncertainty = str(
constants.SCI_CONSTANTS_INFO[self.sci_constant]['uncertainty']
)
else:
self.cache__units = ''
self.cache__uncertainty = ''
self.on_prop_changed('sci_constant', context)
#################### ####################
# - UI # - UI
#################### ####################
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None: def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
col.prop(self, 'sci_constant', text='') col.prop(self, self.blfields['sci_constant'], text='')
def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None: def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
if self.sci_constant: if self.sci_constant:
col.label(text=f'Units: {self.cache__units}') col.label(
col.label(text=f'Uncertainty: {self.cache__uncertainty}') text=f'Units: {sci_constants.SCI_CONSTANTS_INFO[self.sci_constant]["units"]}'
)
col.label(text=f'Ref: {constants.SCI_CONSTANTS_REF[0]}') col.label(
text=f'Uncertainty: {sci_constants.SCI_CONSTANTS_INFO[self.sci_constant]["uncertainty"]}'
)
#################### ####################
# - Callbacks # - Output
#################### ####################
@events.computes_output_socket('Value', props={'sci_constant'}) @events.computes_output_socket('Value', props={'sci_constant'})
def compute_value(self, props: dict) -> typ.Any: def compute_value(self, props: dict) -> typ.Any:
return constants.SCI_CONSTANTS[props['sci_constant']] return sci_constants.SCI_CONSTANTS[props['sci_constant']]
#################### ####################

View File

@ -33,6 +33,7 @@ class WaveConstantNode(base.MaxwellSimNode):
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
'Wavelength': { 'Wavelength': {
'WL': sockets.ExprSocketDef( 'WL': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Value,
physical_type=spux.PhysicalType.Length, physical_type=spux.PhysicalType.Length,
# Defaults # Defaults
default_unit=spu.nm, default_unit=spu.nm,
@ -58,18 +59,18 @@ class WaveConstantNode(base.MaxwellSimNode):
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'WL': sockets.ExprSocketDef( 'WL': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Value, active_kind=ct.FlowKind.Value,
unit_dimension=spux.Dims.length, physical_type=spux.PhysicalType.Length,
), ),
'Freq': sockets.ExprSocketDef( 'Freq': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Value, active_kind=ct.FlowKind.Value,
unit_dimension=spux.Dims.frequency, physical_type=spux.PhysicalType.Freq,
), ),
} }
#################### ####################
# - Properties # - Properties
#################### ####################
use_range: bool = bl_cache.BLField(False) use_range: bool = bl_cache.BLField(False, prop_ui=True)
#################### ####################
# - UI # - UI
@ -80,14 +81,14 @@ class WaveConstantNode(base.MaxwellSimNode):
Parameters: Parameters:
col: Target for defining UI elements. col: Target for defining UI elements.
""" """
col.prop(self, self.blfields['use_range'], toggle=True) col.prop(self, self.blfields['use_range'], toggle=True, text='Range')
#################### ####################
# - Events # - Events
#################### ####################
@events.on_value_changed( @events.on_value_changed(
prop_name={'active_socket_set', 'use_range'}, prop_name={'active_socket_set', 'use_range'},
props='use_range', props={'use_range'},
run_on_init=True, run_on_init=True,
) )
def on_use_range_changed(self, props: dict) -> None: def on_use_range_changed(self, props: dict) -> None:
@ -128,7 +129,8 @@ class WaveConstantNode(base.MaxwellSimNode):
) )
def compute_wl_value(self, input_sockets: dict) -> sp.Expr: def compute_wl_value(self, input_sockets: dict) -> sp.Expr:
"""Compute a single wavelength value from either wavelength/frequency.""" """Compute a single wavelength value from either wavelength/frequency."""
if input_sockets['WL'] is not None: has_wl = not ct.FlowSignal.check(input_sockets['WL'])
if has_wl:
return input_sockets['WL'] return input_sockets['WL']
return sci_constants.vac_speed_of_light / input_sockets['Freq'] return sci_constants.vac_speed_of_light / input_sockets['Freq']
@ -141,7 +143,8 @@ class WaveConstantNode(base.MaxwellSimNode):
) )
def compute_freq_value(self, input_sockets: dict) -> sp.Expr: def compute_freq_value(self, input_sockets: dict) -> sp.Expr:
"""Compute a single frequency value from either wavelength/frequency.""" """Compute a single frequency value from either wavelength/frequency."""
if input_sockets['Freq'] is not None: has_freq = not ct.FlowSignal.check(input_sockets['Freq'])
if has_freq:
return input_sockets['Freq'] return input_sockets['Freq']
return sci_constants.vac_speed_of_light / input_sockets['WL'] return sci_constants.vac_speed_of_light / input_sockets['WL']
@ -158,11 +161,20 @@ class WaveConstantNode(base.MaxwellSimNode):
) )
def compute_wl_range(self, input_sockets: dict) -> sp.Expr: def compute_wl_range(self, input_sockets: dict) -> sp.Expr:
"""Compute wavelength range from either wavelength/frequency ranges.""" """Compute wavelength range from either wavelength/frequency ranges."""
if input_sockets['WL'] is not None: has_wl = not ct.FlowSignal.check(input_sockets['WL'])
if has_wl:
return input_sockets['WL'] return input_sockets['WL']
return input_sockets['Freq'].rescale_bounds( freq = input_sockets['Freq']
lambda bound: sci_constants.vac_speed_of_light / bound, reverse=True return ct.LazyArrayRangeFlow(
start=spux.scale_to_unit(
sci_constants.vac_speed_of_light / (freq.stop * freq.unit), spu.um
),
stop=spux.scale_to_unit(
sci_constants.vac_speed_of_light / (freq.start * freq.unit), spu.um
),
steps=freq.steps,
unit=spu.um,
) )
@events.computes_output_socket( @events.computes_output_socket(
@ -177,11 +189,20 @@ class WaveConstantNode(base.MaxwellSimNode):
) )
def compute_freq_range(self, input_sockets: dict) -> sp.Expr: def compute_freq_range(self, input_sockets: dict) -> sp.Expr:
"""Compute frequency range from either wavelength/frequency ranges.""" """Compute frequency range from either wavelength/frequency ranges."""
if input_sockets['Freq'] is not None: has_freq = not ct.FlowSignal.check(input_sockets['Freq'])
if has_freq:
return input_sockets['Freq'] return input_sockets['Freq']
return input_sockets['WL'].rescale_bounds( wl = input_sockets['WL']
lambda bound: sci_constants.vac_speed_of_light / bound, reverse=True return ct.LazyArrayRangeFlow(
start=spux.scale_to_unit(
sci_constants.vac_speed_of_light / (wl.stop * wl.unit), spux.THz
),
stop=spux.scale_to_unit(
sci_constants.vac_speed_of_light / (wl.start * wl.unit), spux.THz
),
steps=wl.steps,
unit=spux.THz,
) )

View File

@ -35,7 +35,7 @@ class LoadCloudSim(bpy.types.Operator):
node = context.node node = context.node
# Try Loading Simulation Data # Try Loading Simulation Data
node.sim_data = bl_cache.Signal.InvalidateCache #node.sim_data = bl_cache.Signal.InvalidateCache
sim_data = node.sim_data sim_data = node.sim_data
if sim_data is None: if sim_data is None:
self.report( self.report(
@ -70,18 +70,26 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
should_exist=True, should_exist=True,
), ),
} }
output_sockets: typ.ClassVar = {
'Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
}
####################
# - Properties
####################
sim_data_loaded: bool = bl_cache.BLField(False) sim_data_loaded: bool = bl_cache.BLField(False)
@bl_cache.cached_bl_property() ####################
# - Computed
####################
@property
def sim_data(self) -> td.SimulationData | None: def sim_data(self) -> td.SimulationData | None:
cloud_task = self._compute_input( cloud_task = self._compute_input(
'Cloud Task', kind=ct.FlowKind.Value, optional=True 'Cloud Task', kind=ct.FlowKind.Value, optional=True
) )
has_cloud_task = not ct.FlowSignal.check(cloud_task)
if ( if (
# Check Flow has_cloud_task
not ct.FlowSignal.check(cloud_task)
# Check Task
and cloud_task is not None and cloud_task is not None
and isinstance(cloud_task, tdcloud.CloudTask) and isinstance(cloud_task, tdcloud.CloudTask)
and cloud_task.status == 'success' and cloud_task.status == 'success'
@ -97,7 +105,7 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
#################### ####################
# - UI # - UI
#################### ####################
def draw_operators(self, context, layout): def draw_operators(self, _: bpy.types.Context, layout: bpy.types.UILayout):
if self.sim_data_loaded: if self.sim_data_loaded:
layout.operator(ct.OperatorType.NodeLoadCloudSim, text='Reload Sim') layout.operator(ct.OperatorType.NodeLoadCloudSim, text='Reload Sim')
else: else:
@ -106,11 +114,6 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
#################### ####################
# - Events # - Events
#################### ####################
@events.on_value_changed(socket_name='Cloud Task')
def on_cloud_task_changed(self):
self.inputs['Cloud Task'].on_cloud_updated()
## TODO: Must we babysit sockets like this?
@events.on_value_changed( @events.on_value_changed(
prop_name='sim_data_loaded', run_on_init=True, props={'sim_data_loaded'} prop_name='sim_data_loaded', run_on_init=True, props={'sim_data_loaded'}
) )

View File

@ -33,6 +33,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Size': sockets.ExprSocketDef( 'Size': sockets.ExprSocketDef(
shape=(3,), shape=(3,),
physical_type=spux.PhysicalType.Length, physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
), ),
'Spatial Subdivs': sockets.ExprSocketDef( 'Spatial Subdivs': sockets.ExprSocketDef(
shape=(3,), shape=(3,),
@ -124,11 +125,30 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
# - Preview # - Preview
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, # Trigger
prop_name='preview_active', prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
)
def on_preview_changed(self, managed_objs, props, input_sockets):
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
else:
mesh.hide_preview()
@events.on_value_changed(
# Trigger
socket_name={'Center', 'Size'},
run_on_init=True,
# Loaded
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={
'Center': 'BlenderUnits', 'Center': 'BlenderUnits',
@ -136,7 +156,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
) )
def on_inputs_changed( def on_inputs_changed(
self, self,
props: dict,
managed_objs: dict, managed_objs: dict,
input_sockets: dict, input_sockets: dict,
unit_systems: dict, unit_systems: dict,
@ -153,9 +172,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
}, },
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
#################### ####################

View File

@ -31,6 +31,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
'Size': sockets.ExprSocketDef( 'Size': sockets.ExprSocketDef(
shape=(3,), shape=(3,),
physical_type=spux.PhysicalType.Length, physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
), ),
'Samples/Space': sockets.ExprSocketDef( 'Samples/Space': sockets.ExprSocketDef(
shape=(3,), shape=(3,),
@ -123,11 +124,29 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
# - Preview - Changes to Input Sockets # - Preview - Changes to Input Sockets
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, # Trigger
prop_name='preview_active', prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center', 'Size'}, )
def on_preview_changed(self, managed_objs, props):
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
else:
mesh.hide_preview()
@events.on_value_changed(
# Trigger
socket_name={'Center', 'Size'},
run_on_init=True,
# Loaded
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={
'Center': 'BlenderUnits', 'Center': 'BlenderUnits',
@ -135,7 +154,6 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
) )
def on_inputs_changed( def on_inputs_changed(
self, self,
props: dict,
managed_objs: dict, managed_objs: dict,
input_sockets: dict, input_sockets: dict,
unit_systems: dict, unit_systems: dict,
@ -152,9 +170,6 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
}, },
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
#################### ####################

View File

@ -1,12 +1,13 @@
from . import file_exporters, viewer, web_exporters #from . import file_exporters, viewer, web_exporters
from . import viewer
BL_REGISTER = [ BL_REGISTER = [
*viewer.BL_REGISTER, *viewer.BL_REGISTER,
*file_exporters.BL_REGISTER, #*file_exporters.BL_REGISTER,
*web_exporters.BL_REGISTER, #*web_exporters.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**viewer.BL_NODES, **viewer.BL_NODES,
**file_exporters.BL_NODES, #**file_exporters.BL_NODES,
**web_exporters.BL_NODES, #**web_exporters.BL_NODES,
} }

View File

@ -35,27 +35,8 @@ class SimDomainNode(base.MaxwellSimNode):
} }
#################### ####################
# - Event Methods # - Events
#################### ####################
@events.computes_output_socket(
'Domain',
input_sockets={'Duration', 'Center', 'Size', 'Grid', 'Ambient Medium'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Duration': 'Tidy3DUnits',
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
},
)
def compute_domain(self, input_sockets: dict, unit_systems) -> sp.Expr:
return {
'run_time': input_sockets['Duration'],
'center': input_sockets['Center'],
'size': input_sockets['Size'],
'grid_spec': input_sockets['Grid'],
'medium': input_sockets['Ambient Medium'],
}
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active', prop_name='preview_active',
@ -91,6 +72,28 @@ class SimDomainNode(base.MaxwellSimNode):
if props['preview_active']: if props['preview_active']:
managed_objs['mesh'].show_preview() managed_objs['mesh'].show_preview()
####################
# - Outputs
####################
@events.computes_output_socket(
'Domain',
input_sockets={'Duration', 'Center', 'Size', 'Grid', 'Ambient Medium'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Duration': 'Tidy3DUnits',
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
},
)
def compute_domain(self, input_sockets, unit_systems) -> sp.Expr:
return {
'run_time': input_sockets['Duration'],
'center': input_sockets['Center'],
'size': input_sockets['Size'],
'grid_spec': input_sockets['Grid'],
'medium': input_sockets['Ambient Medium'],
}
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -217,7 +217,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Called by `self.on_prop_changed()` when `self.active_kind` was changed. Called by `self.on_prop_changed()` when `self.active_kind` was changed.
""" """
self.display_shape = ( self.display_shape = (
'SQUARE' if self.active_kind == ct.FlowKind.LazyValueRange else 'CIRCLE' 'SQUARE' if self.active_kind == ct.FlowKind.LazyArrayRange else 'CIRCLE'
) # + ('_DOT' if self.use_units else '') ) # + ('_DOT' if self.use_units else '')
## TODO: Valid Active Kinds should be a subset/subenum(?) of FlowKind ## TODO: Valid Active Kinds should be a subset/subenum(?) of FlowKind
@ -373,6 +373,32 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
""" """
self.trigger_event(ct.FlowEvent.LinkChanged) self.trigger_event(ct.FlowEvent.LinkChanged)
def remove_invalidated_links(self) -> None:
"""Reevaluates the capabilities of all socket links, and removes any that no longer match.
Links are removed with a simple `node_tree.links.remove()`, which directly emulates a user trying to remove the node link.
**Note** that all of the usual consent-semantics apply just the same as if the user had manually tried to remove the link.
Notes:
Called by nodes directly on their sockets, after altering any property that might influence the capabilities of that socket.
This prevents invalid use when the user alters a property, which **would** disallow adding a _new_ link identical to one that already exists.
In such a case, the existing (non-capability-respecting) link should be removed, as it has become invalid.
"""
node_tree = self.id_data
for link in self.links:
if not link.from_socket.capabilities.is_compatible_with(
link.to_socket.capabilities
):
log.error(
'Deleted link between "%s" (%s) and "%s" (%s) due to invalidated capabilities',
link.from_socket.bl_label,
link.from_socket.capabilities,
link.to_socket.bl_label,
link.to_socket.capabilities,
)
node_tree.links.remove(link)
#################### ####################
# - Event Chain # - Event Chain
#################### ####################

View File

@ -2,6 +2,7 @@ import enum
import typing as typ import typing as typ
import bpy import bpy
import pydantic as pyd
import sympy as sp import sympy as sp
from blender_maxwell.utils import bl_cache, logger from blender_maxwell.utils import bl_cache, logger
@ -63,6 +64,7 @@ class InfoDisplayCol(enum.StrEnum):
class ExprBLSocket(base.MaxwellSimSocket): class ExprBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Expr socket_type = ct.SocketType.Expr
bl_label = 'Expr' bl_label = 'Expr'
use_info_draw = True
#################### ####################
# - Properties # - Properties
@ -70,7 +72,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
shape: tuple[int, ...] | None = bl_cache.BLField(None) shape: tuple[int, ...] | None = bl_cache.BLField(None)
mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real, prop_ui=True) mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real, prop_ui=True)
physical_type: spux.PhysicalType | None = bl_cache.BLField(None) physical_type: spux.PhysicalType | None = bl_cache.BLField(None)
symbols: frozenset[spux.Symbol] = bl_cache.BLField(frozenset()) symbols: frozenset[sp.Symbol] = bl_cache.BLField(frozenset())
active_unit: enum.Enum = bl_cache.BLField( active_unit: enum.Enum = bl_cache.BLField(
None, enum_cb=lambda self, _: self.search_units(), prop_ui=True None, enum_cb=lambda self, _: self.search_units(), prop_ui=True
@ -102,7 +104,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
) )
# UI: LazyArrayRange # UI: LazyArrayRange
steps: int = bl_cache.BLField(2, abs_min=2) steps: int = bl_cache.BLField(2, abs_min=2, prop_ui=True)
## Expression ## Expression
raw_min_spstr: str = bl_cache.BLField('', prop_ui=True) raw_min_spstr: str = bl_cache.BLField('', prop_ui=True)
raw_max_spstr: str = bl_cache.BLField('', prop_ui=True) raw_max_spstr: str = bl_cache.BLField('', prop_ui=True)
@ -125,6 +127,15 @@ class ExprBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Computed: Raw Expressions # - Computed: Raw Expressions
#################### ####################
@property
def sorted_symbols(self) -> list[sp.Symbol]:
"""Retrieves all symbols and sorts them by name.
Returns:
Repeateably ordered list of symbols.
"""
return sorted(self.symbols, key=lambda sym: sym.name)
@property @property
def raw_value_sp(self) -> spux.SympyExpr: def raw_value_sp(self) -> spux.SympyExpr:
return self._parse_expr_str(self.raw_value_spstr) return self._parse_expr_str(self.raw_value_spstr)
@ -140,7 +151,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Computed: Units # - Computed: Units
#################### ####################
def search_units(self, _: bpy.types.Context) -> list[ct.BLEnumElement]: def search_units(self) -> list[ct.BLEnumElement]:
if self.physical_type is not None: if self.physical_type is not None:
return [ return [
(sp.sstr(unit), spux.sp_to_str(unit), sp.sstr(unit), '', i) (sp.sstr(unit), spux.sp_to_str(unit), sp.sstr(unit), '', i)
@ -163,33 +174,38 @@ class ExprBLSocket(base.MaxwellSimSocket):
return None return None
@unit.setter @unit.setter
def unit(self, unit: spux.Unit) -> None: def unit(self, unit: spux.Unit | None) -> None:
"""Set the unit, without touching the `raw_*` UI properties. """Set the unit, without touching the `raw_*` UI properties.
Notes: Notes:
To set a new unit, **and** convert the `raw_*` UI properties to the new unit, use `self.convert_unit()` instead. To set a new unit, **and** convert the `raw_*` UI properties to the new unit, use `self.convert_unit()` instead.
""" """
if unit in self.physical_type.valid_units: if self.physical_type is not None:
self.active_unit = sp.sstr(unit) if unit in self.physical_type.valid_units:
self.active_unit = sp.sstr(unit)
msg = f'Tried to set invalid unit {unit} (physical type "{self.physical_type}" only supports "{self.physical_type.valid_units}")' else:
raise ValueError(msg) msg = f'Tried to set invalid unit {unit} (physical type "{self.physical_type}" only supports "{self.physical_type.valid_units}")'
raise ValueError(msg)
elif unit is not None:
msg = f'Tried to set invalid unit {unit} (physical type is {self.physical_type}, and has no unit support!)")'
raise ValueError(msg)
def convert_unit(self, unit_to: spux.Unit) -> None: def convert_unit(self, unit_to: spux.Unit) -> None:
if self.active_kind == ct.FlowKind.Value: current_value = self.value
current_value = self.value current_lazy_array_range = self.lazy_array_range
self.unit = unit_to
self.value = current_value self.unit = bl_cache.Signal.InvalidateCache
elif self.active_kind == ct.FlowKind.LazyArrayRange:
current_lazy_array_range = self.lazy_array_range self.value = current_value
self.unit = unit_to self.lazy_array_range = current_lazy_array_range
self.lazy_array_range = current_lazy_array_range
#################### ####################
# - Property Callback # - Property Callback
#################### ####################
def on_socket_prop_changed(self, prop_name: str) -> None: def on_socket_prop_changed(self, prop_name: str) -> None:
if prop_name == 'unit' and self.active_unit is not None: if prop_name == 'physical_type':
self.active_unit = bl_cache.Signal.ResetEnumItems
if prop_name == 'active_unit' and self.active_unit is not None:
self.convert_unit(spux.unit_str_to_unit(self.active_unit)) self.convert_unit(spux.unit_str_to_unit(self.active_unit))
#################### ####################
@ -200,23 +216,23 @@ class ExprBLSocket(base.MaxwellSimSocket):
) -> tuple[spux.MathType, tuple[int, ...] | None, spux.UnitDimension]: ) -> tuple[spux.MathType, tuple[int, ...] | None, spux.UnitDimension]:
# Parse MathType # Parse MathType
mathtype = spux.MathType.from_expr(expr) mathtype = spux.MathType.from_expr(expr)
if self.mathtype != mathtype: if not self.mathtype.is_compatible(mathtype):
msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with mathtype {mathtype}' msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with mathtype {mathtype}'
raise ValueError(msg) raise ValueError(msg)
# Parse Symbols # Parse Symbols
if expr.free_symbols: if expr.free_symbols and not expr.free_symbols.issubset(self.symbols):
if self.mathtype is not None: msg = f'Tried to set expr {expr} with free symbols {expr.free_symbols}, which is incompatible with socket symbols {self.symbols}'
msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with free symbols {expr.free_symbols}' raise ValueError(msg)
raise ValueError(msg)
if not expr.free_symbols.issubset(self.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 # Parse Dimensions
shape = spux.parse_shape(expr) shape = spux.parse_shape(expr)
if shape != self.shape: if shape != self.shape and not (
shape is not None
and self.shape is not None
and len(self.shape) == 1
and 1 in shape
):
msg = f'Expr {expr} has shape {shape}, which is incompatible with the expr socket (shape {self.shape})' msg = f'Expr {expr} has shape {shape}, which is incompatible with the expr socket (shape {self.shape})'
raise ValueError(msg) raise ValueError(msg)
@ -238,7 +254,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
# Try Parsing and Returning the Expression # Try Parsing and Returning the Expression
try: try:
self._parse_expr_info(expr) self._parse_expr_info(expr)
except ValueError(expr) as ex: except ValueError:
log.exception( log.exception(
'Couldn\'t parse expression "%s" in Expr socket.', 'Couldn\'t parse expression "%s" in Expr socket.',
expr_spstr, expr_spstr,
@ -270,6 +286,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
expr = self.raw_value_sp expr = self.raw_value_sp
if expr is None: if expr is None:
return ct.FlowSignal.FlowPending return ct.FlowSignal.FlowPending
return expr
MT_Z = spux.MathType.Integer MT_Z = spux.MathType.Integer
MT_Q = spux.MathType.Rational MT_Q = spux.MathType.Rational
@ -312,7 +329,7 @@ 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, shape = self._parse_expr_info(expr) _mathtype, _shape = self._parse_expr_info(expr)
if self.symbols or self.shape not in [None, (2,), (3,)]: if self.symbols or self.shape not in [None, (2,), (3,)]:
self.raw_value_spstr = sp.sstr(expr) self.raw_value_spstr = sp.sstr(expr)
@ -321,32 +338,33 @@ class ExprBLSocket(base.MaxwellSimSocket):
MT_Q = spux.MathType.Rational MT_Q = spux.MathType.Rational
MT_R = spux.MathType.Real MT_R = spux.MathType.Real
MT_C = spux.MathType.Complex MT_C = spux.MathType.Complex
if shape is None: if self.shape is None:
if mathtype == MT_Z: if self.mathtype == MT_Z:
self.raw_value_int = self._to_raw_value(expr) self.raw_value_int = self._to_raw_value(expr)
elif mathtype == MT_Q: elif self.mathtype == MT_Q:
self.raw_value_rat = self._to_raw_value(expr) self.raw_value_rat = self._to_raw_value(expr)
elif mathtype == MT_R: elif self.mathtype == MT_R:
self.raw_value_float = self._to_raw_value(expr) self.raw_value_float = self._to_raw_value(expr)
elif mathtype == MT_C: elif self.mathtype == MT_C:
self.raw_value_complex = self._to_raw_value(expr) self.raw_value_complex = self._to_raw_value(expr)
elif shape == (2,): elif self.shape == (2,):
if mathtype == MT_Z: if self.mathtype == MT_Z:
self.raw_value_int2 = self._to_raw_value(expr) self.raw_value_int2 = self._to_raw_value(expr)
elif mathtype == MT_Q: elif self.mathtype == MT_Q:
self.raw_value_rat2 = self._to_raw_value(expr) self.raw_value_rat2 = self._to_raw_value(expr)
elif mathtype == MT_R: elif self.mathtype == MT_R:
self.raw_value_float2 = self._to_raw_value(expr) self.raw_value_float2 = self._to_raw_value(expr)
elif mathtype == MT_C: elif self.mathtype == MT_C:
self.raw_value_complex2 = self._to_raw_value(expr) self.raw_value_complex2 = self._to_raw_value(expr)
elif shape == (3,): elif self.shape == (3,):
if mathtype == MT_Z: log.critical(expr)
if self.mathtype == MT_Z:
self.raw_value_int3 = self._to_raw_value(expr) self.raw_value_int3 = self._to_raw_value(expr)
elif mathtype == MT_Q: elif self.mathtype == MT_Q:
self.raw_value_rat3 = self._to_raw_value(expr) self.raw_value_rat3 = self._to_raw_value(expr)
elif mathtype == MT_R: elif self.mathtype == MT_R:
self.raw_value_float3 = self._to_raw_value(expr) self.raw_value_float3 = self._to_raw_value(expr)
elif mathtype == MT_C: elif self.mathtype == MT_C:
self.raw_value_complex3 = self._to_raw_value(expr) self.raw_value_complex3 = self._to_raw_value(expr)
#################### ####################
@ -404,7 +422,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
Called to compute the internal `FlowKind.LazyArrayRange` of this socket. Called to compute the internal `FlowKind.LazyArrayRange` of this socket.
""" """
self.steps = value.steps self.steps = value.steps
self.unit = value.unit
if self.symbols: if self.symbols:
self.raw_min_spstr = sp.sstr(value.start) self.raw_min_spstr = sp.sstr(value.start)
@ -416,21 +433,26 @@ 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
if value.mathtype == MT_Z: if value.mathtype == MT_Z:
self.raw_range_int = [ self.raw_range_int = [
self._to_raw_value(bound) for bound in [value.start, value.stop] self._to_raw_value(bound * unit)
for bound in [value.start, value.stop]
] ]
elif value.mathtype == MT_Q: elif value.mathtype == MT_Q:
self.raw_range_rat = [ self.raw_range_rat = [
self._to_raw_value(bound) for bound in [value.start, value.stop] self._to_raw_value(bound * unit)
for bound in [value.start, value.stop]
] ]
elif value.mathtype == MT_R: elif value.mathtype == MT_R:
self.raw_range_float = [ self.raw_range_float = [
self._to_raw_value(bound) for bound in [value.start, value.stop] self._to_raw_value(bound * unit)
for bound in [value.start, value.stop]
] ]
elif value.mathtype == MT_C: elif value.mathtype == MT_C:
self.raw_range_complex = [ self.raw_range_complex = [
self._to_raw_value(bound) for bound in [value.start, value.stop] self._to_raw_value(bound * unit)
for bound in [value.start, value.stop]
] ]
#################### ####################
@ -441,8 +463,8 @@ class ExprBLSocket(base.MaxwellSimSocket):
# Lazy Value: Arbitrary Expression # Lazy Value: Arbitrary Expression
if self.symbols or self.shape not in [None, (2,), (3,)]: if self.symbols or self.shape not in [None, (2,), (3,)]:
return ct.LazyValueFuncFlow( return ct.LazyValueFuncFlow(
func=sp.lambdify(self.symbols, self.value, 'jax'), func=sp.lambdify(self.sorted_symbols, self.value, 'jax'),
func_args=[spux.MathType.from_expr(sym) for sym in self.symbols], func_args=[spux.MathType.from_expr(sym) for sym in self.sorted_symbols],
supports_jax=True, supports_jax=True,
) )
@ -482,8 +504,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
unit=self.unit, unit=self.unit,
) )
msg = "Expr socket can't produce array from expression with free symbols" return ct.FlowSignal.NoFlow
raise ValueError(msg)
#################### ####################
# - FlowKind: Info # - FlowKind: Info
@ -496,6 +517,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
output_mathtype=self.mathtype, output_mathtype=self.mathtype,
output_unit=self.unit, output_unit=self.unit,
) )
## TODO: When expression can be used w/arrays, then allow directly outputting a LazyArrayRange pumped through the given expression. Or something like that.
#################### ####################
# - FlowKind: Capabilities # - FlowKind: Capabilities
@ -520,10 +542,11 @@ class ExprBLSocket(base.MaxwellSimSocket):
_row.label(text=text) _row.label(text=text)
_col = split.column(align=True) _col = split.column(align=True)
_col.prop(self, 'active_unit', text='') _col.prop(self, self.blfields['active_unit'], text='')
else:
row.label(text=text)
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
# Property Interface
if self.symbols: if self.symbols:
col.prop(self, self.blfields['raw_value_spstr'], text='') col.prop(self, self.blfields['raw_value_spstr'], text='')
@ -575,6 +598,27 @@ class ExprBLSocket(base.MaxwellSimSocket):
for sym in self.symbols: for sym in self.symbols:
col.label(text=spux.pretty_symbol(sym)) col.label(text=spux.pretty_symbol(sym))
def draw_lazy_array_range(self, col: bpy.types.UILayout) -> None:
if self.symbols:
col.prop(self, self.blfields['raw_min_spstr'], text='')
col.prop(self, self.blfields['raw_max_spstr'], text='')
else:
MT_Z = spux.MathType.Integer
MT_Q = spux.MathType.Rational
MT_R = spux.MathType.Real
MT_C = spux.MathType.Complex
if self.mathtype == MT_Z:
col.prop(self, self.blfields['raw_range_int'], text='')
elif self.mathtype == MT_Q:
col.prop(self, self.blfields['raw_range_rat'], text='')
elif self.mathtype == MT_R:
col.prop(self, self.blfields['raw_range_float'], text='')
elif self.mathtype == MT_C:
col.prop(self, self.blfields['raw_range_complex'], text='')
col.prop(self, self.blfields['steps'], text='')
def draw_input_label_row(self, row: bpy.types.UILayout, text) -> None: def draw_input_label_row(self, row: bpy.types.UILayout, text) -> None:
info = self.compute_data(kind=ct.FlowKind.Info) info = self.compute_data(kind=ct.FlowKind.Info)
has_dims = not ct.FlowSignal.check(info) and info.dim_names has_dims = not ct.FlowSignal.check(info) and info.dim_names
@ -630,7 +674,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
_row.label(text=text) _row.label(text=text)
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None: def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
if info.dim_names and self.show_info_columns: if self.show_info_columns:
row = col.row() row = col.row()
box = row.box() box = row.box()
grid = box.grid_flow( grid = box.grid_flow(
@ -696,19 +740,91 @@ class ExprSocketDef(base.SocketDef):
default_unit: spux.Unit | None = None default_unit: spux.Unit | None = None
# FlowKind: Value # FlowKind: Value
default_value: spux.SympyExpr = sp.S(0) default_value: spux.SympyExpr = sp.RealNumber(0)
abs_min: spux.SympyExpr | None = None ## TODO: Not used (yet)
abs_max: spux.SympyExpr | None = None ## TODO: Not used (yet)
## TODO: Idea is to use this scalar uniformly for all shape elements
## TODO: -> But we may want to **allow** using same-shape for diff. bounds.
# FlowKind: LazyArrayRange # FlowKind: LazyArrayRange
default_min: spux.SympyExpr = sp.S(0) default_min: spux.SympyExpr = sp.RealNumber(0)
default_max: spux.SympyExpr = sp.S(1) default_max: spux.SympyExpr = sp.RealNumber(1)
default_steps: int = 2 default_steps: int = 2
## TODO: Configure lin/log/... scaling (w/enumprop in UI) ## TODO: Configure lin/log/... scaling (w/enumprop in UI)
## TODO: Buncha validation :)
# UI # UI
show_info_columns: bool = False show_info_columns: bool = False
####################
# - Validators - Coersion
####################
@pyd.model_validator(mode='after')
def shape_value_coersion(self) -> str:
if self.shape is not None and not isinstance(self.default_value, sp.MatrixBase):
if len(self.shape) == 1:
self.default_value = self.default_value * sp.Matrix.ones(
self.shape[0], 1
)
if len(self.shape) == 2:
self.default_value = self.default_value * sp.Matrix.ones(*self.shape)
return self
@pyd.model_validator(mode='after')
def unit_coersion(self) -> str:
if self.physical_type is not None and self.default_unit is None:
self.default_unit = self.physical_type.default_unit
return self
####################
# - Validators - Assertion
####################
@pyd.model_validator(mode='after')
def valid_shapes(self) -> str:
if self.active_kind == ct.FlowKind.LazyArrayRange and self.shape is not None:
msg = "Can't have a non-None shape when LazyArrayRange is set as the active kind."
raise ValueError(msg)
return self
@pyd.model_validator(mode='after')
def mathtype_value(self) -> str:
default_value_mathtype = spux.MathType.from_expr(self.default_value)
if not self.mathtype.is_compatible(default_value_mathtype):
msg = f'MathType is {self.mathtype}, but tried to set default value {self.default_value} with mathtype {default_value_mathtype}'
raise ValueError(msg)
return self
@pyd.model_validator(mode='after')
def symbols_value(self) -> str:
if (
self.default_value.free_symbols
and not self.default_value.free_symbols.issubset(self.symbols)
):
msg = f'Tried to set default value {self.default_value} with free symbols {self.default_value.free_symbols}, which is incompatible with socket symbols {self.symbols}'
raise ValueError(msg)
return self
@pyd.model_validator(mode='after')
def shape_value(self) -> str:
shape = spux.parse_shape(self.default_value)
if shape != self.shape and not (
shape is not None
and self.shape is not None
and len(self.shape) == 1
and 1 in shape
):
msg = f'Default value {self.default_value} has shape {shape}, which is incompatible with the expr socket (shape {self.shape})'
raise ValueError(msg)
return self
####################
# - Initialization
####################
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
@ -718,12 +834,12 @@ class ExprSocketDef(base.SocketDef):
bl_socket.physical_type = self.physical_type bl_socket.physical_type = self.physical_type
bl_socket.symbols = self.symbols bl_socket.symbols = self.symbols
# Socket Units # Socket Units & FlowKind.Value
if self.default_unit is not None: if self.physical_type is not None:
bl_socket.unit = self.default_unit bl_socket.unit = self.default_unit
bl_socket.value = self.default_value * self.default_unit
# FlowKind: Value else:
bl_socket.value = self.default_value bl_socket.value = self.default_value
# FlowKind: LazyArrayRange # FlowKind: LazyArrayRange
bl_socket.lazy_array_range = ct.LazyArrayRangeFlow( bl_socket.lazy_array_range = ct.LazyArrayRangeFlow(

View File

@ -3,51 +3,64 @@ import typing as typ
import bpy import bpy
import tidy3d as td import tidy3d as td
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__)
class MaxwellBoundCondBLSocket(base.MaxwellSimSocket): class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
"""Describes a single of boundary condition to apply to the half-axis of a simulation domain.
Attributes:
default: The default boundary condition type.
"""
socket_type = ct.SocketType.MaxwellBoundCond socket_type = ct.SocketType.MaxwellBoundCond
bl_label = 'Maxwell Bound Face' bl_label = 'Maxwell Bound Cond'
#################### ####################
# - Properties # - Properties
#################### ####################
default_choice: bpy.props.EnumProperty( default: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
name='Bound Face',
description='A choice of default boundary face', # Capabilities
items=[ ## Allow a boundary condition compatible with any of the following axes.
('PML', 'PML', 'Perfectly matched layer'), allow_axes: set[ct.SimSpaceAxis] = bl_cache.BLField(
('PEC', 'PEC', 'Perfect electrical conductor'), {ct.SimSpaceAxis.X, ct.SimSpaceAxis.Y, ct.SimSpaceAxis.Z},
('PMC', 'PMC', 'Perfect magnetic conductor'), )
('PERIODIC', 'Periodic', 'Infinitely periodic layer'), ## Present a boundary condition compatible with any of the following axes.
], present_axes: set[ct.SimSpaceAxis] = bl_cache.BLField(
default='PML', {ct.SimSpaceAxis.X, ct.SimSpaceAxis.Y, ct.SimSpaceAxis.Z},
update=(lambda self, context: self.on_prop_changed('default_choice', context)),
) )
#################### ####################
# - UI # - UI
#################### ####################
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, 'default_choice', text='') col.prop(self, self.blfields['default'], text='')
#################### ####################
# - Computation of Default Value # - FlowKind
#################### ####################
@property @property
def value(self) -> td.BoundarySpec: def capabilities(self) -> ct.CapabilitiesFlow:
return { return ct.CapabilitiesFlow(
'PML': td.PML(num_layers=12), socket_type=self.socket_type,
'PEC': td.PECBoundary(), active_kind=self.active_kind,
'PMC': td.PMCBoundary(), allow_any=self.allow_axes,
'PERIODIC': td.Periodic(), present_any=self.present_axes,
}[self.default_choice] )
@property
def value(self) -> td.BoundaryEdge:
return self.default.tidy3d_boundary_edge
@value.setter @value.setter
def value(self, value: typ.Literal['PML', 'PEC', 'PMC', 'PERIODIC']) -> None: def value(self, value: ct.BoundCondType) -> None:
self.default_choice = value self.default = value
#################### ####################
@ -56,10 +69,23 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
class MaxwellBoundCondSocketDef(base.SocketDef): class MaxwellBoundCondSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.MaxwellBoundCond socket_type: ct.SocketType = ct.SocketType.MaxwellBoundCond
default_choice: typ.Literal['PML', 'PEC', 'PMC', 'PERIODIC'] = 'PML' default: ct.BoundCondType = ct.BoundCondType.Pml
allow_axes: set[ct.SimSpaceAxis] = {
ct.SimSpaceAxis.X,
ct.SimSpaceAxis.Y,
ct.SimSpaceAxis.Z,
}
present_axes: set[ct.SimSpaceAxis] = {
ct.SimSpaceAxis.X,
ct.SimSpaceAxis.Y,
ct.SimSpaceAxis.Z,
}
def init(self, bl_socket: MaxwellBoundCondBLSocket) -> None: def init(self, bl_socket: MaxwellBoundCondBLSocket) -> None:
bl_socket.value = self.default_choice bl_socket.default = self.default
bl_socket.allow_axes = self.allow_axes
bl_socket.present_axes = self.present_axes
#################### ####################

View File

@ -1,123 +1,101 @@
"""Implements the `MaxwellBoundCondsBLSocket` socket."""
import bpy import bpy
import tidy3d as td import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
BOUND_FACE_ITEMS = [ log = logger.get(__name__)
('PML', 'PML', 'Perfectly matched layer'),
('PEC', 'PEC', 'Perfect electrical conductor'),
('PMC', 'PMC', 'Perfect magnetic conductor'),
('PERIODIC', 'Periodic', 'Infinitely periodic layer'),
]
BOUND_MAP = {
'PML': td.PML(),
'PEC': td.PECBoundary(),
'PMC': td.PMCBoundary(),
'PERIODIC': td.Periodic(),
}
class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket): class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
"""Describes a set of boundary conditions to apply to a simulation domain.
Attributes:
show_definition: Toggle to show/hide default per-axis boundary conditions.
x_pos: Default boundary condition to apply at the boundary of the sim domain's positive x-axis.
x_neg: Default boundary condition to apply at the boundary of the sim domain's negative x-axis.
y_pos: Default boundary condition to apply at the boundary of the sim domain's positive y-axis.
y_neg: Default boundary condition to apply at the boundary of the sim domain's negative y-axis.
z_pos: Default boundary condition to apply at the boundary of the sim domain's positive z-axis.
z_neg: Default boundary condition to apply at the boundary of the sim domain's negative z-axis.
"""
socket_type = ct.SocketType.MaxwellBoundConds socket_type = ct.SocketType.MaxwellBoundConds
bl_label = 'Maxwell Bound Box' bl_label = 'Maxwell Bound Box'
#################### ####################
# - Properties # - Properties
#################### ####################
show_definition: bpy.props.BoolProperty( show_definition: bool = bl_cache.BLField(False, prop_ui=True)
name='Show Bounds Definition',
description='Toggle to show bound faces',
default=False,
update=(lambda self, context: self.on_prop_changed('show_definition', context)),
)
x_pos: bpy.props.EnumProperty( x_pos: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
name='+x Bound Face', x_neg: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
description='+x choice of default boundary face', y_pos: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
items=BOUND_FACE_ITEMS, y_neg: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
default='PML', z_pos: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
update=(lambda self, context: self.on_prop_changed('x_pos', context)), z_neg: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
)
x_neg: bpy.props.EnumProperty(
name='-x Bound Face',
description='-x choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.on_prop_changed('x_neg', context)),
)
y_pos: bpy.props.EnumProperty(
name='+y Bound Face',
description='+y choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.on_prop_changed('y_pos', context)),
)
y_neg: bpy.props.EnumProperty(
name='-y Bound Face',
description='-y choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.on_prop_changed('y_neg', context)),
)
z_pos: bpy.props.EnumProperty(
name='+z Bound Face',
description='+z choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.on_prop_changed('z_pos', context)),
)
z_neg: bpy.props.EnumProperty(
name='-z Bound Face',
description='-z choice of default boundary face',
items=BOUND_FACE_ITEMS,
default='PML',
update=(lambda self, context: self.on_prop_changed('z_neg', context)),
)
#################### ####################
# - UI # - UI
#################### ####################
def draw_label_row(self, row: bpy.types.UILayout, text) -> None: def draw_label_row(self, row: bpy.types.UILayout, text) -> None:
row.label(text=text) row.label(text=text)
row.prop(self, 'show_definition', toggle=True, text='', icon='MOD_LENGTH') row.prop(
self,
self.blfields['show_definition'],
toggle=True,
text='',
icon=ct.Icon.ToggleSocketInfo,
)
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
if not self.show_definition: if self.show_definition:
return for axis in ['x', 'y', 'z']:
row = col.row(align=False)
split = row.split(factor=0.2, align=False)
for axis in ['x', 'y', 'z']: _col = split.column(align=True)
row = col.row(align=False) _col.alignment = 'RIGHT'
split = row.split(factor=0.2, align=False) _col.label(text=axis + ' -')
_col.label(text=' +')
_col = split.column(align=True) _col = split.column(align=True)
_col.alignment = 'RIGHT' _col.prop(self, self.blfields[axis + '_neg'], text='')
_col.label(text=axis + ' -') _col.prop(self, self.blfields[axis + '_pos'], text='')
_col.label(text=' +')
_col = split.column(align=True)
_col.prop(self, axis + '_neg', text='')
_col.prop(self, axis + '_pos', text='')
draw_value_array = draw_value
#################### ####################
# - Computation of Default Value # - Computation of Default Value
#################### ####################
@property @property
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.
Each half-axis has a selection pulled from `ct.BoundCondType`.
Returns:
A usable `tidy3d` boundary specification.
"""
log.debug(
'%s|%s: Computing default value for Boundary Conditions',
self.node.sim_node_name,
self.bl_label,
)
return td.BoundarySpec( return td.BoundarySpec(
x=td.Boundary( x=td.Boundary(
plus=BOUND_MAP[self.x_pos], plus=self.x_pos.tidy3d_boundary_edge,
minus=BOUND_MAP[self.x_neg], minus=self.x_neg.tidy3d_boundary_edge,
), ),
y=td.Boundary( y=td.Boundary(
plus=BOUND_MAP[self.y_pos], plus=self.y_pos.tidy3d_boundary_edge,
minus=BOUND_MAP[self.y_neg], minus=self.y_neg.tidy3d_boundary_edge,
), ),
z=td.Boundary( z=td.Boundary(
plus=BOUND_MAP[self.z_pos], plus=self.z_pos.tidy3d_boundary_edge,
minus=BOUND_MAP[self.z_neg], minus=self.z_neg.tidy3d_boundary_edge,
), ),
) )
@ -128,8 +106,20 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
class MaxwellBoundCondsSocketDef(base.SocketDef): class MaxwellBoundCondsSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.MaxwellBoundConds socket_type: ct.SocketType = ct.SocketType.MaxwellBoundConds
default_x_pos: ct.BoundCondType = ct.BoundCondType.Pml
default_x_neg: ct.BoundCondType = ct.BoundCondType.Pml
default_y_pos: ct.BoundCondType = ct.BoundCondType.Pml
default_y_neg: ct.BoundCondType = ct.BoundCondType.Pml
default_z_pos: ct.BoundCondType = ct.BoundCondType.Pml
default_z_neg: ct.BoundCondType = ct.BoundCondType.Pml
def init(self, bl_socket: MaxwellBoundCondsBLSocket) -> None: def init(self, bl_socket: MaxwellBoundCondsBLSocket) -> None:
pass bl_socket.x_pos = self.default_x_pos
bl_socket.x_neg = self.default_x_neg
bl_socket.y_pos = self.default_y_pos
bl_socket.y_neg = self.default_y_neg
bl_socket.z_pos = self.default_z_pos
bl_socket.z_neg = self.default_z_neg
#################### ####################

View File

@ -1,4 +1,3 @@
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
@ -16,6 +15,8 @@ class MaxwellSourceSocketDef(base.SocketDef):
is_list: bool = False is_list: bool = False
## TODO: capabilities() to require source sockets
def init(self, bl_socket: MaxwellSourceBLSocket) -> None: def init(self, bl_socket: MaxwellSourceBLSocket) -> None:
if self.is_list: if self.is_list:
bl_socket.active_kind = ct.FlowKind.Array bl_socket.active_kind = ct.FlowKind.Array

View File

@ -96,6 +96,23 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
new_task_name: str = bl_cache.BLField('', prop_ui=True) new_task_name: str = bl_cache.BLField('', prop_ui=True)
####################
# - Property Changes
####################
def on_socket_prop_changed(self, prop_name: str) -> None:
if prop_name in [
'api_key',
'existing_folder_id',
'existing_task_id',
'new_task_name',
'should_exist',
]:
self.existing_folder_id = bl_cache.Signal.ResetEnumItems
self.existing_task_id = bl_cache.Signal.ResetEnumItems
####################
# - FlowKinds
####################
@property @property
def capabilities(self) -> ct.CapabilitiesFlow: def capabilities(self) -> ct.CapabilitiesFlow:
return ct.CapabilitiesFlow( return ct.CapabilitiesFlow(
@ -122,7 +139,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return (self.new_task_name, cloud_folder) return (self.new_task_name, cloud_folder)
# No Task Selected: Return None # No Task Selected: Return None
if self.existing_task_id == 'NONE': if self.existing_task_id is None:
return None return None
# Retrieve Cloud Task # Retrieve Cloud Task
@ -135,7 +152,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return cloud_task return cloud_task
return None return ct.FlowSignal.FlowPending
#################### ####################
# - Searchers # - Searchers
@ -158,7 +175,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return [] return []
def search_cloud_tasks(self) -> list[ct.BLEnumElement]: def search_cloud_tasks(self) -> list[ct.BLEnumElement]:
if self.existing_folder_id == 'NONE' or not tdcloud.IS_AUTHENTICATED: if self.existing_folder_id is None or not tdcloud.IS_AUTHENTICATED:
return [] return []
# Get Cloud Folder # Get Cloud Folder
@ -221,10 +238,6 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
def on_prepare_new_task(self): def on_prepare_new_task(self):
self.should_exist = False self.should_exist = False
def on_cloud_updated(self):
self.existing_folder_id = bl_cache.Signal.ResetEnumItems
self.existing_task_id = bl_cache.Signal.ResetEnumItems
#################### ####################
# - UI # - UI
#################### ####################

View File

@ -13,6 +13,7 @@ def prefix_values_with(prefix: str) -> type[enum.Enum]:
Returns: Returns:
A new StrEnum class with altered member values. A new StrEnum class with altered member values.
""" """
## TODO: DO NOT USE FOR ENUMS WITH METHODS
def _decorator(cls: enum.StrEnum): def _decorator(cls: enum.StrEnum):
new_members = { new_members = {

View File

@ -547,6 +547,9 @@ class BLField:
self._str_cb = str_cb self._str_cb = str_cb
self._enum_cb = enum_cb self._enum_cb = enum_cb
## Type Coercion
self._coerce_output_to = None
## Vector/Matrix Identity ## Vector/Matrix Identity
## -> Matrix Shape assists in the workaround for Matrix Display Bug ## -> Matrix Shape assists in the workaround for Matrix Display Bug
self._is_vector = False self._is_vector = False
@ -797,7 +800,11 @@ class BLField:
} }
## StrEnum ## StrEnum
elif inspect.isclass(AttrType) and issubclass(AttrType, enum.StrEnum): elif (
inspect.isclass(AttrType)
and issubclass(AttrType, enum.StrEnum)
and self._enum_cb is None
):
default_value = self._default_value default_value = self._default_value
BLProp = bpy.props.EnumProperty BLProp = bpy.props.EnumProperty
kwargs_prop |= { kwargs_prop |= {
@ -814,9 +821,14 @@ class BLField:
} }
if self._enum_many: if self._enum_many:
kwargs_prop['options'].add('ENUM_FLAG') kwargs_prop['options'].add('ENUM_FLAG')
self._coerce_output_to = AttrType
## Dynamic Enum ## Dynamic Enum
elif AttrType is enum.Enum and self._enum_cb is not None: elif (
AttrType is enum.Enum
or (inspect.isclass(AttrType) and issubclass(AttrType, enum.StrEnum))
and self._enum_cb is not None
):
if self._default_value is not None: if self._default_value is not None:
msg = 'When using dynamic enum, default value must be None' msg = 'When using dynamic enum, default value must be None'
raise ValueError(msg) raise ValueError(msg)
@ -828,6 +840,8 @@ class BLField:
} }
if self._enum_many: if self._enum_many:
kwargs_prop['options'].add('ENUM_FLAG') kwargs_prop['options'].add('ENUM_FLAG')
if AttrType is not enum.Enum:
self._coerce_output_to = AttrType
## BL Reference ## BL Reference
elif AttrType in typ.get_args(ct.BLIDStruct): elif AttrType in typ.get_args(ct.BLIDStruct):
@ -888,6 +902,9 @@ class BLField:
def __get__( def __get__(
self, bl_instance: BLInstance | None, owner: type[BLInstance] self, bl_instance: BLInstance | None, owner: type[BLInstance]
) -> typ.Any: ) -> typ.Any:
if bl_instance is None:
return None
value = self._cached_bl_property.__get__(bl_instance, owner) value = self._cached_bl_property.__get__(bl_instance, owner)
# enum.Enum: Cast Auto-Injected Dynamic Enum 'NONE' -> None # enum.Enum: Cast Auto-Injected Dynamic Enum 'NONE' -> None
@ -913,7 +930,7 @@ class BLField:
## -> Reject modernity. Return to tuple[]. ## -> Reject modernity. Return to tuple[].
if self._is_vector: if self._is_vector:
## -> tuple()ify the np.array to respect tuple[] type annotation. ## -> tuple()ify the np.array to respect tuple[] type annotation.
return tuple(np.array(value)) return tuple(value)
if self._is_matrix: if self._is_matrix:
# Matrix Display Bug: Correctly Read Row-Major Values w/Reshape # Matrix Display Bug: Correctly Read Row-Major Values w/Reshape
@ -921,6 +938,13 @@ class BLField:
map(tuple, np.array(value).flatten().reshape(self._matrix_shape)) map(tuple, np.array(value).flatten().reshape(self._matrix_shape))
) )
# Coerce Output
## -> Mainly useful for getting the "real" StrEnum back.
if self._coerce_output_to is not None and value is not None:
if self._enum_many:
return {self._coerce_output_to(v) for v in value}
return self._coerce_output_to(value)
return value return value
def __set__(self, bl_instance: BLInstance | None, value: typ.Any) -> None: def __set__(self, bl_instance: BLInstance | None, value: typ.Any) -> None:

View File

@ -25,6 +25,10 @@ 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
log = logger.get(__name__)
SympyType = ( SympyType = (
sp.Basic sp.Basic
| sp.Expr | sp.Expr
@ -47,21 +51,42 @@ class MathType(enum.StrEnum):
Real = enum.auto() Real = enum.auto()
Complex = enum.auto() Complex = enum.auto()
@staticmethod
def combine(*mathtypes: list[typ.Self]) -> typ.Self: def combine(*mathtypes: list[typ.Self]) -> typ.Self:
if MathType.Complex in mathtypes: if MathType.Complex in mathtypes:
return MathType.Complex return MathType.Complex
elif MathType.Real in mathtypes: if MathType.Real in mathtypes:
return MathType.Real return MathType.Real
elif MathType.Rational in mathtypes: if MathType.Rational in mathtypes:
return MathType.Rational return MathType.Rational
elif MathType.Integer in mathtypes: if MathType.Integer in mathtypes:
return MathType.Integer return MathType.Integer
elif MathType.Bool in mathtypes: if MathType.Bool in mathtypes:
return MathType.Bool return MathType.Bool
msg = f"Can't combine mathtypes {mathtypes}"
raise ValueError(msg)
def is_compatible(self, other: typ.Self) -> bool:
MT = MathType
return (
other
in {
MT.Bool: [MT.Bool],
MT.Integer: [MT.Integer],
MT.Rational: [MT.Integer, MT.Rational],
MT.Real: [MT.Integer, MT.Rational, MT.Real],
MT.Complex: [MT.Integer, MT.Rational, MT.Real, MT.Complex],
}[self]
)
@staticmethod @staticmethod
def from_expr(sp_obj: SympyType) -> type: def from_expr(sp_obj: SympyType) -> type:
## TODO: Support for sp.Matrix if isinstance(sp_obj, sp.MatrixBase):
return MathType.combine(
*[MathType.from_expr(v) for v in sp.flatten(sp_obj)]
)
if isinstance(sp_obj, sp.logic.boolalg.Boolean): if isinstance(sp_obj, sp.logic.boolalg.Boolean):
return MathType.Bool return MathType.Bool
if sp_obj.is_integer: if sp_obj.is_integer:
@ -172,7 +197,7 @@ class NumberSize1D(enum.StrEnum):
None: NS.Scalar, None: NS.Scalar,
(2,): NS.Vec2, (2,): NS.Vec2,
(3,): NS.Vec3, (3,): NS.Vec3,
(4,): NS.Vec3, (4,): NS.Vec4,
}[shape] }[shape]
@property @property
@ -182,7 +207,7 @@ class NumberSize1D(enum.StrEnum):
NS.Scalar: None, NS.Scalar: None,
NS.Vec2: (2,), NS.Vec2: (2,),
NS.Vec3: (3,), NS.Vec3: (3,),
NS.Vec3: (4,), NS.Vec4: (4,),
}[self] }[self]
@ -702,7 +727,6 @@ def scale_to_unit(sp_obj: SympyType, unit: spu.Quantity) -> Number:
Raises: Raises:
ValueError: If the result of unit-conversion and -stripping still has units, as determined by `uses_units()`. ValueError: If the result of unit-conversion and -stripping still has units, as determined by `uses_units()`.
""" """
## TODO: An LFU cache could do better than an LRU.
unitless_expr = spu.convert_to(sp_obj, unit) / unit unitless_expr = spu.convert_to(sp_obj, unit) / unit
if not uses_units(unitless_expr): if not uses_units(unitless_expr):
return unitless_expr return unitless_expr
@ -739,7 +763,7 @@ def unit_str_to_unit(unit_str: str) -> Unit | None:
if unit_str in _UNIT_STR_MAP: if unit_str in _UNIT_STR_MAP:
return _UNIT_STR_MAP[unit_str] return _UNIT_STR_MAP[unit_str]
msg = 'No valid unit for unit string {unit_str}' msg = f'No valid unit for unit string {unit_str}'
raise ValueError(msg) raise ValueError(msg)
@ -802,7 +826,7 @@ class PhysicalType(enum.StrEnum):
# Global # Global
PT.Time: Dims.time, PT.Time: Dims.time,
PT.Angle: Dims.angle, PT.Angle: Dims.angle,
PT.SolidAngle: Dims.steradian, ## MISSING PT.SolidAngle: spu.steradian.dimension, ## MISSING
PT.Freq: Dims.frequency, PT.Freq: Dims.frequency,
PT.AngFreq: Dims.angle * Dims.frequency, PT.AngFreq: Dims.angle * Dims.frequency,
# Cartesian # Cartesian
@ -836,7 +860,7 @@ class PhysicalType(enum.StrEnum):
PT.HField: Dims.current / Dims.length, PT.HField: Dims.current / Dims.length,
# Luminal # Luminal
PT.LumIntensity: Dims.luminous_intensity, PT.LumIntensity: Dims.luminous_intensity,
PT.LumFlux: Dims.luminous_intensity * Dims.steradian, PT.LumFlux: Dims.luminous_intensity * spu.steradian.dimension,
PT.Illuminance: Dims.luminous_intensity / Dims.length**2, PT.Illuminance: Dims.luminous_intensity / Dims.length**2,
# Optics # Optics
PT.OrdinaryWaveVector: Dims.frequency, PT.OrdinaryWaveVector: Dims.frequency,
@ -1263,12 +1287,23 @@ def convert_to_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyE
return spu.convert_to(sp_obj, _flat_unit_system_units(unit_system)) return spu.convert_to(sp_obj, _flat_unit_system_units(unit_system))
def strip_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr:
"""Strip units occurring in the given unit system from the expression.
Unit stripping is a "dumb" operation: "Substitute any `sympy` object in `unit_system.values()` with `1`".
Obviously, the semantic correctness of this operation depends entirely on _the units adding no semantic meaning to the expression_.
Notes:
You should probably use `scale_to_unit_system()` or `convert_to_unit_system()`.
"""
return sp_obj.subs({unit: 1 for unit in unit_system.values()})
def scale_to_unit_system( def scale_to_unit_system(
sp_obj: SympyExpr, unit_system: UnitSystem, use_jax_array: bool = False sp_obj: SympyExpr, unit_system: UnitSystem, use_jax_array: bool = False
) -> int | float | complex | tuple | jax.Array: ) -> int | float | complex | tuple | jax.Array:
"""Convert an expression to the units of a given unit system, then strip all units of the unit system. """Convert an expression to the units of a given unit system, then strip all units of the unit system.
Unit stripping is "dumb": Substitute any `sympy` object in `unit_system.values()` with `1`.
Afterwards, it is converted to an appropriate Python type. Afterwards, it is converted to an appropriate Python type.
Notes: Notes:
@ -1287,8 +1322,6 @@ def scale_to_unit_system(
If the returned type is array-like, and `use_jax_array` is specified, then (and **only** then) will a `jax.Array` be returned instead of a nested `tuple`. If the returned type is array-like, and `use_jax_array` is specified, then (and **only** then) will a `jax.Array` be returned instead of a nested `tuple`.
""" """
return sympy_to_python( return sympy_to_python(
convert_to_unit_system(sp_obj, unit_system).subs( strip_unit_system(convert_to_unit_system(sp_obj, unit_system), unit_system),
{unit: 1 for unit in unit_system.values()}
),
use_jax_array=use_jax_array, use_jax_array=use_jax_array,
) )

View File

@ -81,6 +81,7 @@ _NaivelyEncodableTypeSet = frozenset(typ.get_args(NaivelyEncodableType))
class TypeID(enum.StrEnum): class TypeID(enum.StrEnum):
Complex: str = '!type=complex' Complex: str = '!type=complex'
SympyType: str = '!type=sympytype' SympyType: str = '!type=sympytype'
SympyExpr: str = '!type=sympyexpr'
SocketDef: str = '!type=socketdef' SocketDef: str = '!type=socketdef'
ManagedObj: str = '!type=managedobj' ManagedObj: str = '!type=managedobj'