Compare commits

...

2 Commits

Author SHA1 Message Date
Sofus Albert Høgsbro Rose 22daa6c603
feat: Feature-parity with past.
We've added enough nodes to run simulations, and there is now only an
"air gap" remaining between the math nodes and the sim design nodes.

Of particular importance right now are:
- Finalizing the exporter w/Sim Data output.
- Finishing the plane wave, as well as other key source nodes (TFSF,
  Gaussian Beam, Plane Wave stand out in particular).
- Reduce node, as we need to be able to reconstruct total flux
  through a field monitor, as well as generally do statistics.
- Transform node, particularly w/pure-InfoFlow reindexing (we need to be
  able to reindex frequency -> vacwl), fourier transform (we need to be
  able to cast a time-domain field recording to frequency domain).
- Finish debugging the basic structures, in particular the Cylinder
  primitive, but also the generic GeoNodes node, so we can build the
  membrane properly.
- Ah yes, also, the grid definition. To satisfy the naysayers!
2024-05-03 00:19:02 +02:00
Sofus Albert Høgsbro Rose 9df0d20c68
feat: Finished Gaussian Pulse node.
Also fixed several bugs along the way.
Full speed aheaad on the sources!
2024-05-02 20:59:30 +02:00
43 changed files with 1202 additions and 856 deletions

23
TODO.md
View File

@ -1,7 +1,7 @@
# Working TODO # Working TODO
- [x] Wave Constant - [x] Wave Constant
- Sources - Sources
- [ ] Temporal Shapes / Continuous Wave Temporal Shape - [x] Temporal Shapes / Continuous Wave Temporal Shape
- [ ] Temporal Shapes / Symbolic Temporal Shape - [ ] Temporal Shapes / Symbolic Temporal Shape
- [ ] Plane Wave Source - [ ] Plane Wave Source
- [ ] TFSF Source - [ ] TFSF Source
@ -57,6 +57,11 @@
# VALIDATE
- [ ] Does the imaginary part of a complex phasor scale with the real part? Ex. when doing `V/m -> V/um` conversion, does the phase also scale by 1 million?
# Nodes # Nodes
## Analysis ## Analysis
@ -144,9 +149,10 @@
## Sources ## Sources
- [x] Temporal Shapes / Gaussian Pulse Temporal Shape - [x] Temporal Shapes / Gaussian Pulse Temporal Shape
- [x] Temporal Shapes / Continuous Wave Temporal Shape - [x] Temporal Shapes / Continuous Wave Temporal Shape
- [ ] Temporal Shapes / Symbolic Temporal Shape - [ ] Merge Gaussian Pulse and Continuous Wave w/a socket set thing, since the I/O is effectively identical.
- [ ] Specify a Sympy function to generate appropriate array based on - [ ] Temporal Shapes / Expr Temporal Shape
- [ ] Temporal Shapes / Data Temporal Shape - [ ] Specify a Sympy function / data to generate envelope data.
- [ ] Merge with the above.
- [x] Point Dipole Source - [x] Point Dipole Source
- [ ] Use a viz mesh, not empty (empty doesn't play well with alpha hashing). - [ ] Use a viz mesh, not empty (empty doesn't play well with alpha hashing).
@ -585,4 +591,11 @@ Unreported:
- [ ] Shader visualizations approximated from medium `nk` into a shader node graph, aka. a generic BSDF. - [ ] Shader visualizations approximated from medium `nk` into a shader node graph, aka. a generic BSDF.
- [ ] Web importer that gets material data from refractiveindex.info.
- [ ] Easy conversion of lazyarrayrange to mu/sigma frequency for easy computation of pulse fits from data.
- [ ] IDEA: Hand-craft a faster `spu.convert_to`. <https://github.com/sympy/sympy/blob/a44299273eeb4838beaee9af3b688f2f44d7702f/sympy/physics/units/util.py#L51-L129>
- [ ] We should probably communicate with the `sympy` upstream about our deep usage of unit systems. They might be interested in the various workarounds :)

Binary file not shown.

View File

@ -24,3 +24,10 @@ class OperatorType(enum.StrEnum):
# Node: Tidy3DWebImporter # Node: Tidy3DWebImporter
NodeLoadCloudSim = enum.auto() NodeLoadCloudSim = enum.auto()
# Node: Tidy3DWebExporter
NodeUploadSimulation = enum.auto()
NodeRunSimulation = enum.auto()
NodeReloadTrackedTask = enum.auto()
NodeEstCostTrackedTask = enum.auto()
ReleaseTrackedTask = enum.auto()

View File

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

View File

@ -5,11 +5,13 @@ import typing as typ
import bpy import bpy
import sympy as sp import sympy as sp
from blender_maxwell.utils import blender_type_enum
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .socket_types import SocketType from .socket_types import SocketType
log = logger.get(__name__)
BL_SOCKET_DESCR_ANNOT_STRING = ':: ' BL_SOCKET_DESCR_ANNOT_STRING = ':: '
@ -190,13 +192,14 @@ class BLSocketType(enum.StrEnum):
return { return {
BLST.Color: S.Vec4, BLST.Color: S.Vec4,
BLST.Rotation: S.Vec3, BLST.Rotation: S.Vec3,
BLST.Vector: S.Vec3,
BLST.VectorAcceleration: S.Vec3, BLST.VectorAcceleration: S.Vec3,
BLST.VectorDirection: S.Vec3, BLST.VectorDirection: S.Vec3,
BLST.VectorEuler: S.Vec3, BLST.VectorEuler: S.Vec3,
BLST.VectorTranslation: S.Vec3, BLST.VectorTranslation: S.Vec3,
BLST.VectorVelocity: S.Vec3, BLST.VectorVelocity: S.Vec3,
BLST.VectorXYZ: S.Vec3, BLST.VectorXYZ: S.Vec3,
}.get(self, {S.Scalar}) }.get(self, S.Scalar)
@property @property
def unambiguous_physical_type(self) -> spux.PhysicalType | None: def unambiguous_physical_type(self) -> spux.PhysicalType | None:

View File

@ -111,7 +111,10 @@ class CapabilitiesFlow:
for name in self.must_match for name in self.must_match
) )
# ∀b (b ∈ A) Constraint # ∀b (b ∈ A) Constraint
and self.present_any.issubset(other.allow_any) and (
self.present_any & other.allow_any
or (not self.present_any and not self.allow_any)
)
) )
@ -485,13 +488,13 @@ class LazyArrayRangeFlow:
if isinstance(self.start, spux.SympyType): if isinstance(self.start, spux.SympyType):
start_mathtype = spux.MathType.from_expr(self.start) start_mathtype = spux.MathType.from_expr(self.start)
else: else:
start_mathtype = spux.MathType.from_pytype(self.start) start_mathtype = spux.MathType.from_pytype(type(self.start))
# Get Stop Mathtype # Get Stop Mathtype
if isinstance(self.stop, spux.SympyType): if isinstance(self.stop, spux.SympyType):
stop_mathtype = spux.MathType.from_expr(self.stop) stop_mathtype = spux.MathType.from_expr(self.stop)
else: else:
stop_mathtype = spux.MathType.from_pytype(self.stop) stop_mathtype = spux.MathType.from_pytype(type(self.stop))
# Check Equal # Check Equal
if start_mathtype != stop_mathtype: if start_mathtype != stop_mathtype:
@ -739,6 +742,10 @@ class LazyArrayRangeFlow:
msg = f'Invalid kind: {kind}' msg = f'Invalid kind: {kind}'
raise TypeError(msg) raise TypeError(msg)
@functools.cached_property
def realize_array(self) -> ArrayFlow:
return self.realize()
#################### ####################
# - Params # - Params
@ -748,21 +755,52 @@ class ParamsFlow:
func_args: list[spux.SympyExpr] = dataclasses.field(default_factory=list) func_args: list[spux.SympyExpr] = dataclasses.field(default_factory=list)
func_kwargs: dict[str, spux.SympyExpr] = dataclasses.field(default_factory=dict) func_kwargs: dict[str, spux.SympyExpr] = dataclasses.field(default_factory=dict)
symbols: frozenset[spux.Symbol] = frozenset()
@functools.cached_property
def sorted_symbols(self) -> list[sp.Symbol]:
"""Retrieves all symbols by concatenating int, real, and complex symbols, and sorting them by name.
Returns:
All symbols valid for use in the expression.
"""
return sorted(self.symbols, key=lambda sym: sym.name)
#################### ####################
# - Scaled Func Args # - Scaled Func Args
#################### ####################
def scaled_func_args(self, unit_system: spux.UnitSystem): def scaled_func_args(
self,
unit_system: spux.UnitSystem,
symbol_values: dict[spux.Symbol, spux.SympyExpr] = MappingProxyType({}),
):
"""Return the function arguments, scaled to the unit system, stripped of units, and cast to jax-compatible arguments.""" """Return the function arguments, scaled to the unit system, stripped of units, and cast to jax-compatible arguments."""
if not all(sym in self.symbols for sym in symbol_values):
msg = f"Symbols in {symbol_values} don't perfectly match the ParamsFlow symbols {self.symbols}"
raise ValueError(msg)
return [ return [
spux.convert_to_unit_system(func_arg, unit_system, use_jax_array=True) spux.scale_to_unit_system(arg, unit_system, use_jax_array=True)
for func_arg in self.func_args if arg not in symbol_values
else symbol_values[arg]
for arg in self.func_args
] ]
def scaled_func_kwargs(self, unit_system: spux.UnitSystem): def scaled_func_kwargs(
self,
unit_system: spux.UnitSystem,
symbol_values: dict[spux.Symbol, spux.SympyExpr] = MappingProxyType({}),
):
"""Return the function arguments, scaled to the unit system, stripped of units, and cast to jax-compatible arguments.""" """Return the function arguments, scaled to the unit system, stripped of units, and cast to jax-compatible arguments."""
if not all(sym in self.symbols for sym in symbol_values):
msg = f"Symbols in {symbol_values} don't perfectly match the ParamsFlow symbols {self.symbols}"
raise ValueError(msg)
return { return {
arg_name: spux.convert_to_unit_system(arg, unit_system, use_jax_array=True) arg_name: spux.convert_to_unit_system(arg, unit_system, use_jax_array=True)
for arg_name, arg in self.func_args if arg not in symbol_values
else symbol_values[arg]
for arg_name, arg in self.func_kwargs.items()
} }
#################### ####################
@ -780,16 +818,19 @@ class ParamsFlow:
return ParamsFlow( return ParamsFlow(
func_args=self.func_args + other.func_args, func_args=self.func_args + other.func_args,
func_kwargs=self.func_kwargs | other.func_kwargs, func_kwargs=self.func_kwargs | other.func_kwargs,
symbols=self.symbols | other.symbols,
) )
def compose_within( def compose_within(
self, self,
enclosing_func_args: list[spux.SympyExpr] = (), enclosing_func_args: list[spux.SympyExpr] = (),
enclosing_func_kwargs: dict[str, spux.SympyExpr] = MappingProxyType({}), enclosing_func_kwargs: dict[str, spux.SympyExpr] = MappingProxyType({}),
enclosing_symbols: frozenset[spux.Symbol] = frozenset(),
) -> typ.Self: ) -> typ.Self:
return ParamsFlow( return ParamsFlow(
func_args=self.func_args + list(enclosing_func_args), func_args=self.func_args + list(enclosing_func_args),
func_kwargs=self.func_kwargs | dict(enclosing_func_kwargs), func_kwargs=self.func_kwargs | dict(enclosing_func_kwargs),
symbols=self.symbols | enclosing_symbols,
) )
@ -804,8 +845,6 @@ class InfoFlow:
default_factory=dict default_factory=dict
) ## TODO: Rename to dim_idxs ) ## TODO: Rename to dim_idxs
## TODO: Add PhysicalType
@functools.cached_property @functools.cached_property
def dim_lens(self) -> dict[str, int]: def dim_lens(self) -> dict[str, int]:
return {dim_name: len(dim_idx) for dim_name, dim_idx in self.dim_idx.items()} return {dim_name: len(dim_idx) for dim_name, dim_idx in self.dim_idx.items()}
@ -820,6 +859,13 @@ class InfoFlow:
def dim_units(self) -> dict[str, spux.Unit]: def dim_units(self) -> dict[str, spux.Unit]:
return {dim_name: dim_idx.unit for dim_name, dim_idx in self.dim_idx.items()} return {dim_name: dim_idx.unit for dim_name, dim_idx in self.dim_idx.items()}
@functools.cached_property
def dim_physical_types(self) -> dict[str, spux.PhysicalType]:
return {
dim_name: spux.PhysicalType.from_unit(dim_idx.unit)
for dim_name, dim_idx in self.dim_idx.items()
}
@functools.cached_property @functools.cached_property
def dim_idx_arrays(self) -> list[jax.Array]: def dim_idx_arrays(self) -> list[jax.Array]:
return [ return [
@ -850,6 +896,21 @@ class InfoFlow:
#################### ####################
# - Methods # - Methods
#################### ####################
def rescale_dim_idxs(self, new_dim_idxs: dict[str, LazyArrayRangeFlow]) -> typ.Self:
return InfoFlow(
# Dimensions
dim_names=self.dim_names,
dim_idx={
_dim_name: new_dim_idxs.get(_dim_name, dim_idx)
for _dim_name, dim_idx in self.dim_idx.items()
},
# Outputs
output_name=self.output_name,
output_shape=self.output_shape,
output_mathtype=self.output_mathtype,
output_unit=self.output_unit,
)
def delete_dimension(self, dim_name: str) -> typ.Self: def delete_dimension(self, dim_name: str) -> typ.Self:
"""Delete a dimension.""" """Delete a dimension."""
return InfoFlow( return InfoFlow(

View File

@ -43,10 +43,9 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
# Sources # Sources
## Sources / Temporal Shapes ## Sources / Temporal Shapes
GaussianPulseTemporalShape = enum.auto() PulseTemporalShape = enum.auto()
ContinuousWaveTemporalShape = enum.auto() WaveTemporalShape = enum.auto()
SymbolicTemporalShape = enum.auto() ExprTemporalShape = enum.auto()
DataTemporalShape = enum.auto()
## Sources / ## Sources /
PointDipoleSource = enum.auto() PointDipoleSource = enum.auto()
PlaneWaveSource = enum.auto() PlaneWaveSource = enum.auto()

View File

@ -3,11 +3,45 @@
import enum import enum
import typing as typ import typing as typ
import jax.numpy as jnp
import tidy3d as td import tidy3d as td
def manual_amp_time(self, time: float) -> complex:
"""Copied implementation of `pulse.amp_time` for `tidy3d` temporal shapes, which replaces use of `numpy` with `jax.numpy` for `jit`-ability.
Since the function is detached from the method, `self` is not implicitly available. It should be pre-defined from a real source time object using `functools.partial`, before `jax.jit`ing.
## License
**This function is directly copied from `tidy3d`**.
As such, it should be considered available under the `tidy3d` license (as of writing, LGPL 2.1): <https://github.com/flexcompute/tidy3d/blob/develop/LICENSE>
## Reference
Permalink to GitHub source code: <https://github.com/flexcompute/tidy3d/blob/3ee34904eb6687a86a5fb3f4ed6d3295c228cd83/tidy3d/components/source.py#L143C1-L163C25>
"""
twidth = 1.0 / (2 * jnp.pi * self.fwidth)
omega0 = 2 * jnp.pi * self.freq0
time_shifted = time - self.offset * twidth
offset = jnp.exp(1j * self.phase)
oscillation = jnp.exp(-1j * omega0 * time)
amp = jnp.exp(-(time_shifted**2) / 2 / twidth**2) * self.amplitude
pulse_amp = offset * oscillation * amp
# subtract out DC component
if self.remove_dc_component:
pulse_amp = pulse_amp * (1j + time_shifted / twidth**2 / omega0)
else:
# 1j to make it agree in large omega0 limit
pulse_amp = pulse_amp * 1j
return pulse_amp
## TODO: Sim Domain type, w/pydantic checks! ## TODO: Sim Domain type, w/pydantic checks!
class SimSpaceAxis(enum.StrEnum): class SimSpaceAxis(enum.StrEnum):
"""The axis labels of the global simulation coordinate system.""" """The axis labels of the global simulation coordinate system."""
@ -61,13 +95,13 @@ class BoundCondType(enum.StrEnum):
Attributes: Attributes:
Pml: "Perfectly Matched Layer" models infinite free space. Pml: "Perfectly Matched Layer" models infinite free space.
**Should be placed sufficiently far** (ex. $\frac{\lambda}{2}) from any active structures to mitigate divergence. **Should be placed sufficiently far** (ex. $\frac{\lambda}{2}) from any active structures to mitigate divergence.
Periodic: Denotes Bloch-basedrepetition Periodic: Denotes naive Bloch boundaries (aka. periodic w/phase shift of 0).
Pec: "Perfect Electrical Conductor" models a surface that perfectly reflects electric fields. 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. Pmc: "Perfect Magnetic Conductor" models a surface that perfectly reflects the magnetic fields.
""" """
Pml = enum.auto() Pml = enum.auto()
Periodic = enum.auto() NaiveBloch = enum.auto()
Pec = enum.auto() Pec = enum.auto()
Pmc = enum.auto() Pmc = enum.auto()
@ -86,7 +120,7 @@ class BoundCondType(enum.StrEnum):
BCT.Pml: 'PML', BCT.Pml: 'PML',
BCT.Pec: 'PEC', BCT.Pec: 'PEC',
BCT.Pmc: 'PMC', BCT.Pmc: 'PMC',
BCT.Periodic: 'Periodic', BCT.NaiveBloch: 'NaiveBloch',
}[v] }[v]
@staticmethod @staticmethod
@ -115,5 +149,5 @@ class BoundCondType(enum.StrEnum):
BCT.Pml: td.PML(), BCT.Pml: td.PML(),
BCT.Pec: td.PECBoundary(), BCT.Pec: td.PECBoundary(),
BCT.Pmc: td.PMCBoundary(), BCT.Pmc: td.PMCBoundary(),
BCT.Periodic: td.Periodic(), BCT.NaiveBloch: td.Periodic(),
}[self] }[self]

View File

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

View File

@ -45,7 +45,7 @@ class ExtractDataNode(base.MaxwellSimNode):
} }
output_socket_sets: typ.ClassVar = { output_socket_sets: typ.ClassVar = {
'Sim Data': {'Monitor Data': sockets.MaxwellMonitorDataSocketDef()}, 'Sim Data': {'Monitor Data': sockets.MaxwellMonitorDataSocketDef()},
'Monitor Data': {'Expr': sockets.ExprSocketDef()}, 'Monitor Data': {'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array)},
} }
#################### ####################
@ -409,7 +409,7 @@ class ExtractDataNode(base.MaxwellSimNode):
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
# Trigger # Trigger
'Data', 'Expr',
kind=ct.FlowKind.Info, kind=ct.FlowKind.Info,
# Loaded # Loaded
props={'monitor_data_type', 'extract_filter'}, props={'monitor_data_type', 'extract_filter'},

View File

@ -110,10 +110,10 @@ class FilterMathNode(base.MaxwellSimNode):
bl_label = 'Filter Math' bl_label = 'Filter Math'
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
} }
#################### ####################
@ -187,14 +187,13 @@ class FilterMathNode(base.MaxwellSimNode):
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None: def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
layout.prop(self, self.blfields['operation'], text='') layout.prop(self, self.blfields['operation'], text='')
if self.active_socket_set == 'Dimensions': if self.operation in [FilterOperation.PinLen1, FilterOperation.Pin]:
if self.operation in [FilterOperation.PinLen1, FilterOperation.Pin]: layout.prop(self, self.blfields['dim_0'], text='')
layout.prop(self, self.blfields['dim_0'], text='')
if self.operation == FilterOperation.Swap: if self.operation == FilterOperation.Swap:
row = layout.row(align=True) row = layout.row(align=True)
row.prop(self, self.blfields['dim_0'], text='') row.prop(self, self.blfields['dim_0'], text='')
row.prop(self, self.blfields['dim_1'], text='') row.prop(self, self.blfields['dim_1'], text='')
#################### ####################
# - Events # - Events
@ -292,7 +291,7 @@ class FilterMathNode(base.MaxwellSimNode):
return lazy_value_func.compose_within( return lazy_value_func.compose_within(
operation.jax_func(axis_0, axis_1), operation.jax_func(axis_0, axis_1),
enclosing_func_args=[int] if operation == 'PIN' else [], enclosing_func_args=[int] if operation == FilterOperation.Pin else [],
supports_jax=True, supports_jax=True,
) )
return ct.FlowSignal.FlowPending return ct.FlowSignal.FlowPending
@ -383,7 +382,7 @@ class FilterMathNode(base.MaxwellSimNode):
pinned_value = input_sockets['Value'] pinned_value = input_sockets['Value']
has_pinned_value = not ct.FlowSignal.check(pinned_value) has_pinned_value = not ct.FlowSignal.check(pinned_value)
if props['operation'] == 'PIN' and has_pinned_value: if props['operation'] == FilterOperation.Pin and has_pinned_value:
nearest_idx_to_value = info.dim_idx[dim_0].nearest_idx_of( nearest_idx_to_value = info.dim_idx[dim_0].nearest_idx_of(
pinned_value, require_sorted=True pinned_value, require_sorted=True
) )

View File

@ -4,7 +4,6 @@ import enum
import typing as typ import typing as typ
import bpy import bpy
import jax
import jax.numpy as jnp import jax.numpy as jnp
import sympy as sp import sympy as sp
@ -138,6 +137,9 @@ class MapOperation(enum.StrEnum):
@staticmethod @staticmethod
def by_element_shape(shape: tuple[int, ...] | None) -> list[typ.Self]: def by_element_shape(shape: tuple[int, ...] | None) -> list[typ.Self]:
MO = MapOperation MO = MapOperation
if shape == 'noshape':
return []
# By Number # By Number
if shape is None: if shape is None:
return [ return [
@ -259,7 +261,7 @@ class MapOperation(enum.StrEnum):
), ),
## TODO: Matrix -> Vec ## TODO: Matrix -> Vec
## TODO: Matrix -> Matrices ## TODO: Matrix -> Matrices
}.get(self, info) }.get(self, info)()
class MapMathNode(base.MaxwellSimNode): class MapMathNode(base.MaxwellSimNode):
@ -346,10 +348,10 @@ class MapMathNode(base.MaxwellSimNode):
bl_label = 'Map Math' bl_label = 'Map Math'
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
} }
#################### ####################
@ -366,12 +368,12 @@ class MapMathNode(base.MaxwellSimNode):
if has_info: if has_info:
return info.output_shape return info.output_shape
return None return 'noshape'
output_shape: tuple[int, ...] | None = bl_cache.BLField(None) output_shape: tuple[int, ...] | None = bl_cache.BLField(None)
def search_operations(self) -> list[ct.BLEnumElement]: def search_operations(self) -> list[ct.BLEnumElement]:
if self.expr_output_shape is not None: if self.expr_output_shape != 'noshape':
return [ return [
operation.bl_enum_element(i) operation.bl_enum_element(i)
for i, operation in enumerate( for i, operation in enumerate(
@ -401,8 +403,8 @@ class MapMathNode(base.MaxwellSimNode):
run_on_init=True, run_on_init=True,
) )
def on_input_changed(self): def on_input_changed(self):
# if self.operation not in MapOperation.by_element_shape(self.expr_output_shape): if self.operation not in MapOperation.by_element_shape(self.expr_output_shape):
self.operation = bl_cache.Signal.ResetEnumItems self.operation = bl_cache.Signal.ResetEnumItems
@events.on_value_changed( @events.on_value_changed(
# Trigger # Trigger
@ -449,7 +451,7 @@ class MapMathNode(base.MaxwellSimNode):
mapper = input_sockets['Mapper'] mapper = input_sockets['Mapper']
has_expr = not ct.FlowSignal.check(expr) has_expr = not ct.FlowSignal.check(expr)
has_mapper = not ct.FlowSignal.check(expr) has_mapper = not ct.FlowSignal.check(mapper)
if has_expr and operation is not None: if has_expr and operation is not None:
if not has_mapper: if not has_mapper:
@ -494,11 +496,11 @@ class MapMathNode(base.MaxwellSimNode):
# - Compute Auxiliary: Info / Params # - Compute Auxiliary: Info / Params
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Data', 'Expr',
kind=ct.FlowKind.Info, kind=ct.FlowKind.Info,
props={'active_socket_set', 'operation'}, props={'active_socket_set', 'operation'},
input_sockets={'Data'}, input_sockets={'Expr'},
input_socket_kinds={'Data': ct.FlowKind.Info}, input_socket_kinds={'Expr': ct.FlowKind.Info},
) )
def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow: def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow:
operation = props['operation'] operation = props['operation']
@ -512,13 +514,13 @@ class MapMathNode(base.MaxwellSimNode):
return ct.FlowSignal.FlowPending return ct.FlowSignal.FlowPending
@events.computes_output_socket( @events.computes_output_socket(
'Data', 'Expr',
kind=ct.FlowKind.Params, kind=ct.FlowKind.Params,
input_sockets={'Data'}, input_sockets={'Expr'},
input_socket_kinds={'Data': ct.FlowKind.Params}, input_socket_kinds={'Expr': ct.FlowKind.Params},
) )
def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow | ct.FlowSignal: def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow | ct.FlowSignal:
return input_sockets['Data'] return input_sockets['Expr']
#################### ####################

View File

@ -56,11 +56,11 @@ class OperateMathNode(base.MaxwellSimNode):
bl_label = 'Operate Math' bl_label = 'Operate Math'
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr L': sockets.ExprSocketDef(), 'Expr L': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
'Expr R': sockets.ExprSocketDef(), 'Expr R': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
} }
#################### ####################

View File

@ -2,8 +2,6 @@ import enum
import typing as typ import typing as typ
import bpy import bpy
import jax
import jax.numpy as jnp
import jaxtyping as jtyp import jaxtyping as jtyp
import matplotlib.axis as mpl_ax import matplotlib.axis as mpl_ax
import sympy as sp import sympy as sp
@ -196,6 +194,7 @@ class VizNode(base.MaxwellSimNode):
#################### ####################
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef( 'Expr': sockets.ExprSocketDef(
active_kind=ct.FlowKind.Array,
symbols={_x := sp.Symbol('x', real=True)}, symbols={_x := sp.Symbol('x', real=True)},
default_value=2 * _x, default_value=2 * _x,
), ),
@ -284,16 +283,57 @@ class VizNode(base.MaxwellSimNode):
socket_name='Expr', socket_name='Expr',
input_sockets={'Expr'}, input_sockets={'Expr'},
run_on_init=True, run_on_init=True,
input_socket_kinds={'Expr': ct.FlowKind.Info}, input_socket_kinds={'Expr': {ct.FlowKind.Info, ct.FlowKind.Params}},
input_sockets_optional={'Expr': True}, input_sockets_optional={'Expr': True},
) )
def on_any_changed(self, input_sockets: dict): def on_any_changed(self, input_sockets: dict):
if not ct.FlowSignal.check_single( info = input_sockets['Expr'][ct.FlowKind.Info]
input_sockets['Expr'], ct.FlowSignal.FlowPending params = input_sockets['Expr'][ct.FlowKind.Params]
):
has_info = not ct.FlowSignal.check(info)
has_params = not ct.FlowSignal.check(params)
# Reset Viz Mode/Target
has_nonpending_info = not ct.FlowSignal.check_single(
info, ct.FlowSignal.FlowPending
)
if has_nonpending_info:
self.viz_mode = bl_cache.Signal.ResetEnumItems self.viz_mode = bl_cache.Signal.ResetEnumItems
self.viz_target = bl_cache.Signal.ResetEnumItems self.viz_target = bl_cache.Signal.ResetEnumItems
# Provide Sockets for Symbol Realization
## -> This happens if Params contains not-yet-realized symbols.
if has_info and has_params and params.symbols:
if set(self.loose_input_sockets) != {
sym.name for sym in params.symbols if sym.name in info.dim_names
}:
self.loose_input_sockets = {
sym.name: sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange,
shape=None,
mathtype=info.dim_mathtypes[sym.name],
physical_type=info.dim_physical_types[sym.name],
default_min=(
info.dim_idx[sym.name].start
if not sp.S(info.dim_idx[sym.name].start).is_infinite
else sp.S(0)
),
default_max=(
info.dim_idx[sym.name].start
if not sp.S(info.dim_idx[sym.name].stop).is_infinite
else sp.S(1)
),
default_steps=50,
)
for sym in sorted(
params.symbols, key=lambda el: info.dim_names.index(el.name)
)
if sym.name in info.dim_names
}
elif self.loose_input_sockets:
self.loose_input_sockets = {}
@events.on_value_changed( @events.on_value_changed(
prop_name='viz_mode', prop_name='viz_mode',
run_on_init=True, run_on_init=True,
@ -309,39 +349,62 @@ class VizNode(base.MaxwellSimNode):
props={'viz_mode', 'viz_target', 'colormap'}, props={'viz_mode', 'viz_target', 'colormap'},
input_sockets={'Expr'}, input_sockets={'Expr'},
input_socket_kinds={ input_socket_kinds={
'Expr': {ct.FlowKind.Array, ct.FlowKind.LazyValueFunc, ct.FlowKind.Info} 'Expr': {ct.FlowKind.LazyValueFunc, ct.FlowKind.Info, ct.FlowKind.Params}
}, },
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
all_loose_input_sockets=True,
stop_propagation=True, stop_propagation=True,
) )
def on_show_plot( def on_show_plot(
self, self, managed_objs, props, input_sockets, loose_input_sockets, unit_systems
managed_objs: dict,
input_sockets: dict,
props: dict,
): ):
# Retrieve Inputs # Retrieve Inputs
array_flow = input_sockets['Expr'][ct.FlowKind.Array]
info = input_sockets['Expr'][ct.FlowKind.Info] info = input_sockets['Expr'][ct.FlowKind.Info]
params = input_sockets['Expr'][ct.FlowKind.Params]
has_info = not ct.FlowSignal.check(info)
has_params = not ct.FlowSignal.check(params)
# Check Flow
if ( if (
any(ct.FlowSignal.check(inp) for inp in [array_flow, info]) not has_info
or not has_params
or props['viz_mode'] is None or props['viz_mode'] is None
or props['viz_target'] is None or props['viz_target'] is None
): ):
return return
# Viz Target # Compute Data
lazy_value_func = input_sockets['Expr'][ct.FlowKind.LazyValueFunc]
symbol_values = (
loose_input_sockets
if not params.symbols
else {
sym: loose_input_sockets[sym.name]
.realize_array.rescale_to_unit(info.dim_units[sym.name])
.values
for sym in params.sorted_symbols
}
)
data = lazy_value_func.func_jax(
*params.scaled_func_args(
unit_systems['BlenderUnits'], symbol_values=symbol_values
),
**params.scaled_func_kwargs(
unit_systems['BlenderUnits'], symbol_values=symbol_values
),
)
if params.symbols:
info = info.rescale_dim_idxs(loose_input_sockets)
# Visualize by-Target
if props['viz_target'] == VizTarget.Plot2D: if props['viz_target'] == VizTarget.Plot2D:
managed_objs['plot'].mpl_plot_to_image( managed_objs['plot'].mpl_plot_to_image(
lambda ax: VizMode.to_plotter(props['viz_mode'])( lambda ax: VizMode.to_plotter(props['viz_mode'])(data, info, ax),
array_flow.values, info, ax
),
bl_select=True, bl_select=True,
) )
if props['viz_target'] == VizTarget.Pixels: if props['viz_target'] == VizTarget.Pixels:
managed_objs['plot'].map_2d_to_image( managed_objs['plot'].map_2d_to_image(
array_flow.values, data,
colormap=props['colormap'], colormap=props['colormap'],
bl_select=True, bl_select=True,
) )

View File

@ -477,6 +477,7 @@ class MaxwellSimNode(bpy.types.Node):
for socket_name, socket_def in created_sockets.items(): for socket_name, socket_def in created_sockets.items():
socket_def.preinit(all_bl_sockets[socket_name]) socket_def.preinit(all_bl_sockets[socket_name])
socket_def.init(all_bl_sockets[socket_name]) socket_def.init(all_bl_sockets[socket_name])
socket_def.postinit(all_bl_sockets[socket_name])
def _sync_sockets(self) -> None: def _sync_sockets(self) -> None:
"""Synchronize the node's sockets with the active sockets. """Synchronize the node's sockets with the active sockets.

View File

@ -208,7 +208,7 @@ class BlochBoundCondNode(base.MaxwellSimNode):
return td.Periodic() return td.Periodic()
# Source-Derived # Source-Derived
if props['active_socket_set'] == 'Naive': if props['active_socket_set'] == 'Source-Derived':
sim_domain = input_sockets['Sim Domain'] sim_domain = input_sockets['Sim Domain']
valid_sim_axis = props['valid_sim_axis'] valid_sim_axis = props['valid_sim_axis']

View File

@ -66,6 +66,8 @@ class PhysicalConstantNode(base.MaxwellSimNode):
# - UI # - UI
#################### ####################
def draw_props(self, _, col: bpy.types.UILayout) -> None: def draw_props(self, _, col: bpy.types.UILayout) -> None:
col.prop(self, self.blfields['physical_type'], text='')
row = col.row(align=True) row = col.row(align=True)
row.prop(self, self.blfields['mathtype'], text='') row.prop(self, self.blfields['mathtype'], text='')
row.prop(self, self.blfields['size'], text='') row.prop(self, self.blfields['size'], text='')
@ -74,25 +76,34 @@ class PhysicalConstantNode(base.MaxwellSimNode):
# - Events # - Events
#################### ####################
@events.on_value_changed( @events.on_value_changed(
prop_name={'physical_type', 'mathtype', 'size'}, # Trigger
prop_name={'physical_type'},
run_on_init=True, run_on_init=True,
props={'physical_type', 'mathtype', 'size'}, # Loaded
props={'physical_type'},
) )
def on_mathtype_or_size_changed(self, props) -> None: def on_physical_type_changed(self, props) -> None:
"""Change the input/output expression sockets to match the mathtype and size declared in the node.""" """Change the input/output expression sockets to match the mathtype and size declared in the node."""
shape = props['size'].shape
# Set Input Socket Physical Type # Set Input Socket Physical Type
if self.inputs['Value'].physical_type != props['physical_type']: if self.inputs['Value'].physical_type != props['physical_type']:
self.inputs['Value'].physical_type = props['physical_type'] self.inputs['Value'].physical_type = props['physical_type']
self.search_mathtypes = bl_cache.Signal.ResetEnumItems self.mathtype = bl_cache.Signal.ResetEnumItems
self.search_sizes = bl_cache.Signal.ResetEnumItems self.size = bl_cache.Signal.ResetEnumItems
@events.on_value_changed(
# Trigger
prop_name={'mathtype', 'size'},
run_on_init=True,
# Loaded
props={'physical_type', 'mathtype', 'size'},
)
def on_mathtype_or_size_changed(self, props) -> None:
# Set Input Socket Math Type # Set Input Socket Math Type
if self.inputs['Value'].mathtype != props['mathtype']: if self.inputs['Value'].mathtype != props['mathtype']:
self.inputs['Value'].mathtype = props['mathtype'] self.inputs['Value'].mathtype = props['mathtype']
# Set Input Socket Shape # Set Input Socket Shape
shape = props['size'].shape
if self.inputs['Value'].shape != shape: if self.inputs['Value'].shape != shape:
self.inputs['Value'].shape = shape self.inputs['Value'].shape = shape

View File

@ -147,7 +147,9 @@ class WaveConstantNode(base.MaxwellSimNode):
if has_freq: if has_freq:
return input_sockets['Freq'] return input_sockets['Freq']
return sci_constants.vac_speed_of_light / input_sockets['WL'] return spu.convert_to(
sci_constants.vac_speed_of_light / input_sockets['WL'], spux.THz
)
@events.computes_output_socket( @events.computes_output_socket(
'WL', 'WL',

View File

@ -301,15 +301,16 @@ class LibraryMediumNode(base.MaxwellSimNode):
#################### ####################
@events.on_show_plot( @events.on_show_plot(
managed_objs={'plot'}, managed_objs={'plot'},
props={'material'}, props={'medium'},
stop_propagation=True, stop_propagation=True,
) )
def on_show_plot( def on_show_plot(
self, self,
managed_objs: dict, managed_objs,
props,
): ):
managed_objs['plot'].mpl_plot_to_image( managed_objs['plot'].mpl_plot_to_image(
lambda ax: self.medium.plot(self.medium.frequency_range, ax=ax), lambda ax: self.medium.plot(props['medium'].frequency_range, ax=ax),
bl_select=True, bl_select=True,
) )
## TODO: Plot based on Wl, not freq. ## TODO: Plot based on Wl, not freq.

View File

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

View File

@ -1,16 +1,23 @@
import bpy import typing as typ
import bpy
import tidy3d as td
from blender_maxwell.services import tdcloud
from blender_maxwell.utils import bl_cache, logger
from ......services import tdcloud
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base, events
log = logger.get(__name__)
#################### ####################
# - Web Uploader / Loader / Runner / Releaser # - Web Uploader / Loader / Runner / Releaser
#################### ####################
class UploadSimulation(bpy.types.Operator): class UploadSimulation(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__upload_simulation' bl_idname = ct.OperatorType.NodeUploadSimulation
bl_label = 'Upload Tidy3D Simulation' bl_label = 'Upload Tidy3D Simulation'
bl_description = 'Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud' bl_description = 'Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud'
@ -23,7 +30,7 @@ class UploadSimulation(bpy.types.Operator):
and context.node.lock_tree and context.node.lock_tree
and tdcloud.IS_AUTHENTICATED and tdcloud.IS_AUTHENTICATED
and not context.node.tracked_task_id and not context.node.tracked_task_id
and context.node.inputs['FDTD Sim'].is_linked and context.node.inputs['Sim'].is_linked
) )
def execute(self, context): def execute(self, context):
@ -33,7 +40,7 @@ class UploadSimulation(bpy.types.Operator):
class RunSimulation(bpy.types.Operator): class RunSimulation(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__run_simulation' bl_idname = ct.OperatorType.NodeRunSimulation
bl_label = 'Run Tracked Tidy3D Sim' bl_label = 'Run Tracked Tidy3D Sim'
bl_description = 'Run the currently tracked simulation task' bl_description = 'Run the currently tracked simulation task'
@ -61,7 +68,7 @@ class RunSimulation(bpy.types.Operator):
class ReloadTrackedTask(bpy.types.Operator): class ReloadTrackedTask(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__reload_tracked_task' bl_idname = ct.OperatorType.NodeReloadTrackedTask
bl_label = 'Reload Tracked Tidy3D Cloud Task' bl_label = 'Reload Tracked Tidy3D Cloud Task'
bl_description = 'Reload the currently tracked simulation task' bl_description = 'Reload the currently tracked simulation task'
@ -86,7 +93,7 @@ class ReloadTrackedTask(bpy.types.Operator):
class EstCostTrackedTask(bpy.types.Operator): class EstCostTrackedTask(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__est_cost_tracked_task' bl_idname = ct.OperatorType.NodeEstCostTrackedTask
bl_label = 'Est Cost of Tracked Tidy3D Cloud Task' bl_label = 'Est Cost of Tracked Tidy3D Cloud Task'
bl_description = 'Reload the currently tracked simulation task' bl_description = 'Reload the currently tracked simulation task'
@ -113,7 +120,7 @@ class EstCostTrackedTask(bpy.types.Operator):
class ReleaseTrackedTask(bpy.types.Operator): class ReleaseTrackedTask(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__release_tracked_task' bl_idname = ct.OperatorType.ReleaseTrackedTask
bl_label = 'Release Tracked Tidy3D Cloud Task' bl_label = 'Release Tracked Tidy3D Cloud Task'
bl_description = 'Release the currently tracked simulation task' bl_description = 'Release the currently tracked simulation task'
@ -140,92 +147,60 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.Tidy3DWebExporter node_type = ct.NodeType.Tidy3DWebExporter
bl_label = 'Tidy3D Web Exporter' bl_label = 'Tidy3D Web Exporter'
input_sockets = { input_sockets: typ.ClassVar = {
'FDTD Sim': sockets.MaxwellFDTDSimSocketDef(), 'Sim': sockets.MaxwellFDTDSimSocketDef(),
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef( 'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=False, should_exist=False,
), ),
} }
output_sockets: typ.ClassVar = {
'Sim Data': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
}
#################### ####################
# - Properties # - Properties
#################### ####################
lock_tree: bpy.props.BoolProperty( lock_tree: bool = bl_cache.BLField(False, prop_ui=True)
name='Whether to lock the attached tree', tracked_task_id: str = bl_cache.BLField('', prop_ui=True)
description='Whether or not to lock the attached tree',
default=False,
update=lambda self, context: self.sync_lock_tree(context),
)
tracked_task_id: bpy.props.StringProperty(
name='Tracked Task ID',
description='The currently tracked task ID',
default='',
update=lambda self, context: self.sync_tracked_task_id(context),
)
# Cache
cache_total_monitor_data: bpy.props.FloatProperty(
name='(Cached) Total Monitor Data',
description='Required storage space by all monitors',
default=0.0,
)
cache_est_cost: bpy.props.FloatProperty(
name='(Cached) Estimated Total Cost',
description='Est. Cost in FlexCompute units',
default=-1.0,
)
#################### ####################
# - Sync Methods # - Computed
#################### ####################
def sync_lock_tree(self, context): @bl_cache.cached_bl_property(persist=False)
if self.lock_tree: def sim(self) -> td.Simulation | None:
self.trigger_event(ct.FlowEvent.EnableLock) sim = self._compute_input('Sim')
self.locked = False has_sim = not ct.FlowSignal.check(sim)
for bl_socket in self.inputs:
if bl_socket.name == 'FDTD Sim':
continue
bl_socket.locked = False
else: if has_sim:
self.trigger_event(ct.FlowEvent.DisableLock) sim.validate_pre_upload(source_required=True)
return sim
return None
self.on_prop_changed('lock_tree', context) @bl_cache.cached_bl_property(persist=False)
def total_monitor_data(self) -> float:
if self.sim is not None:
return sum(self.sim.monitors_data_size.values())
return 0.0
def sync_tracked_task_id(self, context): @bl_cache.cached_bl_property(persist=False)
# Select Tracked Task def est_cost(self) -> float | None:
if self.tracked_task_id: if self.tracked_task_id != '':
cloud_task = tdcloud.TidyCloudTasks.task(self.tracked_task_id)
task_info = tdcloud.TidyCloudTasks.task_info(self.tracked_task_id) task_info = tdcloud.TidyCloudTasks.task_info(self.tracked_task_id)
if task_info is not None:
return task_info.cost_est()
self.loose_output_sockets = { return None
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
}
self.inputs['Cloud Task'].locked = True
# Release Tracked Task
else:
self.cache_est_cost = -1.0
self.loose_output_sockets = {}
self.inputs['Cloud Task'].on_prepare_new_task()
self.inputs['Cloud Task'].locked = False
self.on_prop_changed('tracked_task_id', context)
#################### ####################
# - Output Socket Callbacks # - Methods
#################### ####################
def validate_sim(self):
if (sim := self._compute_input('FDTD Sim')) is None:
msg = 'Tried to validate simulation, but none is attached'
raise ValueError(msg)
sim.validate_pre_upload(source_required=True)
def upload_sim(self): def upload_sim(self):
if (sim := self._compute_input('FDTD Sim')) is None: if self.sim is None:
msg = 'Tried to upload simulation, but none is attached' msg = 'Tried to upload simulation, but none is attached'
raise ValueError(msg) raise ValueError(msg)
@ -242,7 +217,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
cloud_task = tdcloud.TidyCloudTasks.mk_task( cloud_task = tdcloud.TidyCloudTasks.mk_task(
task_name=new_task[0], task_name=new_task[0],
cloud_folder=new_task[1], cloud_folder=new_task[1],
sim=sim, sim=self.sim,
verbose=True, verbose=True,
) )
@ -271,22 +246,24 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
# Row: Upload Sim Buttons # Row: Upload Sim Buttons
row = layout.row(align=True) row = layout.row(align=True)
row.operator( row.operator(
UploadSimulation.bl_idname, ct.OperatorType.NodeUploadSimulation,
text='Upload', text='Upload',
) )
tree_lock_icon = 'LOCKED' if self.lock_tree else 'UNLOCKED' tree_lock_icon = 'LOCKED' if self.lock_tree else 'UNLOCKED'
row.prop(self, 'lock_tree', toggle=True, icon=tree_lock_icon, text='') row.prop(
self, self.blfields['lock_tree'], toggle=True, icon=tree_lock_icon, text=''
)
# Row: Run Sim Buttons # Row: Run Sim Buttons
row = layout.row(align=True) row = layout.row(align=True)
row.operator( row.operator(
RunSimulation.bl_idname, ct.OperatorType.NodeRunSimulation,
text='Run', text='Run',
) )
if self.tracked_task_id: if self.tracked_task_id:
tree_lock_icon = 'LOOP_BACK' tree_lock_icon = 'LOOP_BACK'
row.operator( row.operator(
ReleaseTrackedTask.bl_idname, ct.OperatorType.ReleaseTrackedTask,
icon='LOOP_BACK', icon='LOOP_BACK',
text='', text='',
) )
@ -313,7 +290,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
col.label(icon=conn_icon) col.label(icon=conn_icon)
# Simulation Info # Simulation Info
if self.inputs['FDTD Sim'].is_linked: if self.inputs['Sim'].is_linked:
row = layout.row() row = layout.row()
row.alignment = 'CENTER' row.alignment = 'CENTER'
row.label(text='Sim Info') row.label(text='Sim Info')
@ -327,7 +304,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
## Split: Right Column ## Split: Right Column
col = split.column(align=False) col = split.column(align=False)
col.alignment = 'RIGHT' col.alignment = 'RIGHT'
col.label(text=f'{self.cache_total_monitor_data / 1_000_000:.2f}MB') col.label(text=f'{self.total_monitor_data / 1_000_000:.2f}MB')
# Cloud Task Info # Cloud Task Info
if self.tracked_task_id and tdcloud.IS_AUTHENTICATED: if self.tracked_task_id and tdcloud.IS_AUTHENTICATED:
@ -348,12 +325,12 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
text=f'Status: {task_info.status.capitalize()}', text=f'Status: {task_info.status.capitalize()}',
) )
row.operator( row.operator(
ReloadTrackedTask.bl_idname, ct.OperatorType.NodeReloadTrackedTask,
text='', text='',
icon='FILE_REFRESH', icon='FILE_REFRESH',
) )
row.operator( row.operator(
EstCostTrackedTask.bl_idname, ct.OperatorType.NodeEstCostTrackedTask,
text='', text='',
icon='SORTTIME', icon='SORTTIME',
) )
@ -369,9 +346,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
col.label(text='Real Cost') col.label(text='Real Cost')
## Split: Right Column ## Split: Right Column
cost_est = ( cost_est = f'{self.est_cost:.2f}' if self.est_cost >= 0 else 'TBD'
f'{self.cache_est_cost:.2f}' if self.cache_est_cost >= 0 else 'TBD'
)
cost_real = ( cost_real = (
f'{task_info.cost_real:.2f}' f'{task_info.cost_real:.2f}'
if task_info.cost_real is not None if task_info.cost_real is not None
@ -386,9 +361,37 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
# Connection Information # Connection Information
####################
# - Events
####################
@events.on_value_changed(prop_name='lock_tree', props={'lock_tree'})
def on_lock_tree_changed(self, props):
if props['lock_tree']:
self.trigger_event(ct.FlowEvent.EnableLock)
self.locked = False
for bl_socket in self.inputs:
if bl_socket.name == 'Sim':
continue
bl_socket.locked = False
else:
self.trigger_event(ct.FlowEvent.DisableLock)
@events.on_value_changed(prop_name='tracked_task_id', props={'tracked_task_id'})
def on_tracked_task_id_changed(self, props):
if props['tracked_task_id']:
self.inputs['Cloud Task'].locked = True
else:
self.total_monitor_data = bl_cache.Signal.InvalidateCache
self.est_cost = bl_cache.Signal.InvalidateCache
self.inputs['Cloud Task'].on_prepare_new_task()
self.inputs['Cloud Task'].locked = False
#################### ####################
# - Output Methods # - Output Methods
#################### ####################
## TODO: Retrieve simulation data if/when the simulation is done
@events.computes_output_socket( @events.computes_output_socket(
'Cloud Task', 'Cloud Task',
input_sockets={'Cloud Task'}, input_sockets={'Cloud Task'},
@ -399,21 +402,6 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
return None return None
####################
# - Output Methods
####################
@events.on_value_changed(
socket_name='FDTD Sim',
input_sockets={'FDTD Sim'},
)
def on_value_changed__fdtd_sim(self, input_sockets):
if (sim := self._compute_input('FDTD Sim')) is None:
self.cache_total_monitor_data = 0
return
sim.validate_pre_upload(source_required=True)
self.cache_total_monitor_data = sum(sim.monitors_data_size.values())
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -1,14 +1,16 @@
# from . import sim_grid # from . import sim_grid
# from . import sim_grid_axes # from . import sim_grid_axes
from . import fdtd_sim, sim_domain from . import fdtd_sim, sim_domain, combine
BL_REGISTER = [ BL_REGISTER = [
*combine.BL_REGISTER,
*sim_domain.BL_REGISTER, *sim_domain.BL_REGISTER,
# *sim_grid.BL_REGISTER, # *sim_grid.BL_REGISTER,
# *sim_grid_axes.BL_REGISTER, # *sim_grid_axes.BL_REGISTER,
*fdtd_sim.BL_REGISTER, *fdtd_sim.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**combine.BL_NODES,
**sim_domain.BL_NODES, **sim_domain.BL_NODES,
# **sim_grid.BL_NODES, # **sim_grid.BL_NODES,
# **sim_grid_axes.BL_NODES, # **sim_grid_axes.BL_NODES,

View File

@ -1,8 +1,9 @@
import typing as typ import typing as typ
import bpy
import sympy as sp import sympy as sp
from blender_maxwell.utils import bl_cache
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base, events from .. import base, events
@ -19,24 +20,8 @@ class CombineNode(base.MaxwellSimNode):
'Maxwell Sources': {}, 'Maxwell Sources': {},
'Maxwell Structures': {}, 'Maxwell Structures': {},
'Maxwell Monitors': {}, 'Maxwell Monitors': {},
'Real 3D Vector': {f'x_{i}': sockets.RealNumberSocketDef() for i in range(3)},
# "Point 3D": {
# axis: sockets.PhysicalLengthSocketDef()
# for i, axis in zip(
# range(3),
# ["x", "y", "z"]
# )
# },
# "Size 3D": {
# axis_key: sockets.PhysicalLengthSocketDef()
# for i, axis_key, axis_label in zip(
# range(3),
# ["x_size", "y_size", "z_size"],
# ["X Size", "Y Size", "Z Size"],
# )
# },
} }
output_socket_sets = { output_socket_sets: typ.ClassVar = {
'Maxwell Sources': { 'Maxwell Sources': {
'Sources': sockets.MaxwellSourceSocketDef( 'Sources': sockets.MaxwellSourceSocketDef(
is_list=True, is_list=True,
@ -52,43 +37,69 @@ class CombineNode(base.MaxwellSimNode):
is_list=True, is_list=True,
), ),
}, },
'Real 3D Vector': {
'Real 3D Vector': sockets.Real3DVectorSocketDef(),
},
# "Point 3D": {
# "3D Point": sockets.PhysicalPoint3DSocketDef(),
# },
# "Size 3D": {
# "3D Size": sockets.PhysicalSize3DSocketDef(),
# },
} }
amount: bpy.props.IntProperty( ####################
name='# Objects to Combine', # - Draw
description='Amount of Objects to Combine', ####################
default=1, amount: int = bl_cache.BLField(2, abs_min=1, prop_ui=True)
min=1,
# max=MAX_AMOUNT,
update=lambda self, context: self.on_prop_changed('amount', context),
)
#################### ####################
# - Draw # - Draw
#################### ####################
def draw_props(self, context, layout): def draw_props(self, context, layout):
layout.prop(self, 'amount', text='#') layout.prop(self, self.blfields['amount'], text='')
####################
# - Events
####################
@events.on_value_changed(
# Trigger
prop_name={'active_socket_set', 'amount'},
props={'active_socket_set', 'amount'},
run_on_init=True,
)
def on_inputs_changed(self, props):
if props['active_socket_set'] == 'Maxwell Sources':
if (
not self.loose_input_sockets
or not next(iter(self.loose_input_sockets)).startswith('Source')
or len(self.loose_input_sockets) != props['amount']
):
self.loose_input_sockets = {
f'Source #{i}': sockets.MaxwellSourceSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Structures':
if (
not self.loose_input_sockets
or not next(iter(self.loose_input_sockets)).startswith('Structure')
or len(self.loose_input_sockets) != props['amount']
):
self.loose_input_sockets = {
f'Structure #{i}': sockets.MaxwellStructureSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Monitors':
if (
not self.loose_input_sockets
or not next(iter(self.loose_input_sockets)).startswith('Monitor')
or len(self.loose_input_sockets) != props['amount']
):
self.loose_input_sockets = {
f'Monitor #{i}': sockets.MaxwellMonitorSocketDef()
for i in range(props['amount'])
}
elif self.loose_input_sockets:
self.loose_input_sockets = {}
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket(
'Real 3D Vector', input_sockets={'x_0', 'x_1', 'x_2'}
)
def compute_real_3d_vector(self, input_sockets) -> sp.Expr:
return sp.Matrix([input_sockets[f'x_{i}'] for i in range(3)])
@events.computes_output_socket( @events.computes_output_socket(
'Sources', 'Sources',
kind=ct.FlowKind.Array,
all_loose_input_sockets=True, all_loose_input_sockets=True,
props={'amount'}, props={'amount'},
) )
@ -97,6 +108,7 @@ class CombineNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'Structures', 'Structures',
kind=ct.FlowKind.Array,
all_loose_input_sockets=True, all_loose_input_sockets=True,
props={'amount'}, props={'amount'},
) )
@ -105,45 +117,13 @@ class CombineNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'Monitors', 'Monitors',
kind=ct.FlowKind.Array,
all_loose_input_sockets=True, all_loose_input_sockets=True,
props={'amount'}, props={'amount'},
) )
def compute_monitors(self, loose_input_sockets, props) -> sp.Expr: def compute_monitors(self, loose_input_sockets, props) -> sp.Expr:
return [loose_input_sockets[f'Monitor #{i}'] for i in range(props['amount'])] return [loose_input_sockets[f'Monitor #{i}'] for i in range(props['amount'])]
####################
# - Input Socket Compilation
####################
@events.on_value_changed(
prop_name='active_socket_set',
props={'active_socket_set', 'amount'},
run_on_init=True,
)
def on_value_changed__active_socket_set(self, props):
if props['active_socket_set'] == 'Maxwell Sources':
self.loose_input_sockets = {
f'Source #{i}': sockets.MaxwellSourceSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Structures':
self.loose_input_sockets = {
f'Structure #{i}': sockets.MaxwellStructureSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Monitors':
self.loose_input_sockets = {
f'Monitor #{i}': sockets.MaxwellMonitorSocketDef()
for i in range(props['amount'])
}
else:
self.loose_input_sockets = {}
@events.on_value_changed(
prop_name='amount',
)
def on_value_changed__amount(self):
self.on_value_changed__active_socket_set()
#################### ####################
# - Blender Registration # - Blender Registration
@ -151,4 +131,4 @@ class CombineNode(base.MaxwellSimNode):
BL_REGISTER = [ BL_REGISTER = [
CombineNode, CombineNode,
] ]
BL_NODES = {ct.NodeType.Combine: (ct.NodeCategory.MAXWELLSIM_UTILITIES)} BL_NODES = {ct.NodeType.Combine: (ct.NodeCategory.MAXWELLSIM_SIMS)}

View File

@ -1,3 +1,5 @@
import typing as typ
import sympy as sp import sympy as sp
import tidy3d as td import tidy3d as td
@ -13,9 +15,9 @@ class FDTDSimNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Domain': sockets.MaxwellSimDomainSocketDef(),
'BCs': sockets.MaxwellBoundCondsSocketDef(), 'BCs': sockets.MaxwellBoundCondsSocketDef(),
'Domain': sockets.MaxwellSimDomainSocketDef(),
'Sources': sockets.MaxwellSourceSocketDef( 'Sources': sockets.MaxwellSourceSocketDef(
is_list=True, is_list=True,
), ),
@ -26,32 +28,33 @@ class FDTDSimNode(base.MaxwellSimNode):
is_list=True, is_list=True,
), ),
} }
output_sockets = { output_sockets: typ.ClassVar = {
'FDTD Sim': sockets.MaxwellFDTDSimSocketDef(), 'Sim': sockets.MaxwellFDTDSimSocketDef(),
} }
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'FDTD Sim', 'Sim',
kind=ct.FlowKind.Value, kind=ct.FlowKind.Value,
input_sockets={'Sources', 'Structures', 'Domain', 'BCs', 'Monitors'}, input_sockets={'Sources', 'Structures', 'Domain', 'BCs', 'Monitors'},
input_socket_kinds={
'Sources': ct.FlowKind.Array,
'Structures': ct.FlowKind.Array,
'Domain': ct.FlowKind.Value,
'BCs': ct.FlowKind.Value,
'Monitors': ct.FlowKind.Array,
},
) )
def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr: def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr:
## TODO: Visualize the boundary conditions on top of the sim domain
sim_domain = input_sockets['Domain'] sim_domain = input_sockets['Domain']
sources = input_sockets['Sources'] sources = input_sockets['Sources']
structures = input_sockets['Structures'] structures = input_sockets['Structures']
bounds = input_sockets['BCs'] bounds = input_sockets['BCs']
monitors = input_sockets['Monitors'] monitors = input_sockets['Monitors']
# if not isinstance(sources, list):
# sources = [sources]
# if not isinstance(structures, list):
# structures = [structures]
# if not isinstance(monitors, list):
# monitors = [monitors]
return td.Simulation( return td.Simulation(
**sim_domain, ## run_time=, size=, grid=, medium= **sim_domain, ## run_time=, size=, grid=, medium=
structures=structures, structures=structures,

View File

@ -4,11 +4,15 @@ import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
from .. import base, events from .. import base, events
log = logger.get(__name__)
class SimDomainNode(base.MaxwellSimNode): class SimDomainNode(base.MaxwellSimNode):
node_type = ct.NodeType.SimDomain node_type = ct.NodeType.SimDomain
@ -16,12 +20,27 @@ class SimDomainNode(base.MaxwellSimNode):
use_sim_node_name = True use_sim_node_name = True
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Duration': sockets.PhysicalTimeSocketDef( 'Duration': sockets.ExprSocketDef(
default_value=5 * spu.ps, physical_type=spux.PhysicalType.Time,
default_unit=spu.ps, default_unit=spu.picosecond,
default_value=5,
abs_min=0,
),
'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
'Size': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([1, 1, 1]),
abs_min=0.001,
), ),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(),
'Grid': sockets.MaxwellSimGridSocketDef(), 'Grid': sockets.MaxwellSimGridSocketDef(),
'Ambient Medium': sockets.MaxwellMediumSocketDef(), 'Ambient Medium': sockets.MaxwellMediumSocketDef(),
} }
@ -34,44 +53,6 @@ class SimDomainNode(base.MaxwellSimNode):
'modifier': managed_objs.ManagedBLModifier, 'modifier': managed_objs.ManagedBLModifier,
} }
####################
# - Events
####################
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_input_changed(
self,
props: dict,
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.SimulationSimDomain),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
#################### ####################
# - Outputs # - Outputs
#################### ####################
@ -94,6 +75,59 @@ class SimDomainNode(base.MaxwellSimNode):
'medium': input_sockets['Ambient Medium'], 'medium': input_sockets['Ambient Medium'],
} }
####################
# - Preview
####################
@events.on_value_changed(
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
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
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_input_changed(
self,
managed_objs,
input_sockets,
unit_systems,
):
mesh = managed_objs['mesh']
modifier = managed_objs['modifier']
center = input_sockets['Center']
size = input_sockets['Size']
unit_system = unit_systems['BlenderUnits']
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES',
{
'node_group': import_geonodes(GeoNodes.SimulationSimDomain),
'inputs': {'Size': size},
'unit_system': unit_system,
},
)
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -1,15 +1,18 @@
# from . import uniform_current_source from . import (
from . import plane_wave_source, point_dipole_source, temporal_shapes # astigmatic_gaussian_beam_source,
# gaussian_beam_source,
# from . import gaussian_beam_source # plane_wave_source,
# from . import astigmatic_gaussian_beam_source point_dipole_source,
# from . import tfsf_source temporal_shapes,
# tfsf_source,
# uniform_current_source,
)
BL_REGISTER = [ BL_REGISTER = [
*temporal_shapes.BL_REGISTER, *temporal_shapes.BL_REGISTER,
*point_dipole_source.BL_REGISTER, *point_dipole_source.BL_REGISTER,
# *uniform_current_source.BL_REGISTER, # *uniform_current_source.BL_REGISTER,
*plane_wave_source.BL_REGISTER, # *plane_wave_source.BL_REGISTER,
# *gaussian_beam_source.BL_REGISTER, # *gaussian_beam_source.BL_REGISTER,
# *astigmatic_gaussian_beam_source.BL_REGISTER, # *astigmatic_gaussian_beam_source.BL_REGISTER,
# *tfsf_source.BL_REGISTER, # *tfsf_source.BL_REGISTER,
@ -18,7 +21,7 @@ BL_NODES = {
**temporal_shapes.BL_NODES, **temporal_shapes.BL_NODES,
**point_dipole_source.BL_NODES, **point_dipole_source.BL_NODES,
# **uniform_current_source.BL_NODES, # **uniform_current_source.BL_NODES,
**plane_wave_source.BL_NODES, # **plane_wave_source.BL_NODES,
# **gaussian_beam_source.BL_NODES, # **gaussian_beam_source.BL_NODES,
# **astigmatic_gaussian_beam_source.BL_NODES, # **astigmatic_gaussian_beam_source.BL_NODES,
# **tfsf_source.BL_NODES, # **tfsf_source.BL_NODES,

View File

@ -1,64 +1,62 @@
import typing as typ import typing as typ
import bpy import bpy
import sympy as sp
import tidy3d as td import tidy3d as td
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
from .. import base, events from .. import base, events
log = logger.get(__name__)
class PointDipoleSourceNode(base.MaxwellSimNode): class PointDipoleSourceNode(base.MaxwellSimNode):
node_type = ct.NodeType.PointDipoleSource node_type = ct.NodeType.PointDipoleSource
bl_label = 'Point Dipole Source' bl_label = 'Point Dipole Source'
use_sim_node_name = True
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(), 'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([0, 0, 0]),
),
'Interpolate': sockets.BoolSocketDef( 'Interpolate': sockets.BoolSocketDef(
default_value=True, default_value=True,
), ),
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Source': sockets.MaxwellSourceSocketDef(), 'Source': sockets.MaxwellSourceSocketDef(),
} }
managed_obj_types = { managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh, 'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
} }
#################### ####################
# - Properties # - Properties
#################### ####################
pol_axis: bpy.props.EnumProperty( pol_axis: ct.SimSpaceAxis = bl_cache.BLField(ct.SimSpaceAxis.X, prop_ui=True)
name='Polarization Axis',
description='Polarization Axis',
items=[
('EX', 'Ex', 'Electric field in x-dir'),
('EY', 'Ey', 'Electric field in y-dir'),
('EZ', 'Ez', 'Electric field in z-dir'),
],
default='EX',
update=(lambda self, context: self.on_prop_changed('pol_axis', context)),
)
#################### ####################
# - UI # - UI
#################### ####################
def draw_props(self, context, layout): def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
split = layout.split(factor=0.6) layout.prop(self, self.blfields['pol_axis'], expand=True)
col = split.column()
col.label(text='Pol Axis')
col = split.column()
col.prop(self, 'pol_axis', text='')
#################### ####################
# - Output Socket Computation # - Outputs
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Source', 'Source',
@ -76,9 +74,9 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
unit_systems: dict, unit_systems: dict,
) -> td.PointDipole: ) -> td.PointDipole:
pol_axis = { pol_axis = {
'EX': 'Ex', ct.SimSpaceAxis.X: 'Ex',
'EY': 'Ey', ct.SimSpaceAxis.Y: 'Ey',
'EZ': 'Ez', ct.SimSpaceAxis.Z: 'Ez',
}[props['pol_axis']] }[props['pol_axis']]
return td.PointDipole( return td.PointDipole(
@ -88,39 +86,59 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
polarization=pol_axis, polarization=pol_axis,
) )
##################### ####################
## - Preview # - Preview
##################### ####################
# @events.on_value_changed( @events.on_value_changed(
# socket_name='Center', prop_name='preview_active',
# input_sockets={'Center'}, run_on_init=True,
# managed_objs={'sphere_empty'}, props={'preview_active'},
# ) managed_objs={'mesh'},
# def on_value_changed__center( )
# self, def on_preview_changed(self, props, managed_objs) -> None:
# input_sockets: dict, """Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
# managed_objs: dict[str, ct.schemas.ManagedObj], mesh = managed_objs['mesh']
# ):
# _center = input_sockets['Center']
# center = tuple(spu.convert_to(_center, spu.um) / spu.um)
# ## TODO: Preview unit system?? Presume um for now
# mobj = managed_objs['sphere_empty'] # Push Preview State to Managed Mesh
# bl_object = mobj.bl_object('EMPTY') if props['preview_active']:
# bl_object.location = center # tuple([float(el) for el in center]) mesh.show_preview()
else:
mesh.hide_preview()
# @events.on_show_preview( @events.on_value_changed(
# managed_objs={'sphere_empty'}, socket_name={'Center'},
# ) prop_name='pol_axis',
# def on_show_preview( run_on_init=True,
# self, # Pass Data
# managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs={'mesh', 'modifier'},
# ): props={'pol_axis'},
# managed_objs['sphere_empty'].show_preview( input_sockets={'Center'},
# 'EMPTY', unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
# empty_display_type='SPHERE', scale_input_sockets={'Center': 'BlenderUnits'},
# ) )
# managed_objs['sphere_empty'].bl_object('EMPTY').empty_display_size = 0.2 def on_inputs_changed(
self, managed_objs, props, input_sockets, unit_systems
) -> None:
mesh = managed_objs['mesh']
modifier = managed_objs['modifier']
center = input_sockets['Center']
unit_system = unit_systems['BlenderUnits']
axis = {
ct.SimSpaceAxis.X: 0,
ct.SimSpaceAxis.Y: 1,
ct.SimSpaceAxis.Z: 2,
}[props['pol_axis']]
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES',
{
'node_group': import_geonodes(GeoNodes.SourcePointDipole),
'inputs': {'Axis': axis},
'unit_system': unit_system,
},
)
#################### ####################

View File

@ -1,15 +1,13 @@
from . import gaussian_pulse_temporal_shape # from . import expr_temporal_shape, pulse_temporal_shape, wave_temporal_shape
from . import pulse_temporal_shape, wave_temporal_shape
# from . import continuous_wave_temporal_shape
# from . import array_temporal_shape
BL_REGISTER = [ BL_REGISTER = [
*gaussian_pulse_temporal_shape.BL_REGISTER, *pulse_temporal_shape.BL_REGISTER,
# *continuous_wave_temporal_shape.BL_REGISTER, *wave_temporal_shape.BL_REGISTER,
# *array_temporal_shape.BL_REGISTER, # *expr_temporal_shape.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**gaussian_pulse_temporal_shape.BL_NODES, **pulse_temporal_shape.BL_NODES,
# **continuous_wave_temporal_shape.BL_NODES, **wave_temporal_shape.BL_NODES,
# **array_temporal_shape.BL_NODES, # **expr_temporal_shape.BL_NODES,
} }

View File

@ -1,75 +0,0 @@
import sympy.physics.units as spu
import tidy3d as td
from .... import contracts, sockets
from ... import base, events
class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.ContinuousWaveTemporalShape
bl_label = 'Continuous Wave Temporal Shape'
# bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
# "amplitude": sockets.RealNumberSocketDef(
# label="Temporal Shape",
# ), ## Should have a unit of some kind...
'phase': sockets.PhysicalAngleSocketDef(
label='Phase',
),
'freq_center': sockets.PhysicalFreqSocketDef(
label='Freq Center',
),
'freq_std': sockets.PhysicalFreqSocketDef(
label='Freq STD',
),
'time_delay_rel_ang_freq': sockets.RealNumberSocketDef(
label='Time Delay rel. Ang. Freq',
default_value=5.0,
),
}
output_sockets = {
'temporal_shape': sockets.MaxwellTemporalShapeSocketDef(
label='Temporal Shape',
),
}
####################
# - Output Socket Computation
####################
@events.computes_output_socket('temporal_shape')
def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole:
_phase = self.compute_input('phase')
_freq_center = self.compute_input('freq_center')
_freq_std = self.compute_input('freq_std')
time_delay_rel_ang_freq = self.compute_input('time_delay_rel_ang_freq')
cheating_amplitude = 1.0
phase = spu.convert_to(_phase, spu.radian) / spu.radian
freq_center = spu.convert_to(_freq_center, spu.hertz) / spu.hertz
freq_std = spu.convert_to(_freq_std, spu.hertz) / spu.hertz
return td.ContinuousWave(
amplitude=cheating_amplitude,
phase=phase,
freq0=freq_center,
fwidth=freq_std,
offset=time_delay_rel_ang_freq,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
ContinuousWaveTemporalShapeNode,
]
BL_NODES = {
contracts.NodeType.ContinuousWaveTemporalShape: (
contracts.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES
)
}

View File

@ -1,149 +0,0 @@
import bpy
import numpy as np
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spuex
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.GaussianPulseTemporalShape
bl_label = 'Gaussian Pulse Temporal Shape'
# bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
# "amplitude": sockets.RealNumberSocketDef(
# label="Temporal Shape",
# ), ## Should have a unit of some kind...
'Freq Center': sockets.PhysicalFreqSocketDef(
default_value=500 * spuex.terahertz,
),
'Freq Std.': sockets.PhysicalFreqSocketDef(
default_value=200 * spuex.terahertz,
),
'Phase': sockets.PhysicalAngleSocketDef(),
'Delay rel. AngFreq': sockets.RealNumberSocketDef(
default_value=5.0,
),
'Remove DC': sockets.BoolSocketDef(
default_value=True,
),
}
output_sockets = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
}
managed_obj_types = {
'amp_time': managed_objs.ManagedBLImage,
}
####################
# - Properties
####################
plot_time_start: bpy.props.FloatProperty(
name='Plot Time Start (ps)',
description='The instance ID of a particular MaxwellSimNode instance, used to index caches',
default=0.0,
update=(lambda self, context: self.on_prop_changed('plot_time_start', context)),
)
plot_time_end: bpy.props.FloatProperty(
name='Plot Time End (ps)',
description='The instance ID of a particular MaxwellSimNode instance, used to index caches',
default=5,
update=(lambda self, context: self.on_prop_changed('plot_time_start', context)),
)
####################
# - UI
####################
def draw_props(self, _, layout):
layout.label(text='Plot Settings')
split = layout.split(factor=0.6)
col = split.column()
col.label(text='t-Range (ps)')
col = split.column()
col.prop(self, 'plot_time_start', text='')
col.prop(self, 'plot_time_end', text='')
####################
# - Output Socket Computation
####################
@events.computes_output_socket(
'Temporal Shape',
input_sockets={
'Freq Center',
'Freq Std.',
'Phase',
'Delay rel. AngFreq',
'Remove DC',
},
)
def compute_source(self, input_sockets: dict) -> td.GaussianPulse:
if (
(_freq_center := input_sockets['Freq Center']) is None
or (_freq_std := input_sockets['Freq Std.']) is None
or (_phase := input_sockets['Phase']) is None
or (time_delay_rel_ang_freq := input_sockets['Delay rel. AngFreq']) is None
or (remove_dc_component := input_sockets['Remove DC']) is None
):
raise ValueError('Inputs not defined')
cheating_amplitude = 1.0
freq_center = spu.convert_to(_freq_center, spu.hertz) / spu.hertz
freq_std = spu.convert_to(_freq_std, spu.hertz) / spu.hertz
phase = spu.convert_to(_phase, spu.radian) / spu.radian
return td.GaussianPulse(
amplitude=cheating_amplitude,
phase=phase,
freq0=freq_center,
fwidth=freq_std,
offset=time_delay_rel_ang_freq,
remove_dc_component=remove_dc_component,
)
@events.on_show_plot(
managed_objs={'amp_time'},
props={'plot_time_start', 'plot_time_end'},
output_sockets={'Temporal Shape'},
stop_propagation=True,
)
def on_show_plot(
self,
managed_objs: dict,
output_sockets: dict,
props: dict,
):
temporal_shape = output_sockets['Temporal Shape']
plot_time_start = props['plot_time_start'] * 1e-15
plot_time_end = props['plot_time_end'] * 1e-15
times = np.linspace(plot_time_start, plot_time_end)
managed_objs['amp_time'].mpl_plot_to_image(
lambda ax: temporal_shape.plot_spectrum(times, ax=ax),
bl_select=True,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
GaussianPulseTemporalShapeNode,
]
BL_NODES = {
ct.NodeType.GaussianPulseTemporalShape: (
ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES
)
}

View File

@ -0,0 +1,161 @@
"""Implements the `PulseTemporalShapeNode`."""
import functools
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
class PulseTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.PulseTemporalShape
bl_label = 'Gaussian Pulse Temporal Shape'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'max E': sockets.ExprSocketDef(
mathtype=spux.MathType.Complex,
physical_type=spux.PhysicalType.EField,
default_value=1 + 0j,
),
'μ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=500,
),
'σ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=200,
),
'Offset Time': sockets.ExprSocketDef(default_value=5, abs_min=2.5),
'Remove DC': sockets.BoolSocketDef(
default_value=True,
),
}
output_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'E(t)': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
####################
# - UI
####################
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.3, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Off t:')
## RHS: Parameter Units
col = split.column()
col.label(text='1 / 2π·σ(𝑓)')
####################
# - FlowKind: Value
####################
@events.computes_output_socket(
'Temporal Shape',
input_sockets={
'max E',
'μ Freq',
'σ Freq',
'Offset Time',
'Remove DC',
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'max E': 'Tidy3DUnits',
'μ Freq': 'Tidy3DUnits',
'σ Freq': 'Tidy3DUnits',
},
)
def compute_temporal_shape(self, input_sockets, unit_systems) -> td.GaussianPulse:
return td.GaussianPulse(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
remove_dc_component=input_sockets['Remove DC'],
)
####################
# - FlowKind: LazyValueFunc / Info / Params
####################
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.LazyValueFunc,
output_sockets={'Temporal Shape'},
)
def compute_time_to_efield_lazy(self, output_sockets) -> td.GaussianPulse:
temporal_shape = output_sockets['Temporal Shape']
jax_amp_time = functools.partial(ct.manual_amp_time, temporal_shape)
## TODO: Don't just partial() it up, do it property in the ParamsFlow!
## -> Right now it's recompiled every time.
return ct.LazyValueFuncFlow(
func=jax_amp_time,
func_args=[spux.PhysicalType.Time],
supports_jax=True,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Info,
)
def compute_time_to_efield_info(self) -> td.GaussianPulse:
return ct.InfoFlow(
dim_names=['t'],
dim_idx={
't': ct.LazyArrayRangeFlow(
start=sp.S(0), stop=sp.oo, steps=0, unit=spu.second
)
},
output_name='E',
output_shape=None,
output_mathtype=spux.MathType.Complex,
output_unit=spu.volt / spu.um,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Params,
)
def compute_time_to_efield_params(self) -> td.GaussianPulse:
sym_time = sp.Symbol('t', real=True, nonnegative=True)
return ct.ParamsFlow(func_args=[sym_time], symbols={sym_time})
####################
# - Blender Registration
####################
BL_REGISTER = [
PulseTemporalShapeNode,
]
BL_NODES = {
ct.NodeType.PulseTemporalShape: (ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES)
}

View File

@ -0,0 +1,156 @@
"""Implements the `WaveTemporalShapeNode`."""
import functools
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
class WaveTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.WaveTemporalShape
bl_label = 'Continuous Wave Temporal Shape'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'max E': sockets.ExprSocketDef(
mathtype=spux.MathType.Complex,
physical_type=spux.PhysicalType.EField,
default_value=1 + 0j,
),
'μ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=500,
),
'σ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=200,
),
'Offset Time': sockets.ExprSocketDef(default_value=5, abs_min=2.5),
}
output_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'E(t)': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
####################
# - UI
####################
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.3, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Off t:')
## RHS: Parameter Units
col = split.column()
col.label(text='1 / 2π·σ(𝑓)')
####################
# - FlowKind: Value
####################
@events.computes_output_socket(
'Temporal Shape',
input_sockets={
'max E',
'μ Freq',
'σ Freq',
'Offset Time',
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'max E': 'Tidy3DUnits',
'μ Freq': 'Tidy3DUnits',
'σ Freq': 'Tidy3DUnits',
},
)
def compute_temporal_shape(self, input_sockets, unit_systems) -> td.GaussianPulse:
return td.ContinuousWave(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
)
####################
# - FlowKind: LazyValueFunc / Info / Params
####################
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.LazyValueFunc,
output_sockets={'Temporal Shape'},
)
def compute_time_to_efield_lazy(self, output_sockets) -> td.GaussianPulse:
temporal_shape = output_sockets['Temporal Shape']
jax_amp_time = functools.partial(ct.manual_amp_time, temporal_shape)
## TODO: Don't just partial() it up, do it property in the ParamsFlow!
## -> Right now it's recompiled every time.
return ct.LazyValueFuncFlow(
func=jax_amp_time,
func_args=[spux.PhysicalType.Time],
supports_jax=True,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Info,
)
def compute_time_to_efield_info(self) -> td.GaussianPulse:
return ct.InfoFlow(
dim_names=['t'],
dim_idx={
't': ct.LazyArrayRangeFlow(
start=sp.S(0), stop=sp.oo, steps=0, unit=spu.second
)
},
output_name='E',
output_shape=None,
output_mathtype=spux.MathType.Complex,
output_unit=spu.volt / spu.um,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Params,
)
def compute_time_to_efield_params(self) -> td.GaussianPulse:
sym_time = sp.Symbol('t', real=True, nonnegative=True)
return ct.ParamsFlow(func_args=[sym_time], symbols={sym_time})
####################
# - Blender Registration
####################
BL_REGISTER = [
WaveTemporalShapeNode,
]
BL_NODES = {
ct.NodeType.WaveTemporalShape: (ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES)
}

View File

@ -1,8 +1,12 @@
import typing as typ import typing as typ
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from blender_maxwell.utils import bl_cache, logger from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from ... import bl_socket_map, managed_objs, sockets from ... import bl_socket_map, managed_objs, sockets
from ... import contracts as ct from ... import contracts as ct
@ -22,7 +26,13 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'GeoNodes': sockets.BlenderGeoNodesSocketDef(), 'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(), 'Structure': sockets.MaxwellStructureSocketDef(),
@ -34,7 +44,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
} }
#################### ####################
# - Output # - Outputs
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Structure', 'Structure',
@ -43,14 +53,10 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
) )
def compute_structure( def compute_structure(
self, self,
input_sockets: dict, input_sockets,
managed_objs: dict, managed_objs,
) -> td.Structure: ) -> td.Structure:
"""Computes a triangle-mesh based Tidy3D structure, by manually copying mesh data from Blender to a `td.TriangleMesh`.""" """Computes a triangle-mesh based Tidy3D structure, by manually copying mesh data from Blender to a `td.TriangleMesh`."""
# Simulate Input Value Change
## This ensures that the mesh has been re-computed.
self.on_input_socket_changed()
## TODO: mesh_as_arrays might not take the Center into account. ## TODO: mesh_as_arrays might not take the Center into account.
## - Alternatively, Tidy3D might have a way to transform? ## - Alternatively, Tidy3D might have a way to transform?
mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays
@ -68,20 +74,12 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
@events.on_value_changed( @events.on_value_changed(
prop_name='preview_active', prop_name='preview_active',
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center'},
managed_objs={'mesh'}, managed_objs={'mesh'},
) )
def on_preview_changed(self, props, input_sockets) -> None: def on_preview_changed(self, props) -> None:
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen.""" """Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh'] mesh = managed_objs['mesh']
# No Mesh: Create Empty Object
## Ensures that when there is mesh data, it'll be correctly previewed.
## Bit of a workaround - the idea is usually to make the MObj as needed.
if not mesh.exists:
center = input_sockets['Center']
_ = mesh.bl_object(location=center)
# Push Preview State to Managed Mesh # Push Preview State to Managed Mesh
if props['preview_active']: if props['preview_active']:
mesh.show_preview() mesh.show_preview()
@ -139,8 +137,8 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
) )
def on_input_changed( def on_input_changed(
self, self,
managed_objs: dict, managed_objs,
input_sockets: dict, input_sockets,
) -> None: ) -> None:
"""Declares new loose input sockets in response to a new GeoNodes tree (if any).""" """Declares new loose input sockets in response to a new GeoNodes tree (if any)."""
geonodes = input_sockets['GeoNodes'] geonodes = input_sockets['GeoNodes']

View File

@ -5,11 +5,15 @@ import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import managed_objs, sockets
from ... import base, events from ... import base, events
log = logger.get(__name__)
class BoxStructureNode(base.MaxwellSimNode): class BoxStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoxStructure node_type = ct.NodeType.BoxStructure
@ -21,9 +25,20 @@ class BoxStructureNode(base.MaxwellSimNode):
#################### ####################
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.ExprSocketDef(
'Size': sockets.PhysicalSize3DSocketDef( shape=(3,),
default_value=sp.Matrix([500, 500, 500]) * spu.nm mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
'Size': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.nanometer,
default_value=sp.Matrix([500, 500, 500]),
abs_min=0.001,
), ),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
@ -36,7 +51,7 @@ class BoxStructureNode(base.MaxwellSimNode):
} }
#################### ####################
# - Event Methods # - Outputs
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Structure', 'Structure',
@ -47,7 +62,7 @@ class BoxStructureNode(base.MaxwellSimNode):
'Size': 'Tidy3DUnits', 'Size': 'Tidy3DUnits',
}, },
) )
def compute_structure(self, input_sockets: dict, unit_systems: dict) -> td.Box: def compute_structure(self, input_sockets, unit_systems) -> td.Box:
return td.Structure( return td.Structure(
geometry=td.Box( geometry=td.Box(
center=input_sockets['Center'], center=input_sockets['Center'],
@ -56,11 +71,27 @@ class BoxStructureNode(base.MaxwellSimNode):
medium=input_sockets['Medium'], medium=input_sockets['Medium'],
) )
####################
# - Preview
####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active', prop_name='preview_active',
run_on_init=True, run_on_init=True,
props={'preview_active'}, props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
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(
socket_name={'Center', 'Size'},
run_on_init=True,
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
@ -70,26 +101,26 @@ class BoxStructureNode(base.MaxwellSimNode):
) )
def on_inputs_changed( def on_inputs_changed(
self, self,
props: dict, managed_objs,
managed_objs: dict, input_sockets,
input_sockets: dict, unit_systems,
unit_systems: dict,
): ):
# Push Input Values to GeoNodes Modifier mesh = managed_objs['mesh']
managed_objs['modifier'].bl_modifier( modifier = managed_objs['modifier']
managed_objs['mesh'].bl_object(location=input_sockets['Center']), center = input_sockets['Center']
size = input_sockets['Size']
unit_system = unit_systems['BlenderUnits']
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES', 'NODES',
{ {
'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox), 'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
'unit_system': unit_systems['BlenderUnits'], 'inputs': {'Size': size},
'inputs': { 'unit_system': unit_system,
'Size': input_sockets['Size'],
},
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
#################### ####################

View File

@ -1,14 +1,19 @@
import typing as typ import typing as typ
import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import managed_objs, sockets
from ... import base, events from ... import base, events
log = logger.get(__name__)
class SphereStructureNode(base.MaxwellSimNode): class SphereStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.SphereStructure node_type = ct.NodeType.SphereStructure
@ -20,9 +25,17 @@ class SphereStructureNode(base.MaxwellSimNode):
#################### ####################
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.ExprSocketDef(
'Radius': sockets.PhysicalLengthSocketDef( shape=(3,),
default_value=150 * spu.nm, mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
'Radius': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Length,
default_unit=spu.nanometer,
default_value=150,
), ),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
@ -35,7 +48,7 @@ class SphereStructureNode(base.MaxwellSimNode):
} }
#################### ####################
# - Output Socket Computation # - Outputs
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Structure', 'Structure',
@ -46,7 +59,7 @@ class SphereStructureNode(base.MaxwellSimNode):
'Radius': 'Tidy3DUnits', 'Radius': 'Tidy3DUnits',
}, },
) )
def compute_structure(self, input_sockets: dict) -> td.Box: def compute_structure(self, input_sockets, unit_systems) -> td.Box:
return td.Structure( return td.Structure(
geometry=td.Sphere( geometry=td.Sphere(
radius=input_sockets['Radius'], radius=input_sockets['Radius'],
@ -56,43 +69,55 @@ class SphereStructureNode(base.MaxwellSimNode):
) )
#################### ####################
# - Preview - Changes to Input Sockets # - Preview
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Radius'},
prop_name='preview_active', prop_name='preview_active',
run_on_init=True, run_on_init=True,
props={'preview_active'}, props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
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(
socket_name={'Center', 'Radius'},
run_on_init=True,
input_sockets={'Center', 'Radius'}, input_sockets={'Center', 'Radius'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={
'Center': 'Tidy3DUnits', 'Center': 'BlenderUnits',
'Radius': 'Tidy3DUnits',
}, },
) )
def on_inputs_changed( def on_inputs_changed(
self, self,
props: dict, managed_objs,
managed_objs: dict, input_sockets,
input_sockets: dict, unit_systems,
unit_systems: dict,
): ):
# Push Input Values to GeoNodes Modifier mesh = managed_objs['mesh']
managed_objs['modifier'].bl_modifier( modifier = managed_objs['modifier']
managed_objs['mesh'].bl_object(location=input_sockets['Center']), center = input_sockets['Center']
radius = input_sockets['Radius']
unit_system = unit_systems['BlenderUnits']
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES', 'NODES',
{ {
'node_group': import_geonodes(GeoNodes.StructurePrimitiveSphere), 'node_group': import_geonodes(GeoNodes.StructurePrimitiveSphere),
'unit_system': unit_systems['BlenderUnits'], 'inputs': {'Radius': radius},
'inputs': { 'unit_system': unit_system,
'Radius': input_sockets['Radius'],
},
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
#################### ####################

View File

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

View File

@ -1,80 +0,0 @@
import scipy as sc
import sympy as sp
import sympy.physics.units as spu
from .... import contracts, sockets
from ... import base, events
class WaveConverterNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.WaveConverter
bl_label = 'Wave Converter'
# bl_icon = ...
####################
# - Sockets
####################
input_sockets = {}
input_socket_sets = {
'freq_to_vacwl': {
'freq': sockets.PhysicalFreqSocketDef(
label='Freq',
),
},
'vacwl_to_freq': {
'vacwl': sockets.PhysicalVacWLSocketDef(
label='Vac WL',
),
},
}
output_sockets = {}
output_socket_sets = {
'freq_to_vacwl': {
'vacwl': sockets.PhysicalVacWLSocketDef(
label='Vac WL',
),
},
'vacwl_to_freq': {
'freq': sockets.PhysicalFreqSocketDef(
label='Freq',
),
},
}
####################
# - Output Socket Computation
####################
@events.computes_output_socket('freq')
def compute_freq(self: contracts.NodeTypeProtocol) -> sp.Expr:
vac_speed_of_light = sc.constants.speed_of_light * spu.meter / spu.second
vacwl = self.compute_input('vacwl')
return spu.convert_to(
vac_speed_of_light / vacwl,
spu.hertz,
)
@events.computes_output_socket('vacwl')
def compute_vacwl(self: contracts.NodeTypeProtocol) -> sp.Expr:
vac_speed_of_light = sc.constants.speed_of_light * spu.meter / spu.second
freq = self.compute_input('freq')
return spu.convert_to(
vac_speed_of_light / freq,
spu.meter,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
WaveConverterNode,
]
BL_NODES = {
contracts.NodeType.WaveConverter: (
contracts.NodeCategory.MAXWELLSIM_UTILITIES_CONVERTERS
)
}

View File

@ -38,6 +38,14 @@ class SocketDef(pyd.BaseModel, abc.ABC):
""" """
bl_socket.reset_instance_id() bl_socket.reset_instance_id()
def postinit(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Pre-initialize a real Blender node socket from this socket definition.
Parameters:
bl_socket: The Blender node socket to alter using data from this SocketDef.
"""
bl_socket.initializing = False
@abc.abstractmethod @abc.abstractmethod
def init(self, bl_socket: bpy.types.NodeSocket) -> None: def init(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Initializes a real Blender node socket from this socket definition. """Initializes a real Blender node socket from this socket definition.
@ -195,6 +203,9 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
## Identifiers ## Identifiers
cls.bl_idname: str = str(cls.socket_type.value) cls.bl_idname: str = str(cls.socket_type.value)
cls.set_prop('instance_id', bpy.props.StringProperty, no_update=True) cls.set_prop('instance_id', bpy.props.StringProperty, no_update=True)
cls.set_prop(
'initializing', bpy.props.BoolProperty, default=True, no_update=True
)
## Special States ## Special States
cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False) cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False)
@ -217,9 +228,14 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Called by `self.on_prop_changed()` when `self.active_kind` was changed. Called by `self.on_prop_changed()` when `self.active_kind` was changed.
""" """
self.display_shape = ( self.display_shape = (
'SQUARE' if self.active_kind == ct.FlowKind.LazyArrayRange else 'CIRCLE' 'SQUARE'
) # + ('_DOT' if self.use_units else '') if self.active_kind == ct.FlowKind.LazyArrayRange
## TODO: Valid Active Kinds should be a subset/subenum(?) of FlowKind else ('DIAMOND' if self.active_kind == ct.FlowKind.Array else 'CIRCLE')
) + (
'_DOT'
if hasattr(self, 'physical_type') and self.physical_type is not None
else ''
)
def on_socket_prop_changed(self, prop_name: str) -> None: def on_socket_prop_changed(self, prop_name: str) -> None:
"""Called when a property has been updated. """Called when a property has been updated.
@ -244,6 +260,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Attributes: Attributes:
prop_name: The name of the property that was changed. prop_name: The name of the property that was changed.
""" """
## TODO: Evaluate this properly
if self.initializing:
return
if hasattr(self, prop_name): if hasattr(self, prop_name):
# Invalidate UI BLField Caches # Invalidate UI BLField Caches
if prop_name in self.ui_blfields: if prop_name in self.ui_blfields:
@ -532,7 +552,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to set "ct.FlowKind.Value", but socket does not define it' msg = f'Socket {self.bl_label} {self.socket_type}): Tried to set "ct.FlowKind.Value", but socket does not define it'
raise NotImplementedError(msg) raise NotImplementedError(msg)
# ValueArray # Array
@property @property
def array(self) -> ct.ArrayFlow: def array(self) -> ct.ArrayFlow:
"""Throws a descriptive error. """Throws a descriptive error.
@ -811,6 +831,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
{ {
ct.FlowKind.Value: self.draw_value, ct.FlowKind.Value: self.draw_value,
ct.FlowKind.LazyArrayRange: self.draw_lazy_array_range, ct.FlowKind.LazyArrayRange: self.draw_lazy_array_range,
ct.FlowKind.Array: self.draw_array,
}[self.active_kind](col) }[self.active_kind](col)
# Info Drawing # Info Drawing
@ -930,6 +951,16 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
col: Target for defining UI elements. col: Target for defining UI elements.
""" """
def draw_array(self, col: bpy.types.UILayout) -> None:
"""Draws the socket array UI on its own line.
Notes:
Should be overriden by individual socket classes, if they have an editable `FlowKind.Array`.
Parameters:
col: Target for defining UI elements.
"""
#################### ####################
# - UI Methods: Auxilliary # - UI Methods: Auxilliary
#################### ####################

View File

@ -194,10 +194,19 @@ class ExprBLSocket(base.MaxwellSimSocket):
current_value = self.value current_value = self.value
current_lazy_array_range = self.lazy_array_range current_lazy_array_range = self.lazy_array_range
self.unit = bl_cache.Signal.InvalidateCache # Old Unit Not in Physical Type
## -> This happens when dynamically altering self.physical_type
if self.unit in self.physical_type.valid_units:
self.unit = bl_cache.Signal.InvalidateCache
self.value = current_value self.value = current_value
self.lazy_array_range = current_lazy_array_range self.lazy_array_range = current_lazy_array_range
else:
self.unit = bl_cache.Signal.InvalidateCache
# Workaround: Manually Jiggle FlowKind Invalidation
self.value = self.value
self.lazy_value_range = self.lazy_value_range
#################### ####################
# - Property Callback # - Property Callback
@ -238,10 +247,25 @@ class ExprBLSocket(base.MaxwellSimSocket):
return mathtype, shape return mathtype, shape
def _to_raw_value(self, expr: spux.SympyExpr): def _to_raw_value(self, expr: spux.SympyExpr, force_complex: bool = False):
if self.unit is not None: if self.unit is not None:
return spux.sympy_to_python(spux.scale_to_unit(expr, self.unit)) pyvalue = spux.sympy_to_python(spux.scale_to_unit(expr, self.unit))
return spux.sympy_to_python(expr) else:
pyvalue = spux.sympy_to_python(expr)
# Cast complex -> tuple[float, float]
if isinstance(pyvalue, complex) or (
isinstance(pyvalue, int | float) and force_complex
):
return (pyvalue.real, pyvalue.imag)
if isinstance(pyvalue, tuple) and all(
isinstance(v, complex)
or (isinstance(pyvalue, int | float) and force_complex)
for v in pyvalue
):
return tuple([(v.real, v.imag) for v in pyvalue])
return pyvalue
def _parse_expr_str(self, expr_spstr: str) -> None: def _parse_expr_str(self, expr_spstr: str) -> None:
expr = sp.sympify( expr = sp.sympify(
@ -346,7 +370,9 @@ class ExprBLSocket(base.MaxwellSimSocket):
elif self.mathtype == MT_R: elif self.mathtype == MT_R:
self.raw_value_float = self._to_raw_value(expr) self.raw_value_float = self._to_raw_value(expr)
elif self.mathtype == MT_C: elif self.mathtype == MT_C:
self.raw_value_complex = self._to_raw_value(expr) self.raw_value_complex = self._to_raw_value(
expr, force_complex=True
)
elif self.shape == (2,): elif self.shape == (2,):
if self.mathtype == MT_Z: if self.mathtype == MT_Z:
self.raw_value_int2 = self._to_raw_value(expr) self.raw_value_int2 = self._to_raw_value(expr)
@ -355,9 +381,10 @@ class ExprBLSocket(base.MaxwellSimSocket):
elif self.mathtype == MT_R: elif self.mathtype == MT_R:
self.raw_value_float2 = self._to_raw_value(expr) self.raw_value_float2 = self._to_raw_value(expr)
elif self.mathtype == MT_C: elif self.mathtype == MT_C:
self.raw_value_complex2 = self._to_raw_value(expr) self.raw_value_complex2 = self._to_raw_value(
expr, force_complex=True
)
elif self.shape == (3,): elif self.shape == (3,):
log.critical(expr)
if self.mathtype == MT_Z: if self.mathtype == MT_Z:
self.raw_value_int3 = self._to_raw_value(expr) self.raw_value_int3 = self._to_raw_value(expr)
elif self.mathtype == MT_Q: elif self.mathtype == MT_Q:
@ -365,7 +392,9 @@ class ExprBLSocket(base.MaxwellSimSocket):
elif self.mathtype == MT_R: elif self.mathtype == MT_R:
self.raw_value_float3 = self._to_raw_value(expr) self.raw_value_float3 = self._to_raw_value(expr)
elif self.mathtype == MT_C: elif self.mathtype == MT_C:
self.raw_value_complex3 = self._to_raw_value(expr) self.raw_value_complex3 = self._to_raw_value(
expr, force_complex=True
)
#################### ####################
# - FlowKind: LazyArrayRange # - FlowKind: LazyArrayRange
@ -451,7 +480,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
] ]
elif value.mathtype == MT_C: elif value.mathtype == MT_C:
self.raw_range_complex = [ self.raw_range_complex = [
self._to_raw_value(bound * unit) self._to_raw_value(bound * unit, force_complex=True)
for bound in [value.start, value.stop] for bound in [value.start, value.stop]
] ]
@ -674,7 +703,7 @@ class ExprBLSocket(base.MaxwellSimSocket):
_row.label(text=text) _row.label(text=text)
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None: def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
if self.show_info_columns: if self.active_kind == ct.FlowKind.Array and self.show_info_columns:
row = col.row() row = col.row()
box = row.box() box = row.box()
grid = box.grid_flow( grid = box.grid_flow(
@ -725,9 +754,9 @@ class ExprBLSocket(base.MaxwellSimSocket):
#################### ####################
class ExprSocketDef(base.SocketDef): class ExprSocketDef(base.SocketDef):
socket_type: ct.SocketType = ct.SocketType.Expr socket_type: ct.SocketType = ct.SocketType.Expr
active_kind: typ.Literal[ct.FlowKind.Value, ct.FlowKind.LazyArrayRange] = ( active_kind: typ.Literal[
ct.FlowKind.Value ct.FlowKind.Value, ct.FlowKind.LazyArrayRange, ct.FlowKind.Array
) ] = ct.FlowKind.Value
# Socket Interface # Socket Interface
## TODO: __hash__ like socket method based on these? ## TODO: __hash__ like socket method based on these?
@ -740,15 +769,15 @@ class ExprSocketDef(base.SocketDef):
default_unit: spux.Unit | None = None default_unit: spux.Unit | None = None
# FlowKind: Value # FlowKind: Value
default_value: spux.SympyExpr = sp.RealNumber(0) default_value: spux.SympyExpr = sp.S(0)
abs_min: spux.SympyExpr | None = None ## TODO: Not used (yet) abs_min: spux.SympyExpr | None = None ## TODO: Not used (yet)
abs_max: 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: Idea is to use this scalar uniformly for all shape elements
## TODO: -> But we may want to **allow** using same-shape for diff. bounds. ## TODO: -> But we may want to **allow** using same-shape for diff. bounds.
# FlowKind: LazyArrayRange # FlowKind: LazyArrayRange
default_min: spux.SympyExpr = sp.RealNumber(0) default_min: spux.SympyExpr = sp.S(0)
default_max: spux.SympyExpr = sp.RealNumber(1) default_max: spux.SympyExpr = sp.S(1)
default_steps: int = 2 default_steps: int = 2
## TODO: Configure lin/log/... scaling (w/enumprop in UI) ## TODO: Configure lin/log/... scaling (w/enumprop in UI)

View File

@ -1,4 +1,3 @@
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base

View File

@ -1,4 +1,3 @@
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
@ -18,7 +17,7 @@ class MaxwellStructureSocketDef(base.SocketDef):
def init(self, bl_socket: MaxwellStructureBLSocket) -> None: def init(self, bl_socket: MaxwellStructureBLSocket) -> None:
if self.is_list: if self.is_list:
bl_socket.active_kind = ct.FlowKind.ValueArray bl_socket.active_kind = ct.FlowKind.Array
#################### ####################

View File

@ -98,6 +98,12 @@ class MathType(enum.StrEnum):
if sp_obj.is_complex: if sp_obj.is_complex:
return MathType.Complex return MathType.Complex
# Infinities
if sp_obj in [sp.oo, -sp.oo]:
return MathType.Real ## TODO: Strictly, could be ex. integer...
if sp_obj in [sp.zoo, -sp.zoo]:
return MathType.Complex
msg = f"Can't determine MathType from sympy object: {sp_obj}" msg = f"Can't determine MathType from sympy object: {sp_obj}"
raise ValueError(msg) raise ValueError(msg)
@ -755,13 +761,11 @@ def scaling_factor(unit_from: spu.Quantity, unit_to: spu.Quantity) -> Number:
raise ValueError(msg) raise ValueError(msg)
_UNIT_STR_MAP = {sym.name: unit for sym, unit in UNIT_BY_SYMBOL.items()}
@functools.cache @functools.cache
def unit_str_to_unit(unit_str: str) -> Unit | None: def unit_str_to_unit(unit_str: str) -> Unit | None:
if unit_str in _UNIT_STR_MAP: expr = sp.sympify(unit_str).subs(UNIT_BY_SYMBOL)
return _UNIT_STR_MAP[unit_str] if expr.has(spu.Quantity):
return expr
msg = f'No valid unit for unit string {unit_str}' msg = f'No valid unit for unit string {unit_str}'
raise ValueError(msg) raise ValueError(msg)
@ -812,7 +816,6 @@ class PhysicalType(enum.StrEnum):
# Luminal # Luminal
LumIntensity = enum.auto() LumIntensity = enum.auto()
LumFlux = enum.auto() LumFlux = enum.auto()
Luminance = enum.auto()
Illuminance = enum.auto() Illuminance = enum.auto()
# Optics # Optics
OrdinaryWaveVector = enum.auto() OrdinaryWaveVector = enum.auto()
@ -866,7 +869,7 @@ class PhysicalType(enum.StrEnum):
PT.OrdinaryWaveVector: Dims.frequency, PT.OrdinaryWaveVector: Dims.frequency,
PT.AngularWaveVector: Dims.angle * Dims.frequency, PT.AngularWaveVector: Dims.angle * Dims.frequency,
PT.PoyntingVector: Dims.power / Dims.length**2, PT.PoyntingVector: Dims.power / Dims.length**2,
} }[self]
@property @property
def default_unit(self) -> list[Unit]: def default_unit(self) -> list[Unit]:
@ -1072,7 +1075,7 @@ class PhysicalType(enum.StrEnum):
@staticmethod @staticmethod
def from_unit(unit: Unit) -> list[Unit]: def from_unit(unit: Unit) -> list[Unit]:
for physical_type in list[PhysicalType]: for physical_type in list(PhysicalType):
if unit in physical_type.valid_units: if unit in physical_type.valid_units:
return physical_type return physical_type
@ -1161,7 +1164,7 @@ class PhysicalType(enum.StrEnum):
@staticmethod @staticmethod
def to_name(value: typ.Self) -> str: def to_name(value: typ.Self) -> str:
return sp_to_str(value.unit_dim) return PhysicalType(value).name
@staticmethod @staticmethod
def to_icon(value: typ.Self) -> str: def to_icon(value: typ.Self) -> str:
@ -1208,6 +1211,7 @@ UNITS_SI: UnitSystem = {
# Electrodynamics # Electrodynamics
_PT.Current: spu.ampere, _PT.Current: spu.ampere,
_PT.CurrentDensity: spu.ampere / spu.meter**2, _PT.CurrentDensity: spu.ampere / spu.meter**2,
_PT.Voltage: spu.volt,
_PT.Capacitance: spu.farad, _PT.Capacitance: spu.farad,
_PT.Impedance: spu.ohm, _PT.Impedance: spu.ohm,
_PT.Conductance: spu.siemens, _PT.Conductance: spu.siemens,
@ -1278,13 +1282,12 @@ def sympy_to_python(
#################### ####################
# - Convert to Unit System # - Convert to Unit System
#################### ####################
def _flat_unit_system_units(unit_system: UnitSystem) -> SympyExpr:
return list(unit_system.values())
def convert_to_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr: def convert_to_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr:
"""Convert an expression to the units of a given unit system, with appropriate scaling.""" """Convert an expression to the units of a given unit system, with appropriate scaling."""
return spu.convert_to(sp_obj, _flat_unit_system_units(unit_system)) return spu.convert_to(
sp_obj,
{unit_system[PhysicalType.from_unit(unit)] for unit in get_units(sp_obj)},
)
def strip_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr: def strip_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr: