fix: Inching closer.

I'm of the belief that the correct abstractions are now actually
available, and that most-to-all of the required functionality actually
already exists within the code base.
The art is bringing it together!
main
Sofus Albert Høgsbro Rose 2024-05-01 13:54:16 +02:00
parent e330b9a451
commit 7263d585e5
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
29 changed files with 628 additions and 321 deletions

26
TODO.md
View File

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

View File

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

View File

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

View File

@ -57,19 +57,22 @@ class FlowKind(enum.StrEnum):
Info = enum.auto() Info = enum.auto()
@classmethod @classmethod
def scale_to_unit_system(cls, kind: typ.Self, value, socket_type, unit_system): def scale_to_unit_system(
cls,
kind: typ.Self,
value,
unit_system: spux.UnitSystem,
):
if kind == cls.Value: if kind == cls.Value:
return spux.sympy_to_python( return spux.scale_to_unit_system(
spux.scale_to_unit( value,
value, unit_system,
unit_system[socket_type],
)
) )
if kind == cls.LazyArrayRange: if kind == cls.LazyArrayRange:
return value.rescale_to_unit(unit_system[socket_type]) return value.rescale_to_unit_system(unit_system)
if kind == cls.Params: if kind == cls.Params:
return value.rescale_to_unit(unit_system[socket_type]) return value.rescale_to_unit_system(unit_system)
msg = 'Tried to scale unknown kind' msg = 'Tried to scale unknown kind'
raise ValueError(msg) raise ValueError(msg)
@ -187,6 +190,9 @@ class ArrayFlow:
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg) raise ValueError(msg)
def rescale_to_unit_system(self, unit: spu.Quantity) -> typ.Self:
raise NotImplementedError
#################### ####################
# - Lazy Value Func # - Lazy Value Func
@ -469,14 +475,13 @@ class LazyArrayRangeFlow:
# Get Stop Mathtype # Get Stop Mathtype
if isinstance(self.stop, spux.SympyType): if isinstance(self.stop, spux.SympyType):
stop_mathtype = spux.MathType.from_expr(type(self.stop)) stop_mathtype = spux.MathType.from_expr(self.stop)
else: else:
stop_mathtype = spux.MathType.from_pytype(type(self.stop)) stop_mathtype = spux.MathType.from_pytype(self.stop)
# Check Equal # Check Equal
if start_mathtype != stop_mathtype: if start_mathtype != stop_mathtype:
msg = "Mathtypes of start and stop don't agree. Please fix!" return spux.MathType.combine(start_mathtype, stop_mathtype)
raise ValueError(msg)
return start_mathtype return start_mathtype
@ -525,8 +530,8 @@ class LazyArrayRangeFlow:
""" """
if self.unit is not None: if self.unit is not None:
return LazyArrayRangeFlow( return LazyArrayRangeFlow(
start=spu.scale_to_unit(self.start * self.unit, unit), start=spux.scale_to_unit(self.start * self.unit, unit),
stop=spu.scale_to_unit(self.stop * self.unit, unit), stop=spux.scale_to_unit(self.stop * self.unit, unit),
steps=self.steps, steps=self.steps,
scaling=self.scaling, scaling=self.scaling,
unit=unit, unit=unit,
@ -536,6 +541,39 @@ class LazyArrayRangeFlow:
msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}'
raise ValueError(msg) raise ValueError(msg)
def rescale_to_unit_system(self, unit_system: spux.Unit) -> typ.Self:
"""Replaces the units, **with** rescaling of the bounds.
Parameters:
unit: The unit to convert the bounds to.
Returns:
A new `LazyArrayRangeFlow` with replaced unit.
Raises:
ValueError: If the existing unit is `None`, indicating that there is no unit to correct.
"""
if self.unit is not None:
return LazyArrayRangeFlow(
start=spux.strip_unit_system(
spux.convert_to_unit_system(self.start * self.unit, unit_system),
unit_system,
),
stop=spux.strip_unit_system(
spux.convert_to_unit_system(self.start * self.unit, unit_system),
unit_system,
),
steps=self.steps,
scaling=self.scaling,
unit=unit_system[spux.PhysicalType.from_unit(self.unit)],
symbols=self.symbols,
)
msg = (
f'Tried to rescale unitless LazyDataValueRange to unit system {unit_system}'
)
raise ValueError(msg)
#################### ####################
# - Bound Operations # - Bound Operations
#################### ####################

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -217,7 +217,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Called by `self.on_prop_changed()` when `self.active_kind` was changed. Called by `self.on_prop_changed()` when `self.active_kind` was changed.
""" """
self.display_shape = ( self.display_shape = (
'SQUARE' if self.active_kind == ct.FlowKind.LazyValueRange else 'CIRCLE' 'SQUARE' if self.active_kind == ct.FlowKind.LazyArrayRange else 'CIRCLE'
) # + ('_DOT' if self.use_units else '') ) # + ('_DOT' if self.use_units else '')
## TODO: Valid Active Kinds should be a subset/subenum(?) of FlowKind ## TODO: Valid Active Kinds should be a subset/subenum(?) of FlowKind

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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