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
- [ ] Wave Constant
- Bounds
- [ ] Boundary Conds
- [ ] PML
- [ ] PEC
- [ ] PMC
- [ ] Bloch
- [ ] Absorbing
- [x] Wave Constant
- Sources
- [ ] Temporal Shapes / Continuous Wave Temporal Shape
- [ ] Temporal Shapes / Symbolic Temporal Shape
@ -18,8 +11,8 @@
- [ ] Data File Import
- [ ] DataFit Medium
- Monitors
- [ ] EH Field
- [ ] Power Flux
- [x] EH Field
- [x] Power Flux
- [ ] Permittivity
- [ ] Diffraction
- Structures
@ -49,9 +42,9 @@
- Integration
- [ ] Simulation and Analysis of Maxim's Cavity
- Constants
- [ ] Number Constant
- [ ] Vector Constant
- [ ] Physical Constant
- [x] Number Constant
- [x] Vector Constant
- [x] Physical Constant
- [ ] 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.
- [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.
- [ ] Implement support for additional symbols via `Expr`.
- [x] Math / Filter Math
@ -81,8 +74,6 @@
## Inputs
- [x] Wave Constant
- [ ] Fix the LazyValueRange (again!)
- [ ] Document
- [x] Scene
- [ ] 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.
@ -90,14 +81,14 @@
- [x] Constants / Expr Constant
- See IDEAS.
- [x] Constants / Number Constant
- [ ] Fix non-integer sockets
- [ ] Constants / Vector Constant
- [ ] Constants / Physical Constant
- [x] Constants / Vector Constant
- [x] Constants / Physical Constant
- [x] Constants / Scientific Constant
- [ ] 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.
- [x] Constants / Blender Constant
- [ ] Constants / Blender Constant
- [ ] Fix it!
- [ ] Web / Tidy3D Web Importer
@ -200,13 +191,13 @@
## Bounds
- [x] Boundary Conds
- [ ] Boundary Cond / PML Bound Face
- [ ] Dropdown for "Normal" and "Stable"
- [ ] Boundary Cond / PEC Bound Face
- [ ] Boundary Cond / PMC Bound Face
- [ ] Boundary Cond / Bloch Bound Face
- [ ] Implement "simple" mode aka "periodic" mode in Tidy3D
- [ ] Boundary Cond / Absorbing Bound Face
- [x] Boundary Cond / PML Bound Cond
- [ ] 1D plot visualizing the effect of parameters on a 1D wave function
- [x] Boundary Cond / Bloch Bound Cond
- [x] Implement "simple" mode aka "periodic" mode in Tidy3D
- [ ] 1D plot visualizing the effect of parameters on a 1D wave function
- [x] Boundary Cond / Absorbing Bound Cond
- [ ] 1D plot visualizing the effect of parameters on a 1D wave function
## Monitors
- [x] EH Field Monitor

View File

@ -168,13 +168,13 @@ class GeoNodes(enum.StrEnum):
GN.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH,
GN.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH,
## Monitor
GN.MonitorEHField: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorPowerFlux: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorEpsTensor: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorDiffraction: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorProjCartEHField: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorProjAngEHField: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorProjKSpaceEHField: GN_INTERNAL_STRUCTURES_PATH,
GN.MonitorEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorPowerFlux: GN_INTERNAL_MONITORS_PATH,
GN.MonitorEpsTensor: GN_INTERNAL_MONITORS_PATH,
GN.MonitorDiffraction: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjCartEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjAngEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjKSpaceEHField: GN_INTERNAL_MONITORS_PATH,
## Simulation
GN.SimulationSimDomain: 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:
return bpy.data.node_groups[geonodes]
filename = geonodes
filename = str(geonodes)
filepath = str(geonodes.parent_path / (geonodes + '.blend') / 'NodeTree' / geonodes)
directory = filepath.removesuffix(geonodes)
log.info(

View File

@ -40,6 +40,7 @@ from .flow_signals import FlowSignal
from .icons import Icon
from .mobj_types import ManagedObjType
from .node_types import NodeType
from .sim_types import BoundCondType, SimSpaceAxis
from .socket_colors import SOCKET_COLORS
from .socket_types import SocketType
from .tree_types import TreeType
@ -77,6 +78,8 @@ __all__ = [
'BLSocketInfo',
'BLSocketType',
'NodeType',
'BoundCondType',
'SimSpaceAxis',
'NodeCategory',
'NODE_CAT_LABELS',
'ManagedObjType',

View File

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

View File

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

View File

@ -13,9 +13,12 @@ import sympy as sp
import sympy.physics.units as spu
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .socket_types import SocketType
log = logger.get(__name__)
class FlowKind(enum.StrEnum):
"""Defines a kind of data that can flow between nodes.
@ -57,19 +60,22 @@ class FlowKind(enum.StrEnum):
Info = enum.auto()
@classmethod
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system):
if kind == cls.Value:
return spux.sympy_to_python(
spux.scale_to_unit(
def scale_to_unit_system(
cls,
kind: typ.Self,
value,
unit_system[socket_type],
)
unit_system: spux.UnitSystem,
):
if kind == cls.Value:
return spux.scale_to_unit_system(
value,
unit_system,
)
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:
return value.rescale_to_unit(unit_system[socket_type])
return value.rescale_to_unit_system(unit_system)
msg = 'Tried to scale unknown kind'
raise ValueError(msg)
@ -84,17 +90,28 @@ class CapabilitiesFlow:
active_kind: FlowKind
is_universal: bool = False
# == Constraint
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:
return other.is_universal or (
self.socket_type == other.socket_type
and self.active_kind == other.active_kind
# == Constraint
and all(
name in other.must_match
and self.must_match[name] == other.must_match[name]
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}'
raise ValueError(msg)
def rescale_to_unit_system(self, unit: spu.Quantity) -> typ.Self:
raise NotImplementedError
####################
# - Lazy Value Func
@ -469,14 +489,13 @@ class LazyArrayRangeFlow:
# Get Stop Mathtype
if isinstance(self.stop, spux.SympyType):
stop_mathtype = spux.MathType.from_expr(type(self.stop))
stop_mathtype = spux.MathType.from_expr(self.stop)
else:
stop_mathtype = spux.MathType.from_pytype(type(self.stop))
stop_mathtype = spux.MathType.from_pytype(self.stop)
# Check Equal
if start_mathtype != stop_mathtype:
msg = "Mathtypes of start and stop don't agree. Please fix!"
raise ValueError(msg)
return spux.MathType.combine(start_mathtype, stop_mathtype)
return start_mathtype
@ -525,8 +544,8 @@ class LazyArrayRangeFlow:
"""
if self.unit is not None:
return LazyArrayRangeFlow(
start=spu.scale_to_unit(self.start * self.unit, unit),
stop=spu.scale_to_unit(self.stop * self.unit, unit),
start=spux.scale_to_unit(self.start * self.unit, unit),
stop=spux.scale_to_unit(self.stop * self.unit, unit),
steps=self.steps,
scaling=self.scaling,
unit=unit,
@ -536,6 +555,39 @@ class LazyArrayRangeFlow:
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
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
####################

View File

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

View File

@ -90,10 +90,8 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
BoundConds = enum.auto()
## Bounds / Bound Conds
PMLBoundCond = enum.auto()
PECBoundCond = enum.auto()
PMCBoundCond = enum.auto()
BlochBoundCond = enum.auto()
AbsorbingBoundCond = enum.auto()
AdiabAbsorbBoundCond = enum.auto()
# Monitors
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 (bl_object := bpy.data.objects.get(self.name)) is not None:
bl_object = bpy.data.objects.get(self.name)
if bl_object is None:
bl_object = self.bl_object()
if bl_object.name not in preview_collection().objects:
log.info('Moving "%s" to Preview Collection', bl_object.name)
preview_collection().objects.link(bl_object)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
def hide_preview(self) -> None:
"""Removes the managed Blender object from the preview collection.
If it's already removed, do nothing.
"""
if (bl_object := bpy.data.objects.get(self.name)) is not None:
if bl_object.name in preview_collection().objects:
bl_object = bpy.data.objects.get(self.name)
if bl_object is not None and bl_object.name in preview_collection().objects:
log.info('Removing "%s" from Preview Collection', bl_object.name)
preview_collection().objects.unlink(bl_object)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
def bl_select(self) -> None:
"""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."""
log.info('Unlocking All Nodes in NodeTree "%s"', self.bl_label)
for node in self.nodes:
if node.type in ['REROUTE', 'FRAME']:
continue
node.locked = False
for bl_socket in [*node.inputs, *node.outputs]:
bl_socket.locked = False
@ -229,7 +231,9 @@ class MaxwellSimTree(bpy.types.NodeTree):
@contextlib.contextmanager
def repreview_all(self) -> None:
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.newly_previewed_nodes = {}

View File

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

View File

@ -401,7 +401,7 @@ class MapMathNode(base.MaxwellSimNode):
run_on_init=True,
)
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
@events.on_value_changed(

View File

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

View File

@ -2,8 +2,11 @@ import enum
import typing as typ
import bpy
import jax
import jax.numpy as jnp
import jaxtyping as jtyp
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 extra_sympy_units as spux
@ -192,7 +195,10 @@ class VizNode(base.MaxwellSimNode):
# - Sockets
####################
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 = {
'Preview': sockets.AnySocketDef(),
@ -221,8 +227,12 @@ class VizNode(base.MaxwellSimNode):
## - Mode Searcher
#####################
@property
def data_info(self) -> ct.InfoFlow:
return self._compute_input('Expr', kind=ct.FlowKind.Info)
def data_info(self) -> ct.InfoFlow | None:
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]:
if not ct.FlowSignal.check(self.data_info):
@ -298,7 +308,9 @@ class VizNode(base.MaxwellSimNode):
managed_objs={'plot'},
props={'viz_mode', 'viz_target', 'colormap'},
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,
)
def on_show_plot(

View File

@ -599,22 +599,28 @@ class MaxwellSimNode(bpy.types.Node):
It must be currently active.
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)
if bl_socket is not None:
if bl_socket.instance_id:
return (
ct.FlowKind.scale_to_unit_system(
kind,
bl_socket.compute_data(kind=kind),
bl_socket.socket_type,
unit_system,
)
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:
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)
####################

View File

@ -1,10 +1,10 @@
from . import bound_box, bound_faces
from . import bound_cond_nodes, bound_conds
BL_REGISTER = [
*bound_box.BL_REGISTER,
*bound_faces.BL_REGISTER,
*bound_conds.BL_REGISTER,
*bound_cond_nodes.BL_REGISTER,
]
BL_NODES = {
**bound_box.BL_NODES,
**bound_faces.BL_NODES,
**bound_conds.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
from types import MappingProxyType
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .. import contracts as ct
@ -10,7 +11,6 @@ from .. import contracts as ct
log = logger.get(__name__)
UnitSystemID = str
UnitSystem = dict[ct.SocketType, typ.Any]
####################
@ -70,7 +70,7 @@ def event_decorator(
all_loose_input_sockets: bool = False,
all_loose_output_sockets: bool = False,
# 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_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}),
):
@ -213,7 +213,6 @@ def event_decorator(
kind=kind,
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)),
)
@ -269,9 +268,22 @@ def event_decorator(
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
## If there is a FlowPending, then the method would fail.
## Therefore, propagate FlowPending if found.
return method(
node,
**method_kw_args,

View File

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

View File

@ -44,7 +44,7 @@ class NumberConstantNode(base.MaxwellSimNode):
####################
# - 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.prop(self, self.blfields['mathtype'], text='')
row.prop(self, self.blfields['size'], text='')
@ -56,7 +56,7 @@ class NumberConstantNode(base.MaxwellSimNode):
def on_mathtype_size_changed(self, props) -> None:
"""Change the input/output expression sockets to match the mathtype declared in the node."""
self.inputs['Value'].mathtype = props['mathtype']
self.inputs['Value'].shape = props['mathtype'].shape
self.inputs['Value'].shape = props['size'].shape
####################
# - FlowKind

View File

@ -1,6 +1,6 @@
import enum
import typing as typ
import bpy
import sympy as sp
from blender_maxwell.utils import bl_cache
@ -10,7 +10,7 @@ from .... import contracts, sockets
from ... import base, events
class PhysicalConstantNode(base.MaxwellSimTreeNode):
class PhysicalConstantNode(base.MaxwellSimNode):
"""A number of configurable unit dimension, ex. time, length, etc. .
Attributes:
@ -36,12 +36,12 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
prop_ui=True,
)
mathtype: enum.Enum = bl_cache.BLField(
mathtype: spux.MathType = bl_cache.BLField(
enum_cb=lambda self, _: self.search_mathtypes(),
prop_ui=True,
)
size: enum.Enum = bl_cache.BLField(
size: spux.NumberSize1D = bl_cache.BLField(
enum_cb=lambda self, _: self.search_sizes(),
prop_ui=True,
)
@ -62,16 +62,25 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
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.on_value_changed(
prop_name={'physical_type', 'mathtype', 'size'},
run_on_init=True,
props={'physical_type', 'mathtype', 'size'},
)
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."""
shape = spux.NumberSize1D(props['size']).shape
shape = props['size'].shape
# Set Input Socket Physical Type
if self.inputs['Value'].physical_type != props['physical_type']:
@ -90,9 +99,9 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode):
####################
# - Callbacks
####################
@events.computes_output_socket('value')
def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr:
return self.compute_input('value')
@events.computes_output_socket('Value', input_sockets={'Value'})
def compute_value(self, input_sockets) -> sp.Expr:
return input_sockets['Value']
####################

View File

@ -2,7 +2,7 @@ import typing as typ
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 sockets
@ -20,63 +20,43 @@ class ScientificConstantNode(base.MaxwellSimNode):
####################
# - Properties
####################
sci_constant: bpy.props.StringProperty(
name='Sci Constant',
description='The name of a scientific constant',
default='',
search=lambda self, _, edit_text: self.search_sci_constants(edit_text),
update=lambda self, context: self.on_update_sci_constant(context),
sci_constant: str = bl_cache.BLField(
'',
prop_ui=True,
str_cb=lambda self, _, edit_text: self.search_sci_constants(edit_text),
)
cache__units: bpy.props.StringProperty(default='')
cache__uncertainty: bpy.props.StringProperty(default='')
def search_sci_constants(
self,
edit_text: str,
):
return [
name
for name in constants.SCI_CONSTANTS
for name in sci_constants.SCI_CONSTANTS
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
####################
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:
if self.sci_constant:
col.label(text=f'Units: {self.cache__units}')
col.label(text=f'Uncertainty: {self.cache__uncertainty}')
col.label(text=f'Ref: {constants.SCI_CONSTANTS_REF[0]}')
col.label(
text=f'Units: {sci_constants.SCI_CONSTANTS_INFO[self.sci_constant]["units"]}'
)
col.label(
text=f'Uncertainty: {sci_constants.SCI_CONSTANTS_INFO[self.sci_constant]["uncertainty"]}'
)
####################
# - Callbacks
# - Output
####################
@events.computes_output_socket('Value', props={'sci_constant'})
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 = {
'Wavelength': {
'WL': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Value,
physical_type=spux.PhysicalType.Length,
# Defaults
default_unit=spu.nm,
@ -58,18 +59,18 @@ class WaveConstantNode(base.MaxwellSimNode):
output_sockets: typ.ClassVar = {
'WL': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Value,
unit_dimension=spux.Dims.length,
physical_type=spux.PhysicalType.Length,
),
'Freq': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Value,
unit_dimension=spux.Dims.frequency,
physical_type=spux.PhysicalType.Freq,
),
}
####################
# - Properties
####################
use_range: bool = bl_cache.BLField(False)
use_range: bool = bl_cache.BLField(False, prop_ui=True)
####################
# - UI
@ -80,14 +81,14 @@ class WaveConstantNode(base.MaxwellSimNode):
Parameters:
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.on_value_changed(
prop_name={'active_socket_set', 'use_range'},
props='use_range',
props={'use_range'},
run_on_init=True,
)
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:
"""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 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:
"""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 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:
"""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['Freq'].rescale_bounds(
lambda bound: sci_constants.vac_speed_of_light / bound, reverse=True
freq = input_sockets['Freq']
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(
@ -177,11 +189,20 @@ class WaveConstantNode(base.MaxwellSimNode):
)
def compute_freq_range(self, input_sockets: dict) -> sp.Expr:
"""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['WL'].rescale_bounds(
lambda bound: sci_constants.vac_speed_of_light / bound, reverse=True
wl = input_sockets['WL']
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
# Try Loading Simulation Data
node.sim_data = bl_cache.Signal.InvalidateCache
#node.sim_data = bl_cache.Signal.InvalidateCache
sim_data = node.sim_data
if sim_data is None:
self.report(
@ -70,18 +70,26 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
should_exist=True,
),
}
output_sockets: typ.ClassVar = {
'Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
}
####################
# - Properties
####################
sim_data_loaded: bool = bl_cache.BLField(False)
@bl_cache.cached_bl_property()
####################
# - Computed
####################
@property
def sim_data(self) -> td.SimulationData | None:
cloud_task = self._compute_input(
'Cloud Task', kind=ct.FlowKind.Value, optional=True
)
has_cloud_task = not ct.FlowSignal.check(cloud_task)
if (
# Check Flow
not ct.FlowSignal.check(cloud_task)
# Check Task
has_cloud_task
and cloud_task is not None
and isinstance(cloud_task, tdcloud.CloudTask)
and cloud_task.status == 'success'
@ -97,7 +105,7 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
####################
# - UI
####################
def draw_operators(self, context, layout):
def draw_operators(self, _: bpy.types.Context, layout: bpy.types.UILayout):
if self.sim_data_loaded:
layout.operator(ct.OperatorType.NodeLoadCloudSim, text='Reload Sim')
else:
@ -106,11 +114,6 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
####################
# - 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(
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(
shape=(3,),
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
),
'Spatial Subdivs': sockets.ExprSocketDef(
shape=(3,),
@ -124,11 +125,30 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
# - Preview
####################
@events.on_value_changed(
socket_name={'Center', 'Size'},
# Trigger
prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
props={'preview_active'},
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'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
@ -136,7 +156,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
)
def on_inputs_changed(
self,
props: dict,
managed_objs: dict,
input_sockets: 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(
shape=(3,),
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
),
'Samples/Space': sockets.ExprSocketDef(
shape=(3,),
@ -123,11 +124,29 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
# - Preview - Changes to Input Sockets
####################
@events.on_value_changed(
socket_name={'Center', 'Size'},
# Trigger
prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
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'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
@ -135,7 +154,6 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
)
def on_inputs_changed(
self,
props: dict,
managed_objs: dict,
input_sockets: 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 = [
*viewer.BL_REGISTER,
*file_exporters.BL_REGISTER,
*web_exporters.BL_REGISTER,
#*file_exporters.BL_REGISTER,
#*web_exporters.BL_REGISTER,
]
BL_NODES = {
**viewer.BL_NODES,
**file_exporters.BL_NODES,
**web_exporters.BL_NODES,
#**file_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(
socket_name={'Center', 'Size'},
prop_name='preview_active',
@ -91,6 +72,28 @@ class SimDomainNode(base.MaxwellSimNode):
if props['preview_active']:
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

View File

@ -217,7 +217,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Called by `self.on_prop_changed()` when `self.active_kind` was changed.
"""
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 '')
## 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)
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
####################

View File

@ -2,6 +2,7 @@ import enum
import typing as typ
import bpy
import pydantic as pyd
import sympy as sp
from blender_maxwell.utils import bl_cache, logger
@ -63,6 +64,7 @@ class InfoDisplayCol(enum.StrEnum):
class ExprBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Expr
bl_label = 'Expr'
use_info_draw = True
####################
# - Properties
@ -70,7 +72,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
shape: tuple[int, ...] | None = bl_cache.BLField(None)
mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real, prop_ui=True)
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(
None, enum_cb=lambda self, _: self.search_units(), prop_ui=True
@ -102,7 +104,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
)
# UI: LazyArrayRange
steps: int = bl_cache.BLField(2, abs_min=2)
steps: int = bl_cache.BLField(2, abs_min=2, prop_ui=True)
## Expression
raw_min_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
####################
@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
def raw_value_sp(self) -> spux.SympyExpr:
return self._parse_expr_str(self.raw_value_spstr)
@ -140,7 +151,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
####################
# - 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:
return [
(sp.sstr(unit), spux.sp_to_str(unit), sp.sstr(unit), '', i)
@ -163,33 +174,38 @@ class ExprBLSocket(base.MaxwellSimSocket):
return None
@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.
Notes:
To set a new unit, **and** convert the `raw_*` UI properties to the new unit, use `self.convert_unit()` instead.
"""
if self.physical_type is not None:
if unit in self.physical_type.valid_units:
self.active_unit = sp.sstr(unit)
else:
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:
if self.active_kind == ct.FlowKind.Value:
current_value = self.value
self.unit = unit_to
self.value = current_value
elif self.active_kind == ct.FlowKind.LazyArrayRange:
current_lazy_array_range = self.lazy_array_range
self.unit = unit_to
self.unit = bl_cache.Signal.InvalidateCache
self.value = current_value
self.lazy_array_range = current_lazy_array_range
####################
# - Property Callback
####################
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))
####################
@ -200,23 +216,23 @@ class ExprBLSocket(base.MaxwellSimSocket):
) -> tuple[spux.MathType, tuple[int, ...] | None, spux.UnitDimension]:
# Parse MathType
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}'
raise ValueError(msg)
# Parse Symbols
if expr.free_symbols:
if self.mathtype is not None:
msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with free symbols {expr.free_symbols}'
raise ValueError(msg)
if not expr.free_symbols.issubset(self.symbols):
if expr.free_symbols and 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
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})'
raise ValueError(msg)
@ -238,7 +254,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
# Try Parsing and Returning the Expression
try:
self._parse_expr_info(expr)
except ValueError(expr) as ex:
except ValueError:
log.exception(
'Couldn\'t parse expression "%s" in Expr socket.',
expr_spstr,
@ -270,6 +286,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
expr = self.raw_value_sp
if expr is None:
return ct.FlowSignal.FlowPending
return expr
MT_Z = spux.MathType.Integer
MT_Q = spux.MathType.Rational
@ -312,7 +329,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
Notes:
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,)]:
self.raw_value_spstr = sp.sstr(expr)
@ -321,32 +338,33 @@ class ExprBLSocket(base.MaxwellSimSocket):
MT_Q = spux.MathType.Rational
MT_R = spux.MathType.Real
MT_C = spux.MathType.Complex
if shape is None:
if mathtype == MT_Z:
if self.shape is None:
if self.mathtype == MT_Z:
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)
elif mathtype == MT_R:
elif self.mathtype == MT_R:
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)
elif shape == (2,):
if mathtype == MT_Z:
elif self.shape == (2,):
if self.mathtype == MT_Z:
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)
elif mathtype == MT_R:
elif self.mathtype == MT_R:
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)
elif shape == (3,):
if mathtype == MT_Z:
elif self.shape == (3,):
log.critical(expr)
if self.mathtype == MT_Z:
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)
elif mathtype == MT_R:
elif self.mathtype == MT_R:
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)
####################
@ -404,7 +422,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
Called to compute the internal `FlowKind.LazyArrayRange` of this socket.
"""
self.steps = value.steps
self.unit = value.unit
if self.symbols:
self.raw_min_spstr = sp.sstr(value.start)
@ -416,21 +433,26 @@ class ExprBLSocket(base.MaxwellSimSocket):
MT_R = spux.MathType.Real
MT_C = spux.MathType.Complex
unit = value.unit if value.unit is not None else 1
if value.mathtype == MT_Z:
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:
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:
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:
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
if self.symbols or self.shape not in [None, (2,), (3,)]:
return ct.LazyValueFuncFlow(
func=sp.lambdify(self.symbols, self.value, 'jax'),
func_args=[spux.MathType.from_expr(sym) for sym in self.symbols],
func=sp.lambdify(self.sorted_symbols, self.value, 'jax'),
func_args=[spux.MathType.from_expr(sym) for sym in self.sorted_symbols],
supports_jax=True,
)
@ -482,8 +504,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
unit=self.unit,
)
msg = "Expr socket can't produce array from expression with free symbols"
raise ValueError(msg)
return ct.FlowSignal.NoFlow
####################
# - FlowKind: Info
@ -496,6 +517,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
output_mathtype=self.mathtype,
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
@ -520,10 +542,11 @@ class ExprBLSocket(base.MaxwellSimSocket):
_row.label(text=text)
_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:
# Property Interface
if self.symbols:
col.prop(self, self.blfields['raw_value_spstr'], text='')
@ -575,6 +598,27 @@ class ExprBLSocket(base.MaxwellSimSocket):
for sym in self.symbols:
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:
info = self.compute_data(kind=ct.FlowKind.Info)
has_dims = not ct.FlowSignal.check(info) and info.dim_names
@ -630,7 +674,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
_row.label(text=text)
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()
box = row.box()
grid = box.grid_flow(
@ -696,19 +740,91 @@ class ExprSocketDef(base.SocketDef):
default_unit: spux.Unit | None = None
# 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
default_min: spux.SympyExpr = sp.S(0)
default_max: spux.SympyExpr = sp.S(1)
default_min: spux.SympyExpr = sp.RealNumber(0)
default_max: spux.SympyExpr = sp.RealNumber(1)
default_steps: int = 2
## TODO: Configure lin/log/... scaling (w/enumprop in UI)
## TODO: Buncha validation :)
# UI
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:
bl_socket.active_kind = self.active_kind
@ -718,11 +834,11 @@ class ExprSocketDef(base.SocketDef):
bl_socket.physical_type = self.physical_type
bl_socket.symbols = self.symbols
# Socket Units
if self.default_unit is not None:
# Socket Units & FlowKind.Value
if self.physical_type is not None:
bl_socket.unit = self.default_unit
# FlowKind: Value
bl_socket.value = self.default_value * self.default_unit
else:
bl_socket.value = self.default_value
# FlowKind: LazyArrayRange

View File

@ -3,51 +3,64 @@ 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 base
log = logger.get(__name__)
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
bl_label = 'Maxwell Bound Face'
bl_label = 'Maxwell Bound Cond'
####################
# - Properties
####################
default_choice: bpy.props.EnumProperty(
name='Bound Face',
description='A choice of default boundary face',
items=[
('PML', 'PML', 'Perfectly matched layer'),
('PEC', 'PEC', 'Perfect electrical conductor'),
('PMC', 'PMC', 'Perfect magnetic conductor'),
('PERIODIC', 'Periodic', 'Infinitely periodic layer'),
],
default='PML',
update=(lambda self, context: self.on_prop_changed('default_choice', context)),
default: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
# Capabilities
## Allow a boundary condition compatible with any of the following axes.
allow_axes: set[ct.SimSpaceAxis] = bl_cache.BLField(
{ct.SimSpaceAxis.X, ct.SimSpaceAxis.Y, ct.SimSpaceAxis.Z},
)
## Present a boundary condition compatible with any of the following axes.
present_axes: set[ct.SimSpaceAxis] = bl_cache.BLField(
{ct.SimSpaceAxis.X, ct.SimSpaceAxis.Y, ct.SimSpaceAxis.Z},
)
####################
# - UI
####################
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
def value(self) -> td.BoundarySpec:
return {
'PML': td.PML(num_layers=12),
'PEC': td.PECBoundary(),
'PMC': td.PMCBoundary(),
'PERIODIC': td.Periodic(),
}[self.default_choice]
def capabilities(self) -> ct.CapabilitiesFlow:
return ct.CapabilitiesFlow(
socket_type=self.socket_type,
active_kind=self.active_kind,
allow_any=self.allow_axes,
present_any=self.present_axes,
)
@property
def value(self) -> td.BoundaryEdge:
return self.default.tidy3d_boundary_edge
@value.setter
def value(self, value: typ.Literal['PML', 'PEC', 'PMC', 'PERIODIC']) -> None:
self.default_choice = value
def value(self, value: ct.BoundCondType) -> None:
self.default = value
####################
@ -56,10 +69,23 @@ class MaxwellBoundCondBLSocket(base.MaxwellSimSocket):
class MaxwellBoundCondSocketDef(base.SocketDef):
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:
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,91 +1,59 @@
"""Implements the `MaxwellBoundCondsBLSocket` socket."""
import bpy
import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
from ... import contracts as ct
from .. import base
BOUND_FACE_ITEMS = [
('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(),
}
log = logger.get(__name__)
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
bl_label = 'Maxwell Bound Box'
####################
# - Properties
####################
show_definition: bpy.props.BoolProperty(
name='Show Bounds Definition',
description='Toggle to show bound faces',
default=False,
update=(lambda self, context: self.on_prop_changed('show_definition', context)),
)
show_definition: bool = bl_cache.BLField(False, prop_ui=True)
x_pos: 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_pos', context)),
)
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)),
)
x_pos: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
x_neg: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
y_pos: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
y_neg: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
z_pos: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
z_neg: ct.BoundCondType = bl_cache.BLField(ct.BoundCondType.Pml, prop_ui=True)
####################
# - UI
####################
def draw_label_row(self, row: bpy.types.UILayout, text) -> None:
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:
if not self.show_definition:
return
if self.show_definition:
for axis in ['x', 'y', 'z']:
row = col.row(align=False)
split = row.split(factor=0.2, align=False)
@ -96,28 +64,38 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
_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
_col.prop(self, self.blfields[axis + '_neg'], text='')
_col.prop(self, self.blfields[axis + '_pos'], text='')
####################
# - Computation of Default Value
####################
@property
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(
x=td.Boundary(
plus=BOUND_MAP[self.x_pos],
minus=BOUND_MAP[self.x_neg],
plus=self.x_pos.tidy3d_boundary_edge,
minus=self.x_neg.tidy3d_boundary_edge,
),
y=td.Boundary(
plus=BOUND_MAP[self.y_pos],
minus=BOUND_MAP[self.y_neg],
plus=self.y_pos.tidy3d_boundary_edge,
minus=self.y_neg.tidy3d_boundary_edge,
),
z=td.Boundary(
plus=BOUND_MAP[self.z_pos],
minus=BOUND_MAP[self.z_neg],
plus=self.z_pos.tidy3d_boundary_edge,
minus=self.z_neg.tidy3d_boundary_edge,
),
)
@ -128,8 +106,20 @@ class MaxwellBoundCondsBLSocket(base.MaxwellSimSocket):
class MaxwellBoundCondsSocketDef(base.SocketDef):
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:
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 base
@ -16,6 +15,8 @@ class MaxwellSourceSocketDef(base.SocketDef):
is_list: bool = False
## TODO: capabilities() to require source sockets
def init(self, bl_socket: MaxwellSourceBLSocket) -> None:
if self.is_list:
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)
####################
# - 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
def capabilities(self) -> ct.CapabilitiesFlow:
return ct.CapabilitiesFlow(
@ -122,7 +139,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return (self.new_task_name, cloud_folder)
# No Task Selected: Return None
if self.existing_task_id == 'NONE':
if self.existing_task_id is None:
return None
# Retrieve Cloud Task
@ -135,7 +152,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return cloud_task
return None
return ct.FlowSignal.FlowPending
####################
# - Searchers
@ -158,7 +175,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
return []
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 []
# Get Cloud Folder
@ -221,10 +238,6 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
def on_prepare_new_task(self):
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
####################

View File

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

View File

@ -547,6 +547,9 @@ class BLField:
self._str_cb = str_cb
self._enum_cb = enum_cb
## Type Coercion
self._coerce_output_to = None
## Vector/Matrix Identity
## -> Matrix Shape assists in the workaround for Matrix Display Bug
self._is_vector = False
@ -797,7 +800,11 @@ class BLField:
}
## 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
BLProp = bpy.props.EnumProperty
kwargs_prop |= {
@ -814,9 +821,14 @@ class BLField:
}
if self._enum_many:
kwargs_prop['options'].add('ENUM_FLAG')
self._coerce_output_to = AttrType
## 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:
msg = 'When using dynamic enum, default value must be None'
raise ValueError(msg)
@ -828,6 +840,8 @@ class BLField:
}
if self._enum_many:
kwargs_prop['options'].add('ENUM_FLAG')
if AttrType is not enum.Enum:
self._coerce_output_to = AttrType
## BL Reference
elif AttrType in typ.get_args(ct.BLIDStruct):
@ -888,6 +902,9 @@ class BLField:
def __get__(
self, bl_instance: BLInstance | None, owner: type[BLInstance]
) -> typ.Any:
if bl_instance is None:
return None
value = self._cached_bl_property.__get__(bl_instance, owner)
# enum.Enum: Cast Auto-Injected Dynamic Enum 'NONE' -> None
@ -913,7 +930,7 @@ class BLField:
## -> Reject modernity. Return to tuple[].
if self._is_vector:
## -> tuple()ify the np.array to respect tuple[] type annotation.
return tuple(np.array(value))
return tuple(value)
if self._is_matrix:
# 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))
)
# 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
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 . import logger
log = logger.get(__name__)
SympyType = (
sp.Basic
| sp.Expr
@ -47,21 +51,42 @@ class MathType(enum.StrEnum):
Real = enum.auto()
Complex = enum.auto()
@staticmethod
def combine(*mathtypes: list[typ.Self]) -> typ.Self:
if MathType.Complex in mathtypes:
return MathType.Complex
elif MathType.Real in mathtypes:
if MathType.Real in mathtypes:
return MathType.Real
elif MathType.Rational in mathtypes:
if MathType.Rational in mathtypes:
return MathType.Rational
elif MathType.Integer in mathtypes:
if MathType.Integer in mathtypes:
return MathType.Integer
elif MathType.Bool in mathtypes:
if MathType.Bool in mathtypes:
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
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):
return MathType.Bool
if sp_obj.is_integer:
@ -172,7 +197,7 @@ class NumberSize1D(enum.StrEnum):
None: NS.Scalar,
(2,): NS.Vec2,
(3,): NS.Vec3,
(4,): NS.Vec3,
(4,): NS.Vec4,
}[shape]
@property
@ -182,7 +207,7 @@ class NumberSize1D(enum.StrEnum):
NS.Scalar: None,
NS.Vec2: (2,),
NS.Vec3: (3,),
NS.Vec3: (4,),
NS.Vec4: (4,),
}[self]
@ -702,7 +727,6 @@ def scale_to_unit(sp_obj: SympyType, unit: spu.Quantity) -> Number:
Raises:
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
if not uses_units(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:
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)
@ -802,7 +826,7 @@ class PhysicalType(enum.StrEnum):
# Global
PT.Time: Dims.time,
PT.Angle: Dims.angle,
PT.SolidAngle: Dims.steradian, ## MISSING
PT.SolidAngle: spu.steradian.dimension, ## MISSING
PT.Freq: Dims.frequency,
PT.AngFreq: Dims.angle * Dims.frequency,
# Cartesian
@ -836,7 +860,7 @@ class PhysicalType(enum.StrEnum):
PT.HField: Dims.current / Dims.length,
# Luminal
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,
# Optics
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))
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(
sp_obj: SympyExpr, unit_system: UnitSystem, use_jax_array: bool = False
) -> 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.
Unit stripping is "dumb": Substitute any `sympy` object in `unit_system.values()` with `1`.
Afterwards, it is converted to an appropriate Python type.
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`.
"""
return sympy_to_python(
convert_to_unit_system(sp_obj, unit_system).subs(
{unit: 1 for unit in unit_system.values()}
),
strip_unit_system(convert_to_unit_system(sp_obj, unit_system), unit_system),
use_jax_array=use_jax_array,
)

View File

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