From 7263d585e5b57961e41846eaea6a17eff9d3a625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Wed, 1 May 2024 13:54:16 +0200 Subject: [PATCH] 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! --- TODO.md | 26 +- src/blender_maxwell/assets/geonodes.py | 16 +- .../contracts/bl_socket_types.py | 76 +++--- .../maxwell_sim_nodes/contracts/flow_kinds.py | 66 ++++- .../contracts/flow_signals.py | 1 + .../managed_objs/managed_bl_mesh.py | 25 +- .../node_trees/maxwell_sim_nodes/node_tree.py | 6 +- .../maxwell_sim_nodes/nodes/__init__.py | 35 ++- .../nodes/analysis/math/map_math.py | 4 +- .../nodes/analysis/math/operate_math.py | 25 +- .../maxwell_sim_nodes/nodes/analysis/viz.py | 20 +- .../maxwell_sim_nodes/nodes/base.py | 28 +- .../maxwell_sim_nodes/nodes/events.py | 21 +- .../nodes/inputs/constants/__init__.py | 14 +- .../nodes/inputs/constants/number_constant.py | 4 +- .../inputs/constants/physical_constant.py | 25 +- .../inputs/constants/scientific_constant.py | 50 ++-- .../nodes/inputs/wave_constant.py | 47 +++- .../web_importers/tidy_3d_web_importer.py | 25 +- .../nodes/monitors/eh_field_monitor.py | 26 +- .../monitors/field_power_flux_monitor.py | 27 +- .../nodes/outputs/__init__.py | 11 +- .../maxwell_sim_nodes/sockets/base.py | 2 +- .../maxwell_sim_nodes/sockets/expr.py | 247 +++++++++++++----- .../sockets/tidy3d/cloud_task.py | 27 +- .../nodeps/utils/blender_type_enum.py | 1 + src/blender_maxwell/utils/bl_cache.py | 30 ++- .../utils/extra_sympy_units.py | 63 +++-- src/blender_maxwell/utils/serialize.py | 1 + 29 files changed, 628 insertions(+), 321 deletions(-) diff --git a/TODO.md b/TODO.md index f5c7319..1ae57a8 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,5 @@ # Working TODO -- [ ] Wave Constant +- [x] Wave Constant - Bounds - [ ] Boundary Conds - [ ] PML @@ -18,8 +18,8 @@ - [ ] Data File Import - [ ] DataFit Medium - Monitors - - [ ] EH Field - - [ ] Power Flux + - [x] EH Field + - [x] Power Flux - [ ] Permittivity - [ ] Diffraction - Structures @@ -49,9 +49,9 @@ - Integration - [ ] Simulation and Analysis of Maxim's Cavity - Constants - - [ ] Number Constant - - [ ] Vector Constant - - [ ] Physical Constant + - [x] Number Constant + - [x] Vector Constant + - [x] Physical Constant - [ ] Fix many problems by persisting `_enum_cb_cache` and `_str_cb_cache`. @@ -70,7 +70,7 @@ - [ ] Pol SocketType: 3D Poincare sphere visualization of Stokes vectors. - [x] Math / Map Math - - [ ] Remove "By x" socket set let socket sets only be "Function"/"Expr"; then add a dynamic enum underneath to select "By x" based on data support. + - [x] Remove "By x" socket set let socket sets only be "Function"/"Expr"; then add a dynamic enum underneath to select "By x" based on data support. - [ ] Filter the operations based on data support, ex. use positive-definiteness to guide cholesky. - [ ] Implement support for additional symbols via `Expr`. - [x] Math / Filter Math @@ -81,8 +81,6 @@ ## Inputs - [x] Wave Constant - - [ ] Fix the LazyValueRange (again!) - - [ ] Document - [x] Scene - [ ] Implement export of scene time via. Blender unit system. - [ ] Implement optional scene-synced time exporting, so that the simulation definition and scene definition match for analysis needs. @@ -90,14 +88,14 @@ - [x] Constants / Expr Constant - See IDEAS. - [x] Constants / Number Constant - - [ ] Fix non-integer sockets -- [ ] Constants / Vector Constant -- [ ] Constants / Physical Constant +- [x] Constants / Vector Constant +- [x] Constants / Physical Constant - [x] Constants / Scientific Constant - [ ] Nicer (boxed?) node information, maybe centered headers, in a box, etc. . -- [x] Constants / Unit System Constant +- [ ] Constants / Unit System Constant + - [ ] Re-implement with `PhysicalType`. - [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row. -- [x] Constants / Blender Constant +- [ ] Constants / Blender Constant - [ ] Fix it! - [ ] Web / Tidy3D Web Importer diff --git a/src/blender_maxwell/assets/geonodes.py b/src/blender_maxwell/assets/geonodes.py index 47b2755..36e367c 100644 --- a/src/blender_maxwell/assets/geonodes.py +++ b/src/blender_maxwell/assets/geonodes.py @@ -168,13 +168,13 @@ class GeoNodes(enum.StrEnum): GN.StructurePrimitiveCapsule: GN_INTERNAL_STRUCTURES_PATH, GN.StructurePrimitiveCone: GN_INTERNAL_STRUCTURES_PATH, ## Monitor - GN.MonitorEHField: GN_INTERNAL_STRUCTURES_PATH, - GN.MonitorPowerFlux: GN_INTERNAL_STRUCTURES_PATH, - GN.MonitorEpsTensor: GN_INTERNAL_STRUCTURES_PATH, - GN.MonitorDiffraction: GN_INTERNAL_STRUCTURES_PATH, - GN.MonitorProjCartEHField: GN_INTERNAL_STRUCTURES_PATH, - GN.MonitorProjAngEHField: GN_INTERNAL_STRUCTURES_PATH, - GN.MonitorProjKSpaceEHField: GN_INTERNAL_STRUCTURES_PATH, + GN.MonitorEHField: GN_INTERNAL_MONITORS_PATH, + GN.MonitorPowerFlux: GN_INTERNAL_MONITORS_PATH, + GN.MonitorEpsTensor: GN_INTERNAL_MONITORS_PATH, + GN.MonitorDiffraction: GN_INTERNAL_MONITORS_PATH, + GN.MonitorProjCartEHField: GN_INTERNAL_MONITORS_PATH, + GN.MonitorProjAngEHField: GN_INTERNAL_MONITORS_PATH, + GN.MonitorProjKSpaceEHField: GN_INTERNAL_MONITORS_PATH, ## Simulation GN.SimulationSimDomain: GN_INTERNAL_SIMULATIONS_PATH, GN.SimulationBoundConds: GN_INTERNAL_SIMULATIONS_PATH, @@ -225,7 +225,7 @@ def import_geonodes( if import_method == 'link' and geonodes in bpy.data.node_groups: return bpy.data.node_groups[geonodes] - filename = geonodes + filename = str(geonodes) filepath = str(geonodes.parent_path / (geonodes + '.blend') / 'NodeTree' / geonodes) directory = filepath.removesuffix(geonodes) log.info( diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py index 1ad0e29..d76eb26 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl_socket_types.py @@ -25,58 +25,60 @@ class BLSocketInfo: bl_isocket_identifier: spux.ScalarUnitlessRealExpr -@blender_type_enum.prefix_values_with('NodeSocket') class BLSocketType(enum.StrEnum): - Virtual = 'Virtual' + Virtual = 'NodeSocketVirtual' # Blender - Image = 'Image' - Shader = 'Shader' - Material = 'Material' - Geometry = 'Material' - Object = 'Object' - Collection = 'Collection' + Image = 'NodeSocketImage' + Shader = 'NodeSocketShader' + Material = 'NodeSocketMaterial' + Geometry = 'NodeSocketGeometry' + Object = 'NodeSocketObject' + Collection = 'NodeSocketCollection' # Basic - Bool = 'Bool' - String = 'String' - Menu = 'Menu' + Bool = 'NodeSocketBool' + String = 'NodeSocketString' + Menu = 'NodeSocketMenu' # Float - Float = 'Float' - FloatUnsigned = 'FloatUnsigned' - FloatAngle = 'FloatAngle' - FloatDistance = 'FloatDistance' - FloatFactor = 'FloatFactor' - FloatPercentage = 'FloatPercentage' - FloatTime = 'FloatTime' - FloatTimeAbsolute = 'FloatTimeAbsolute' + Float = 'NodeSocketFloat' + FloatUnsigned = 'NodeSocketFloatUnsigned' + FloatAngle = 'NodeSocketFloatAngle' + FloatDistance = 'NodeSocketFloatDistance' + FloatFactor = 'NodeSocketFloatFactor' + FloatPercentage = 'NodeSocketFloatPercentage' + FloatTime = 'NodeSocketFloatTime' + FloatTimeAbsolute = 'NodeSocketFloatTimeAbsolute' # Int - Int = 'Int' - IntFactor = 'IntFactor' - IntPercentage = 'IntPercentage' - IntUnsigned = 'IntUnsigned' + Int = 'NodeSocketInt' + IntFactor = 'NodeSocketIntFactor' + IntPercentage = 'NodeSocketIntPercentage' + IntUnsigned = 'NodeSocketIntUnsigned' # Vector - Color = 'Color' - Rotation = 'Rotation' - Vector = 'Vector' - VectorAcceleration = 'Acceleration' - VectorDirection = 'Direction' - VectorEuler = 'Euler' - VectorTranslation = 'Translation' - VectorVelocity = 'Velocity' - VectorXYZ = 'XYZ' + Color = 'NodeSocketColor' + Rotation = 'NodeSocketRotation' + Vector = 'NodeSocketVector' + VectorAcceleration = 'NodeSocketAcceleration' + VectorDirection = 'NodeSocketDirection' + VectorEuler = 'NodeSocketEuler' + VectorTranslation = 'NodeSocketTranslation' + VectorVelocity = 'NodeSocketVelocity' + VectorXYZ = 'NodeSocketXYZ' @staticmethod def from_bl_isocket( bl_isocket: bpy.types.NodeTreeInterfaceSocket, ) -> typ.Self: - return BLSocketType[bl_isocket.bl_socket_idname] + return BLSocketType(bl_isocket.bl_socket_idname) @staticmethod def info_from_bl_isocket( bl_isocket: bpy.types.NodeTreeInterfaceSocket, ) -> typ.Self: - return BLSocketType.from_bl_isocket(bl_isocket).parse( - bl_isocket.default_value, bl_isocket.description, bl_isocket.identifier - ) + bl_socket_type = BLSocketType.from_bl_isocket(bl_isocket) + if bl_socket_type.has_support: + return bl_socket_type.parse( + bl_isocket.default_value, bl_isocket.description, bl_isocket.identifier + ) + return bl_socket_type.parse(None, bl_isocket.description, bl_isocket.identifier) #################### # - Direct Properties @@ -288,7 +290,7 @@ class BLSocketType(enum.StrEnum): ) # Parse the Default Value - if self.mathtype is not None: + if self.mathtype is not None and bl_default_value is not None: if self.size == spux.NumberSize1D.Scalar: default_value = self.mathtype.pytype(bl_default_value) elif description.startswith('2D'): diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py index e990458..24e0002 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_kinds.py @@ -57,19 +57,22 @@ class FlowKind(enum.StrEnum): Info = enum.auto() @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: - return spux.sympy_to_python( - spux.scale_to_unit( - value, - unit_system[socket_type], - ) + return spux.scale_to_unit_system( + value, + unit_system, ) if kind == cls.LazyArrayRange: - return value.rescale_to_unit(unit_system[socket_type]) + return value.rescale_to_unit_system(unit_system) if kind == cls.Params: - return value.rescale_to_unit(unit_system[socket_type]) + return value.rescale_to_unit_system(unit_system) msg = 'Tried to scale unknown kind' raise ValueError(msg) @@ -187,6 +190,9 @@ class ArrayFlow: msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' raise ValueError(msg) + def rescale_to_unit_system(self, unit: spu.Quantity) -> typ.Self: + raise NotImplementedError + #################### # - Lazy Value Func @@ -469,14 +475,13 @@ class LazyArrayRangeFlow: # Get Stop Mathtype if isinstance(self.stop, spux.SympyType): - stop_mathtype = spux.MathType.from_expr(type(self.stop)) + stop_mathtype = spux.MathType.from_expr(self.stop) else: - stop_mathtype = spux.MathType.from_pytype(type(self.stop)) + stop_mathtype = spux.MathType.from_pytype(self.stop) # Check Equal if start_mathtype != stop_mathtype: - msg = "Mathtypes of start and stop don't agree. Please fix!" - raise ValueError(msg) + return spux.MathType.combine(start_mathtype, stop_mathtype) return start_mathtype @@ -525,8 +530,8 @@ class LazyArrayRangeFlow: """ if self.unit is not None: return LazyArrayRangeFlow( - start=spu.scale_to_unit(self.start * self.unit, unit), - stop=spu.scale_to_unit(self.stop * self.unit, unit), + start=spux.scale_to_unit(self.start * self.unit, unit), + stop=spux.scale_to_unit(self.stop * self.unit, unit), steps=self.steps, scaling=self.scaling, unit=unit, @@ -536,6 +541,39 @@ class LazyArrayRangeFlow: msg = f'Tried to rescale unitless LazyDataValueRange to unit {unit}' raise ValueError(msg) + def rescale_to_unit_system(self, unit_system: spux.Unit) -> typ.Self: + """Replaces the units, **with** rescaling of the bounds. + + Parameters: + unit: The unit to convert the bounds to. + + Returns: + A new `LazyArrayRangeFlow` with replaced unit. + + Raises: + ValueError: If the existing unit is `None`, indicating that there is no unit to correct. + """ + if self.unit is not None: + return LazyArrayRangeFlow( + start=spux.strip_unit_system( + spux.convert_to_unit_system(self.start * self.unit, unit_system), + unit_system, + ), + stop=spux.strip_unit_system( + spux.convert_to_unit_system(self.start * self.unit, unit_system), + unit_system, + ), + steps=self.steps, + scaling=self.scaling, + unit=unit_system[spux.PhysicalType.from_unit(self.unit)], + symbols=self.symbols, + ) + + msg = ( + f'Tried to rescale unitless LazyDataValueRange to unit system {unit_system}' + ) + raise ValueError(msg) + #################### # - Bound Operations #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_signals.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_signals.py index 84fdce0..b213c2f 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_signals.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/flow_signals.py @@ -16,6 +16,7 @@ class FlowSignal(enum.StrEnum): """ + FlowInitializing = enum.auto() FlowPending = enum.auto() NoFlow = enum.auto() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py index d561553..3e26a15 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_mesh.py @@ -110,26 +110,23 @@ class ManagedBLMesh(base.ManagedObj): If it's already included, do nothing. """ - if (bl_object := bpy.data.objects.get(self.name)) is not None: - if bl_object.name not in preview_collection().objects: - log.info('Moving "%s" to Preview Collection', bl_object.name) - preview_collection().objects.link(bl_object) - else: - msg = 'Managed BLMesh does not exist' - raise ValueError(msg) + bl_object = bpy.data.objects.get(self.name) + if bl_object is None: + bl_object = self.bl_object() + + if bl_object.name not in preview_collection().objects: + log.info('Moving "%s" to Preview Collection', bl_object.name) + preview_collection().objects.link(bl_object) def hide_preview(self) -> None: """Removes the managed Blender object from the preview collection. If it's already removed, do nothing. """ - if (bl_object := bpy.data.objects.get(self.name)) is not None: - if bl_object.name in preview_collection().objects: - log.info('Removing "%s" from Preview Collection', bl_object.name) - preview_collection().objects.unlink(bl_object) - else: - msg = 'Managed BLMesh does not exist' - raise ValueError(msg) + bl_object = bpy.data.objects.get(self.name) + if bl_object is not None and bl_object.name in preview_collection().objects: + log.info('Removing "%s" from Preview Collection', bl_object.name) + preview_collection().objects.unlink(bl_object) def bl_select(self) -> None: """Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport.""" diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py index 4176d87..2ab8c2c 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/node_tree.py @@ -206,6 +206,8 @@ class MaxwellSimTree(bpy.types.NodeTree): """Unlock all nodes in the node tree, making them editable.""" log.info('Unlocking All Nodes in NodeTree "%s"', self.bl_label) for node in self.nodes: + if node.type in ['REROUTE', 'FRAME']: + continue node.locked = False for bl_socket in [*node.inputs, *node.outputs]: bl_socket.locked = False @@ -229,7 +231,9 @@ class MaxwellSimTree(bpy.types.NodeTree): @contextlib.contextmanager def repreview_all(self) -> None: all_nodes_with_preview_active = { - node.instance_id: node for node in self.nodes if node.preview_active + node.instance_id: node + for node in self.nodes + if node.type not in ['REROUTE', 'FRAME'] and node.preview_active } self.is_currently_repreviewing = True self.newly_previewed_nodes = {} diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py index 169ed19..480eba6 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/__init__.py @@ -1,40 +1,37 @@ -# from . import kitchen_sink -# from . import bounds from . import ( analysis, + # bounds, inputs, - mediums, + # mediums, monitors, outputs, - simulations, - sources, - structures, - utilities, + # simulations, + # sources, + # structures, + # utilities, ) BL_REGISTER = [ - # *kitchen_sink.BL_REGISTER, *analysis.BL_REGISTER, *inputs.BL_REGISTER, *outputs.BL_REGISTER, - *sources.BL_REGISTER, - *mediums.BL_REGISTER, - *structures.BL_REGISTER, + # *sources.BL_REGISTER, + # *mediums.BL_REGISTER, + # *structures.BL_REGISTER, # *bounds.BL_REGISTER, *monitors.BL_REGISTER, - *simulations.BL_REGISTER, - *utilities.BL_REGISTER, + # *simulations.BL_REGISTER, + # *utilities.BL_REGISTER, ] BL_NODES = { - # **kitchen_sink.BL_NODES, **analysis.BL_NODES, **inputs.BL_NODES, **outputs.BL_NODES, - **sources.BL_NODES, - **mediums.BL_NODES, - **structures.BL_NODES, + # **sources.BL_NODES, + # **mediums.BL_NODES, + # **structures.BL_NODES, # **bounds.BL_NODES, **monitors.BL_NODES, - **simulations.BL_NODES, - **utilities.BL_NODES, + # **simulations.BL_NODES, + # **utilities.BL_NODES, } diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py index 2f5285e..2ff4423 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py @@ -401,8 +401,8 @@ class MapMathNode(base.MaxwellSimNode): run_on_init=True, ) def on_input_changed(self): - if self.operation not in MapOperation.by_element_shape(self.expr_output_shape): - self.operation = bl_cache.Signal.ResetEnumItems + # if self.operation not in MapOperation.by_element_shape(self.expr_output_shape): + self.operation = bl_cache.Signal.ResetEnumItems @events.on_value_changed( # Trigger diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py index 8b5794d..6ddac23 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/operate_math.py @@ -20,6 +20,10 @@ FUNCS = { 'MUL': lambda exprs: exprs[0] * exprs[1], 'DIV': lambda exprs: exprs[0] / exprs[1], 'POW': lambda exprs: exprs[0] ** exprs[1], + 'ATAN2': lambda exprs: sp.atan2(exprs[1], exprs[0]), + # Vector | Vector + 'VEC_VEC_DOT': lambda exprs: exprs[0].dot(exprs[1]), + 'CROSS': lambda exprs: exprs[0].cross(exprs[1]), } SP_FUNCS = FUNCS @@ -52,8 +56,8 @@ class OperateMathNode(base.MaxwellSimNode): bl_label = 'Operate Math' input_sockets: typ.ClassVar = { - 'Expr L': sockets.ExprSocketDef(show_info_columns=False), - 'Expr R': sockets.ExprSocketDef(show_info_columns=False), + 'Expr L': sockets.ExprSocketDef(), + 'Expr R': sockets.ExprSocketDef(), } output_sockets: typ.ClassVar = { 'Expr': sockets.ExprSocketDef(), @@ -73,10 +77,12 @@ class OperateMathNode(base.MaxwellSimNode): def search_categories(self) -> list[ct.BLEnumElement]: """Deduce and return a list of valid categories for the current socket set and input data.""" expr_l_info = self._compute_input( - 'Expr L', kind=ct.FlowKind.Info, optional=True + 'Expr L', + kind=ct.FlowKind.Info, ) expr_r_info = self._compute_input( - 'Expr R', kind=ct.FlowKind.Info, optional=True + 'Expr R', + kind=ct.FlowKind.Info, ) has_expr_l_info = not ct.FlowSignal.check(expr_l_info) @@ -121,6 +127,10 @@ class OperateMathNode(base.MaxwellSimNode): if expr_l_info.output_shape is None and expr_r_info.output_shape is None: categories = [NUMBER_NUMBER] + ## * | Number + elif expr_r_info.output_shape is None: + categories = [] + ## Number | Vector elif ( expr_l_info.output_shape is None and len(expr_r_info.output_shape) == 1 @@ -170,13 +180,12 @@ class OperateMathNode(base.MaxwellSimNode): ('POW', 'L^R', 'Power'), ('ATAN2', 'atan2(L,R)', 'atan2(L,R)'), ] - if self.category in 'Vector | Vector': + if self.category == 'Vector | Vector': if items: items += [None] items += [ ('VEC_VEC_DOT', 'L ยท R', 'Vector-Vector Product'), ('CROSS', 'L x R', 'Cross Product'), - ('PROJ', 'proj(L, R)', 'Projection'), ] if self.category == 'Matrix | Vector': if items: @@ -364,9 +373,7 @@ class OperateMathNode(base.MaxwellSimNode): 'Expr R': ct.FlowKind.Params, }, ) - def compute_params( - self, props, input_sockets - ) -> ct.ParamsFlow | ct.FlowSignal: + def compute_params(self, props, input_sockets) -> ct.ParamsFlow | ct.FlowSignal: operation = props['operation'] params_l = input_sockets['Expr L'] params_r = input_sockets['Expr R'] diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py index 0e008c0..21a9d05 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/viz.py @@ -2,8 +2,11 @@ import enum import typing as typ import bpy +import jax +import jax.numpy as jnp import jaxtyping as jtyp import matplotlib.axis as mpl_ax +import sympy as sp from blender_maxwell.utils import bl_cache, image_ops, logger from blender_maxwell.utils import extra_sympy_units as spux @@ -192,7 +195,10 @@ class VizNode(base.MaxwellSimNode): # - Sockets #################### input_sockets: typ.ClassVar = { - 'Expr': sockets.ExprSocketDef(), + 'Expr': sockets.ExprSocketDef( + symbols={_x := sp.Symbol('x', real=True)}, + default_value=2 * _x, + ), } output_sockets: typ.ClassVar = { 'Preview': sockets.AnySocketDef(), @@ -221,8 +227,12 @@ class VizNode(base.MaxwellSimNode): ## - Mode Searcher ##################### @property - def data_info(self) -> ct.InfoFlow: - return self._compute_input('Expr', kind=ct.FlowKind.Info) + def data_info(self) -> ct.InfoFlow | None: + info = self._compute_input('Expr', kind=ct.FlowKind.Info) + if not ct.FlowSignal.check(info): + return info + + return None def search_modes(self) -> list[ct.BLEnumElement]: if not ct.FlowSignal.check(self.data_info): @@ -298,7 +308,9 @@ class VizNode(base.MaxwellSimNode): managed_objs={'plot'}, props={'viz_mode', 'viz_target', 'colormap'}, input_sockets={'Expr'}, - input_socket_kinds={'Expr': {ct.FlowKind.Array, ct.FlowKind.Info}}, + input_socket_kinds={ + 'Expr': {ct.FlowKind.Array, ct.FlowKind.LazyValueFunc, ct.FlowKind.Info} + }, stop_propagation=True, ) def on_show_plot( diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py index 86e9fa2..b99611a 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -599,22 +599,28 @@ class MaxwellSimNode(bpy.types.Node): It must be currently active. kind: The data flow kind to compute. """ - if (bl_socket := self.inputs.get(input_socket_name)) is not None: - return ( - ct.FlowKind.scale_to_unit_system( - kind, - bl_socket.compute_data(kind=kind), - bl_socket.socket_type, - unit_system, + bl_socket = self.inputs.get(input_socket_name) + if bl_socket is not None: + if bl_socket.instance_id: + return ( + ct.FlowKind.scale_to_unit_system( + kind, + bl_socket.compute_data(kind=kind), + 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: 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) #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py index e70bf2b..c3b3f3f 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py @@ -3,6 +3,7 @@ import inspect import typing as typ from types import MappingProxyType +from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import logger from .. import contracts as ct @@ -10,7 +11,6 @@ from .. import contracts as ct log = logger.get(__name__) UnitSystemID = str -UnitSystem = dict[ct.SocketType, typ.Any] #################### @@ -70,7 +70,7 @@ def event_decorator( all_loose_input_sockets: bool = False, all_loose_output_sockets: bool = False, # Request Unit System Scaling - unit_systems: dict[UnitSystemID, UnitSystem] = MappingProxyType({}), + unit_systems: dict[UnitSystemID, spux.UnitSystem] = MappingProxyType({}), scale_input_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}), scale_output_sockets: dict[ct.SocketName, UnitSystemID] = MappingProxyType({}), ): @@ -213,7 +213,6 @@ def event_decorator( kind=kind, optional=output_sockets_optional.get(output_socket_name, False), ), - node.outputs[output_socket_name].socket_type, unit_systems.get(scale_output_sockets.get(output_socket_name)), ) @@ -269,9 +268,21 @@ def event_decorator( else {} ) + # Propagate Initialization + ## If there is a FlowInitializing, then the method would fail. + ## Therefore, propagate FlowInitializing if found. + if any( + ct.FlowSignal.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 - ## If there is a FlowPending, then the method would fail. - ## Therefore, propagate FlowPending if found. return method( node, **method_kw_args, diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py index 2b51a38..1e4d7da 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/__init__.py @@ -1,18 +1,22 @@ -# from . import scientific_constant -# from . import physical_constant -from . import blender_constant, expr_constant, number_constant, scientific_constant +from . import ( + blender_constant, + expr_constant, + number_constant, + physical_constant, + scientific_constant, +) BL_REGISTER = [ *expr_constant.BL_REGISTER, *scientific_constant.BL_REGISTER, *number_constant.BL_REGISTER, - # *physical_constant.BL_REGISTER, + *physical_constant.BL_REGISTER, *blender_constant.BL_REGISTER, ] BL_NODES = { **expr_constant.BL_NODES, **scientific_constant.BL_NODES, **number_constant.BL_NODES, - # **physical_constant.BL_NODES, + **physical_constant.BL_NODES, **blender_constant.BL_NODES, } diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/number_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/number_constant.py index 47b8fd8..d950328 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/number_constant.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/number_constant.py @@ -44,7 +44,7 @@ class NumberConstantNode(base.MaxwellSimNode): #################### # - UI #################### - def draw_value(self, col: bpy.types.UILayout) -> None: + def draw_props(self, _, col: bpy.types.UILayout) -> None: row = col.row(align=True) row.prop(self, self.blfields['mathtype'], text='') row.prop(self, self.blfields['size'], text='') @@ -56,7 +56,7 @@ class NumberConstantNode(base.MaxwellSimNode): def on_mathtype_size_changed(self, props) -> None: """Change the input/output expression sockets to match the mathtype declared in the node.""" self.inputs['Value'].mathtype = props['mathtype'] - self.inputs['Value'].shape = props['mathtype'].shape + self.inputs['Value'].shape = props['size'].shape #################### # - FlowKind diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/physical_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/physical_constant.py index bf62f66..fa3304d 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/physical_constant.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/physical_constant.py @@ -1,6 +1,6 @@ -import enum import typing as typ +import bpy import sympy as sp from blender_maxwell.utils import bl_cache @@ -10,7 +10,7 @@ from .... import contracts, sockets from ... import base, events -class PhysicalConstantNode(base.MaxwellSimTreeNode): +class PhysicalConstantNode(base.MaxwellSimNode): """A number of configurable unit dimension, ex. time, length, etc. . Attributes: @@ -36,12 +36,12 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode): prop_ui=True, ) - mathtype: enum.Enum = bl_cache.BLField( + mathtype: spux.MathType = bl_cache.BLField( enum_cb=lambda self, _: self.search_mathtypes(), prop_ui=True, ) - size: enum.Enum = bl_cache.BLField( + size: spux.NumberSize1D = bl_cache.BLField( enum_cb=lambda self, _: self.search_sizes(), prop_ui=True, ) @@ -62,16 +62,25 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode): if spux.NumberSize1D.supports_shape(shape) ] + #################### + # - UI + #################### + def draw_props(self, _, col: bpy.types.UILayout) -> None: + row = col.row(align=True) + row.prop(self, self.blfields['mathtype'], text='') + row.prop(self, self.blfields['size'], text='') + #################### # - Events #################### @events.on_value_changed( prop_name={'physical_type', 'mathtype', 'size'}, + run_on_init=True, props={'physical_type', 'mathtype', 'size'}, ) def on_mathtype_or_size_changed(self, props) -> None: """Change the input/output expression sockets to match the mathtype and size declared in the node.""" - shape = spux.NumberSize1D(props['size']).shape + shape = props['size'].shape # Set Input Socket Physical Type if self.inputs['Value'].physical_type != props['physical_type']: @@ -90,9 +99,9 @@ class PhysicalConstantNode(base.MaxwellSimTreeNode): #################### # - Callbacks #################### - @events.computes_output_socket('value') - def compute_value(self: contracts.NodeTypeProtocol) -> sp.Expr: - return self.compute_input('value') + @events.computes_output_socket('Value', input_sockets={'Value'}) + def compute_value(self, input_sockets) -> sp.Expr: + return input_sockets['Value'] #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py index b348d8c..1ca8d9b 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/scientific_constant.py @@ -2,7 +2,7 @@ import typing as typ import bpy -from blender_maxwell.utils import sci_constants as constants +from blender_maxwell.utils import bl_cache, sci_constants from .... import contracts as ct from .... import sockets @@ -20,63 +20,43 @@ class ScientificConstantNode(base.MaxwellSimNode): #################### # - Properties #################### - sci_constant: bpy.props.StringProperty( - name='Sci Constant', - description='The name of a scientific constant', - default='', - search=lambda self, _, edit_text: self.search_sci_constants(edit_text), - update=lambda self, context: self.on_update_sci_constant(context), + sci_constant: str = bl_cache.BLField( + '', + prop_ui=True, + str_cb=lambda self, _, edit_text: self.search_sci_constants(edit_text), ) - cache__units: bpy.props.StringProperty(default='') - cache__uncertainty: bpy.props.StringProperty(default='') - def search_sci_constants( self, edit_text: str, ): return [ name - for name in constants.SCI_CONSTANTS + for name in sci_constants.SCI_CONSTANTS if edit_text.lower() in name.lower() ] - def on_update_sci_constant( - self, - context: bpy.types.Context, - ): - if self.sci_constant: - self.cache__units = str( - constants.SCI_CONSTANTS_INFO[self.sci_constant]['units'] - ) - self.cache__uncertainty = str( - constants.SCI_CONSTANTS_INFO[self.sci_constant]['uncertainty'] - ) - else: - self.cache__units = '' - self.cache__uncertainty = '' - - self.on_prop_changed('sci_constant', context) - #################### # - UI #################### def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None: - col.prop(self, 'sci_constant', text='') + col.prop(self, self.blfields['sci_constant'], text='') def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None: if self.sci_constant: - col.label(text=f'Units: {self.cache__units}') - col.label(text=f'Uncertainty: {self.cache__uncertainty}') - - col.label(text=f'Ref: {constants.SCI_CONSTANTS_REF[0]}') + col.label( + text=f'Units: {sci_constants.SCI_CONSTANTS_INFO[self.sci_constant]["units"]}' + ) + col.label( + text=f'Uncertainty: {sci_constants.SCI_CONSTANTS_INFO[self.sci_constant]["uncertainty"]}' + ) #################### - # - Callbacks + # - Output #################### @events.computes_output_socket('Value', props={'sci_constant'}) def compute_value(self, props: dict) -> typ.Any: - return constants.SCI_CONSTANTS[props['sci_constant']] + return sci_constants.SCI_CONSTANTS[props['sci_constant']] #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py index 749a2ee..3fbe7a0 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/wave_constant.py @@ -33,6 +33,7 @@ class WaveConstantNode(base.MaxwellSimNode): input_socket_sets: typ.ClassVar = { 'Wavelength': { 'WL': sockets.ExprSocketDef( + active_kind=ct.FlowKind.Value, physical_type=spux.PhysicalType.Length, # Defaults default_unit=spu.nm, @@ -58,18 +59,18 @@ class WaveConstantNode(base.MaxwellSimNode): output_sockets: typ.ClassVar = { 'WL': sockets.ExprSocketDef( active_kind=ct.FlowKind.Value, - unit_dimension=spux.Dims.length, + physical_type=spux.PhysicalType.Length, ), 'Freq': sockets.ExprSocketDef( active_kind=ct.FlowKind.Value, - unit_dimension=spux.Dims.frequency, + physical_type=spux.PhysicalType.Freq, ), } #################### # - Properties #################### - use_range: bool = bl_cache.BLField(False) + use_range: bool = bl_cache.BLField(False, prop_ui=True) #################### # - UI @@ -80,14 +81,14 @@ class WaveConstantNode(base.MaxwellSimNode): Parameters: col: Target for defining UI elements. """ - col.prop(self, self.blfields['use_range'], toggle=True) + col.prop(self, self.blfields['use_range'], toggle=True, text='Range') #################### # - Events #################### @events.on_value_changed( prop_name={'active_socket_set', 'use_range'}, - props='use_range', + props={'use_range'}, run_on_init=True, ) def on_use_range_changed(self, props: dict) -> None: @@ -128,7 +129,8 @@ class WaveConstantNode(base.MaxwellSimNode): ) def compute_wl_value(self, input_sockets: dict) -> sp.Expr: """Compute a single wavelength value from either wavelength/frequency.""" - if input_sockets['WL'] is not None: + has_wl = not ct.FlowSignal.check(input_sockets['WL']) + if has_wl: return input_sockets['WL'] return sci_constants.vac_speed_of_light / input_sockets['Freq'] @@ -141,7 +143,8 @@ class WaveConstantNode(base.MaxwellSimNode): ) def compute_freq_value(self, input_sockets: dict) -> sp.Expr: """Compute a single frequency value from either wavelength/frequency.""" - if input_sockets['Freq'] is not None: + has_freq = not ct.FlowSignal.check(input_sockets['Freq']) + if has_freq: return input_sockets['Freq'] return sci_constants.vac_speed_of_light / input_sockets['WL'] @@ -158,11 +161,20 @@ class WaveConstantNode(base.MaxwellSimNode): ) def compute_wl_range(self, input_sockets: dict) -> sp.Expr: """Compute wavelength range from either wavelength/frequency ranges.""" - if input_sockets['WL'] is not None: + has_wl = not ct.FlowSignal.check(input_sockets['WL']) + if has_wl: return input_sockets['WL'] - return input_sockets['Freq'].rescale_bounds( - lambda bound: sci_constants.vac_speed_of_light / bound, reverse=True + freq = input_sockets['Freq'] + return ct.LazyArrayRangeFlow( + start=spux.scale_to_unit( + sci_constants.vac_speed_of_light / (freq.stop * freq.unit), spu.um + ), + stop=spux.scale_to_unit( + sci_constants.vac_speed_of_light / (freq.start * freq.unit), spu.um + ), + steps=freq.steps, + unit=spu.um, ) @events.computes_output_socket( @@ -177,11 +189,20 @@ class WaveConstantNode(base.MaxwellSimNode): ) def compute_freq_range(self, input_sockets: dict) -> sp.Expr: """Compute frequency range from either wavelength/frequency ranges.""" - if input_sockets['Freq'] is not None: + has_freq = not ct.FlowSignal.check(input_sockets['Freq']) + if has_freq: return input_sockets['Freq'] - return input_sockets['WL'].rescale_bounds( - lambda bound: sci_constants.vac_speed_of_light / bound, reverse=True + wl = input_sockets['WL'] + return ct.LazyArrayRangeFlow( + start=spux.scale_to_unit( + sci_constants.vac_speed_of_light / (wl.stop * wl.unit), spux.THz + ), + stop=spux.scale_to_unit( + sci_constants.vac_speed_of_light / (wl.start * wl.unit), spux.THz + ), + steps=wl.steps, + unit=spux.THz, ) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py index 608d570..eae2538 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py @@ -35,7 +35,7 @@ class LoadCloudSim(bpy.types.Operator): node = context.node # Try Loading Simulation Data - node.sim_data = bl_cache.Signal.InvalidateCache + #node.sim_data = bl_cache.Signal.InvalidateCache sim_data = node.sim_data if sim_data is None: self.report( @@ -70,18 +70,26 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode): should_exist=True, ), } + output_sockets: typ.ClassVar = { + 'Sim Data': sockets.MaxwellFDTDSimDataSocketDef(), + } + #################### + # - Properties + #################### sim_data_loaded: bool = bl_cache.BLField(False) - @bl_cache.cached_bl_property() + #################### + # - Computed + #################### + @property def sim_data(self) -> td.SimulationData | None: cloud_task = self._compute_input( 'Cloud Task', kind=ct.FlowKind.Value, optional=True ) + has_cloud_task = not ct.FlowSignal.check(cloud_task) if ( - # Check Flow - not ct.FlowSignal.check(cloud_task) - # Check Task + has_cloud_task and cloud_task is not None and isinstance(cloud_task, tdcloud.CloudTask) and cloud_task.status == 'success' @@ -97,7 +105,7 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode): #################### # - UI #################### - def draw_operators(self, context, layout): + def draw_operators(self, _: bpy.types.Context, layout: bpy.types.UILayout): if self.sim_data_loaded: layout.operator(ct.OperatorType.NodeLoadCloudSim, text='Reload Sim') else: @@ -106,11 +114,6 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode): #################### # - Events #################### - @events.on_value_changed(socket_name='Cloud Task') - def on_cloud_task_changed(self): - self.inputs['Cloud Task'].on_cloud_updated() - ## TODO: Must we babysit sockets like this? - @events.on_value_changed( prop_name='sim_data_loaded', run_on_init=True, props={'sim_data_loaded'} ) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py index 99029c9..8892be8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/eh_field_monitor.py @@ -33,6 +33,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode): 'Size': sockets.ExprSocketDef( shape=(3,), physical_type=spux.PhysicalType.Length, + default_value=sp.Matrix([1, 1, 1]), ), 'Spatial Subdivs': sockets.ExprSocketDef( shape=(3,), @@ -124,11 +125,30 @@ class EHFieldMonitorNode(base.MaxwellSimNode): # - Preview #################### @events.on_value_changed( - socket_name={'Center', 'Size'}, + # Trigger prop_name='preview_active', + # Loaded + managed_objs={'mesh'}, props={'preview_active'}, input_sockets={'Center', 'Size'}, + ) + def on_preview_changed(self, managed_objs, props, input_sockets): + """Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen.""" + mesh = managed_objs['mesh'] + + # Push Preview State to Managed Mesh + if props['preview_active']: + mesh.show_preview() + else: + mesh.hide_preview() + + @events.on_value_changed( + # Trigger + socket_name={'Center', 'Size'}, + run_on_init=True, + # Loaded managed_objs={'mesh', 'modifier'}, + input_sockets={'Center', 'Size'}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, scale_input_sockets={ 'Center': 'BlenderUnits', @@ -136,7 +156,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode): ) def on_inputs_changed( self, - props: dict, managed_objs: dict, input_sockets: dict, unit_systems: dict, @@ -153,9 +172,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode): }, }, ) - # Push Preview State - if props['preview_active']: - managed_objs['mesh'].show_preview() #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py index 899473a..9e668e0 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/monitors/field_power_flux_monitor.py @@ -31,6 +31,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode): 'Size': sockets.ExprSocketDef( shape=(3,), physical_type=spux.PhysicalType.Length, + default_value=sp.Matrix([1, 1, 1]), ), 'Samples/Space': sockets.ExprSocketDef( shape=(3,), @@ -123,11 +124,29 @@ class PowerFluxMonitorNode(base.MaxwellSimNode): # - Preview - Changes to Input Sockets #################### @events.on_value_changed( - socket_name={'Center', 'Size'}, + # Trigger prop_name='preview_active', + # Loaded + managed_objs={'mesh'}, props={'preview_active'}, - input_sockets={'Center', 'Size'}, + ) + def on_preview_changed(self, managed_objs, props): + """Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen.""" + mesh = managed_objs['mesh'] + + # Push Preview State to Managed Mesh + if props['preview_active']: + mesh.show_preview() + else: + mesh.hide_preview() + + @events.on_value_changed( + # Trigger + socket_name={'Center', 'Size'}, + run_on_init=True, + # Loaded managed_objs={'mesh', 'modifier'}, + input_sockets={'Center', 'Size'}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, scale_input_sockets={ 'Center': 'BlenderUnits', @@ -135,7 +154,6 @@ class PowerFluxMonitorNode(base.MaxwellSimNode): ) def on_inputs_changed( self, - props: dict, managed_objs: dict, input_sockets: dict, unit_systems: dict, @@ -152,9 +170,6 @@ class PowerFluxMonitorNode(base.MaxwellSimNode): }, }, ) - # Push Preview State - if props['preview_active']: - managed_objs['mesh'].show_preview() #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py index 5cbc8f8..67fb705 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/__init__.py @@ -1,12 +1,13 @@ -from . import file_exporters, viewer, web_exporters +#from . import file_exporters, viewer, web_exporters +from . import viewer BL_REGISTER = [ *viewer.BL_REGISTER, - *file_exporters.BL_REGISTER, - *web_exporters.BL_REGISTER, + #*file_exporters.BL_REGISTER, + #*web_exporters.BL_REGISTER, ] BL_NODES = { **viewer.BL_NODES, - **file_exporters.BL_NODES, - **web_exporters.BL_NODES, + #**file_exporters.BL_NODES, + #**web_exporters.BL_NODES, } diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py index 9708a6a..a565cfd 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -217,7 +217,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket): Called by `self.on_prop_changed()` when `self.active_kind` was changed. """ self.display_shape = ( - 'SQUARE' if self.active_kind == ct.FlowKind.LazyValueRange else 'CIRCLE' + 'SQUARE' if self.active_kind == ct.FlowKind.LazyArrayRange else 'CIRCLE' ) # + ('_DOT' if self.use_units else '') ## TODO: Valid Active Kinds should be a subset/subenum(?) of FlowKind diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py index ab5b285..e1110bd 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/expr.py @@ -2,6 +2,7 @@ import enum import typing as typ import bpy +import pydantic as pyd import sympy as sp from blender_maxwell.utils import bl_cache, logger @@ -63,6 +64,7 @@ class InfoDisplayCol(enum.StrEnum): class ExprBLSocket(base.MaxwellSimSocket): socket_type = ct.SocketType.Expr bl_label = 'Expr' + use_info_draw = True #################### # - Properties @@ -70,7 +72,7 @@ class ExprBLSocket(base.MaxwellSimSocket): shape: tuple[int, ...] | None = bl_cache.BLField(None) mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real, prop_ui=True) physical_type: spux.PhysicalType | None = bl_cache.BLField(None) - symbols: frozenset[spux.Symbol] = bl_cache.BLField(frozenset()) + symbols: frozenset[sp.Symbol] = bl_cache.BLField(frozenset()) active_unit: enum.Enum = bl_cache.BLField( None, enum_cb=lambda self, _: self.search_units(), prop_ui=True @@ -102,7 +104,7 @@ class ExprBLSocket(base.MaxwellSimSocket): ) # UI: LazyArrayRange - steps: int = bl_cache.BLField(2, abs_min=2) + steps: int = bl_cache.BLField(2, abs_min=2, prop_ui=True) ## Expression raw_min_spstr: str = bl_cache.BLField('', prop_ui=True) raw_max_spstr: str = bl_cache.BLField('', prop_ui=True) @@ -125,6 +127,15 @@ class ExprBLSocket(base.MaxwellSimSocket): #################### # - Computed: Raw Expressions #################### + @property + def sorted_symbols(self) -> list[sp.Symbol]: + """Retrieves all symbols and sorts them by name. + + Returns: + Repeateably ordered list of symbols. + """ + return sorted(self.symbols, key=lambda sym: sym.name) + @property def raw_value_sp(self) -> spux.SympyExpr: return self._parse_expr_str(self.raw_value_spstr) @@ -140,7 +151,7 @@ class ExprBLSocket(base.MaxwellSimSocket): #################### # - Computed: Units #################### - def search_units(self, _: bpy.types.Context) -> list[ct.BLEnumElement]: + def search_units(self) -> list[ct.BLEnumElement]: if self.physical_type is not None: return [ (sp.sstr(unit), spux.sp_to_str(unit), sp.sstr(unit), '', i) @@ -163,33 +174,38 @@ class ExprBLSocket(base.MaxwellSimSocket): return None @unit.setter - def unit(self, unit: spux.Unit) -> None: + def unit(self, unit: spux.Unit | None) -> None: """Set the unit, without touching the `raw_*` UI properties. Notes: To set a new unit, **and** convert the `raw_*` UI properties to the new unit, use `self.convert_unit()` instead. """ - if 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}")' - raise ValueError(msg) + if self.physical_type is not None: + if unit in self.physical_type.valid_units: + self.active_unit = sp.sstr(unit) + else: + msg = f'Tried to set invalid unit {unit} (physical type "{self.physical_type}" only supports "{self.physical_type.valid_units}")' + raise ValueError(msg) + elif unit is not None: + msg = f'Tried to set invalid unit {unit} (physical type is {self.physical_type}, and has no unit support!)")' + raise ValueError(msg) def convert_unit(self, unit_to: spux.Unit) -> None: - if self.active_kind == ct.FlowKind.Value: - current_value = self.value - self.unit = unit_to - self.value = current_value - elif self.active_kind == ct.FlowKind.LazyArrayRange: - current_lazy_array_range = self.lazy_array_range - self.unit = unit_to - self.lazy_array_range = current_lazy_array_range + current_value = self.value + current_lazy_array_range = self.lazy_array_range + + self.unit = bl_cache.Signal.InvalidateCache + + self.value = current_value + self.lazy_array_range = current_lazy_array_range #################### # - Property Callback #################### def on_socket_prop_changed(self, prop_name: str) -> None: - if prop_name == 'unit' and self.active_unit is not None: + if prop_name == 'physical_type': + self.active_unit = bl_cache.Signal.ResetEnumItems + if prop_name == 'active_unit' and self.active_unit is not None: self.convert_unit(spux.unit_str_to_unit(self.active_unit)) #################### @@ -200,23 +216,23 @@ class ExprBLSocket(base.MaxwellSimSocket): ) -> tuple[spux.MathType, tuple[int, ...] | None, spux.UnitDimension]: # Parse MathType mathtype = spux.MathType.from_expr(expr) - if self.mathtype != mathtype: + if not self.mathtype.is_compatible(mathtype): msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with mathtype {mathtype}' raise ValueError(msg) # Parse Symbols - if expr.free_symbols: - if self.mathtype is not None: - msg = f'MathType is {self.mathtype}, but tried to set expr {expr} with free symbols {expr.free_symbols}' - raise ValueError(msg) - - if not expr.free_symbols.issubset(self.symbols): - msg = f'Tried to set expr {expr} with free symbols {expr.free_symbols}, which is incompatible with socket symbols {self.symbols}' - raise ValueError(msg) + if expr.free_symbols and not expr.free_symbols.issubset(self.symbols): + msg = f'Tried to set expr {expr} with free symbols {expr.free_symbols}, which is incompatible with socket symbols {self.symbols}' + raise ValueError(msg) # Parse Dimensions shape = spux.parse_shape(expr) - if shape != self.shape: + if shape != self.shape and not ( + shape is not None + and self.shape is not None + and len(self.shape) == 1 + and 1 in shape + ): msg = f'Expr {expr} has shape {shape}, which is incompatible with the expr socket (shape {self.shape})' raise ValueError(msg) @@ -238,7 +254,7 @@ class ExprBLSocket(base.MaxwellSimSocket): # Try Parsing and Returning the Expression try: self._parse_expr_info(expr) - except ValueError(expr) as ex: + except ValueError: log.exception( 'Couldn\'t parse expression "%s" in Expr socket.', expr_spstr, @@ -270,6 +286,7 @@ class ExprBLSocket(base.MaxwellSimSocket): expr = self.raw_value_sp if expr is None: return ct.FlowSignal.FlowPending + return expr MT_Z = spux.MathType.Integer MT_Q = spux.MathType.Rational @@ -312,7 +329,7 @@ class ExprBLSocket(base.MaxwellSimSocket): Notes: Called to set the internal `FlowKind.Value` of this socket. """ - mathtype, shape = self._parse_expr_info(expr) + _mathtype, _shape = self._parse_expr_info(expr) if self.symbols or self.shape not in [None, (2,), (3,)]: self.raw_value_spstr = sp.sstr(expr) @@ -321,32 +338,33 @@ class ExprBLSocket(base.MaxwellSimSocket): MT_Q = spux.MathType.Rational MT_R = spux.MathType.Real MT_C = spux.MathType.Complex - if shape is None: - if mathtype == MT_Z: + if self.shape is None: + if self.mathtype == MT_Z: self.raw_value_int = self._to_raw_value(expr) - elif mathtype == MT_Q: + elif self.mathtype == MT_Q: self.raw_value_rat = self._to_raw_value(expr) - elif mathtype == MT_R: + elif self.mathtype == MT_R: self.raw_value_float = self._to_raw_value(expr) - elif mathtype == MT_C: + elif self.mathtype == MT_C: self.raw_value_complex = self._to_raw_value(expr) - elif shape == (2,): - if mathtype == MT_Z: + elif self.shape == (2,): + if self.mathtype == MT_Z: self.raw_value_int2 = self._to_raw_value(expr) - elif mathtype == MT_Q: + elif self.mathtype == MT_Q: self.raw_value_rat2 = self._to_raw_value(expr) - elif mathtype == MT_R: + elif self.mathtype == MT_R: self.raw_value_float2 = self._to_raw_value(expr) - elif mathtype == MT_C: + elif self.mathtype == MT_C: self.raw_value_complex2 = self._to_raw_value(expr) - elif shape == (3,): - if mathtype == MT_Z: + elif self.shape == (3,): + log.critical(expr) + if self.mathtype == MT_Z: self.raw_value_int3 = self._to_raw_value(expr) - elif mathtype == MT_Q: + elif self.mathtype == MT_Q: self.raw_value_rat3 = self._to_raw_value(expr) - elif mathtype == MT_R: + elif self.mathtype == MT_R: self.raw_value_float3 = self._to_raw_value(expr) - elif mathtype == MT_C: + elif self.mathtype == MT_C: self.raw_value_complex3 = self._to_raw_value(expr) #################### @@ -404,7 +422,6 @@ class ExprBLSocket(base.MaxwellSimSocket): Called to compute the internal `FlowKind.LazyArrayRange` of this socket. """ self.steps = value.steps - self.unit = value.unit if self.symbols: self.raw_min_spstr = sp.sstr(value.start) @@ -416,21 +433,26 @@ class ExprBLSocket(base.MaxwellSimSocket): MT_R = spux.MathType.Real MT_C = spux.MathType.Complex + unit = value.unit if value.unit is not None else 1 if value.mathtype == MT_Z: self.raw_range_int = [ - self._to_raw_value(bound) for bound in [value.start, value.stop] + self._to_raw_value(bound * unit) + for bound in [value.start, value.stop] ] elif value.mathtype == MT_Q: self.raw_range_rat = [ - self._to_raw_value(bound) for bound in [value.start, value.stop] + self._to_raw_value(bound * unit) + for bound in [value.start, value.stop] ] elif value.mathtype == MT_R: self.raw_range_float = [ - self._to_raw_value(bound) for bound in [value.start, value.stop] + self._to_raw_value(bound * unit) + for bound in [value.start, value.stop] ] elif value.mathtype == MT_C: self.raw_range_complex = [ - self._to_raw_value(bound) for bound in [value.start, value.stop] + self._to_raw_value(bound * unit) + for bound in [value.start, value.stop] ] #################### @@ -441,8 +463,8 @@ class ExprBLSocket(base.MaxwellSimSocket): # Lazy Value: Arbitrary Expression if self.symbols or self.shape not in [None, (2,), (3,)]: return ct.LazyValueFuncFlow( - func=sp.lambdify(self.symbols, self.value, 'jax'), - func_args=[spux.MathType.from_expr(sym) for sym in self.symbols], + func=sp.lambdify(self.sorted_symbols, self.value, 'jax'), + func_args=[spux.MathType.from_expr(sym) for sym in self.sorted_symbols], supports_jax=True, ) @@ -482,8 +504,7 @@ class ExprBLSocket(base.MaxwellSimSocket): unit=self.unit, ) - msg = "Expr socket can't produce array from expression with free symbols" - raise ValueError(msg) + return ct.FlowSignal.NoFlow #################### # - FlowKind: Info @@ -496,6 +517,7 @@ class ExprBLSocket(base.MaxwellSimSocket): output_mathtype=self.mathtype, output_unit=self.unit, ) + ## TODO: When expression can be used w/arrays, then allow directly outputting a LazyArrayRange pumped through the given expression. Or something like that. #################### # - FlowKind: Capabilities @@ -520,10 +542,11 @@ class ExprBLSocket(base.MaxwellSimSocket): _row.label(text=text) _col = split.column(align=True) - _col.prop(self, 'active_unit', text='') + _col.prop(self, self.blfields['active_unit'], text='') + else: + row.label(text=text) def draw_value(self, col: bpy.types.UILayout) -> None: - # Property Interface if self.symbols: col.prop(self, self.blfields['raw_value_spstr'], text='') @@ -575,6 +598,27 @@ class ExprBLSocket(base.MaxwellSimSocket): for sym in self.symbols: col.label(text=spux.pretty_symbol(sym)) + def draw_lazy_array_range(self, col: bpy.types.UILayout) -> None: + if self.symbols: + col.prop(self, self.blfields['raw_min_spstr'], text='') + col.prop(self, self.blfields['raw_max_spstr'], text='') + + else: + MT_Z = spux.MathType.Integer + MT_Q = spux.MathType.Rational + MT_R = spux.MathType.Real + MT_C = spux.MathType.Complex + if self.mathtype == MT_Z: + col.prop(self, self.blfields['raw_range_int'], text='') + elif self.mathtype == MT_Q: + col.prop(self, self.blfields['raw_range_rat'], text='') + elif self.mathtype == MT_R: + col.prop(self, self.blfields['raw_range_float'], text='') + elif self.mathtype == MT_C: + col.prop(self, self.blfields['raw_range_complex'], text='') + + col.prop(self, self.blfields['steps'], text='') + def draw_input_label_row(self, row: bpy.types.UILayout, text) -> None: info = self.compute_data(kind=ct.FlowKind.Info) has_dims = not ct.FlowSignal.check(info) and info.dim_names @@ -630,7 +674,7 @@ class ExprBLSocket(base.MaxwellSimSocket): _row.label(text=text) def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None: - if info.dim_names and self.show_info_columns: + if self.show_info_columns: row = col.row() box = row.box() grid = box.grid_flow( @@ -696,19 +740,87 @@ class ExprSocketDef(base.SocketDef): default_unit: spux.Unit | None = None # FlowKind: Value - default_value: spux.SympyExpr = sp.S(0) + default_value: spux.SympyExpr = sp.RealNumber(0) # FlowKind: LazyArrayRange - default_min: spux.SympyExpr = sp.S(0) - default_max: spux.SympyExpr = sp.S(1) + default_min: spux.SympyExpr = sp.RealNumber(0) + default_max: spux.SympyExpr = sp.RealNumber(1) default_steps: int = 2 ## TODO: Configure lin/log/... scaling (w/enumprop in UI) - ## TODO: Buncha validation :) - # UI show_info_columns: bool = False + #################### + # - Validators - Coersion + #################### + @pyd.model_validator(mode='after') + def shape_value_coersion(self) -> str: + if self.shape is not None and not isinstance(self.default_value, sp.MatrixBase): + if len(self.shape) == 1: + self.default_value = self.default_value * sp.Matrix.ones( + self.shape[0], 1 + ) + if len(self.shape) == 2: + self.default_value = self.default_value * sp.Matrix.ones(*self.shape) + + return self + + @pyd.model_validator(mode='after') + def unit_coersion(self) -> str: + if self.physical_type is not None and self.default_unit is None: + self.default_unit = self.physical_type.default_unit + + return self + + #################### + # - Validators - Assertion + #################### + @pyd.model_validator(mode='after') + def valid_shapes(self) -> str: + if self.active_kind == ct.FlowKind.LazyArrayRange and self.shape is not None: + msg = "Can't have a non-None shape when LazyArrayRange is set as the active kind." + raise ValueError(msg) + + return self + + @pyd.model_validator(mode='after') + def mathtype_value(self) -> str: + default_value_mathtype = spux.MathType.from_expr(self.default_value) + if not self.mathtype.is_compatible(default_value_mathtype): + msg = f'MathType is {self.mathtype}, but tried to set default value {self.default_value} with mathtype {default_value_mathtype}' + raise ValueError(msg) + + return self + + @pyd.model_validator(mode='after') + def symbols_value(self) -> str: + if ( + self.default_value.free_symbols + and not self.default_value.free_symbols.issubset(self.symbols) + ): + msg = f'Tried to set default value {self.default_value} with free symbols {self.default_value.free_symbols}, which is incompatible with socket symbols {self.symbols}' + raise ValueError(msg) + + return self + + @pyd.model_validator(mode='after') + def shape_value(self) -> str: + shape = spux.parse_shape(self.default_value) + if shape != self.shape and not ( + shape is not None + and self.shape is not None + and len(self.shape) == 1 + and 1 in shape + ): + msg = f'Default value {self.default_value} has shape {shape}, which is incompatible with the expr socket (shape {self.shape})' + raise ValueError(msg) + + return self + + #################### + # - Initialization + #################### def init(self, bl_socket: ExprBLSocket) -> None: bl_socket.active_kind = self.active_kind @@ -718,12 +830,13 @@ class ExprSocketDef(base.SocketDef): bl_socket.physical_type = self.physical_type bl_socket.symbols = self.symbols - # Socket Units - if self.default_unit is not None: + # Socket Units & FlowKind.Value + log.critical(self) + if self.physical_type is not None: bl_socket.unit = self.default_unit - - # FlowKind: Value - bl_socket.value = self.default_value + bl_socket.value = self.default_value * self.default_unit + else: + bl_socket.value = self.default_value # FlowKind: LazyArrayRange bl_socket.lazy_array_range = ct.LazyArrayRangeFlow( diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py index 211f652..1147c0f 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py @@ -96,6 +96,23 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): new_task_name: str = bl_cache.BLField('', prop_ui=True) + #################### + # - Property Changes + #################### + def on_socket_prop_changed(self, prop_name: str) -> None: + if prop_name in [ + 'api_key', + 'existing_folder_id', + 'existing_task_id', + 'new_task_name', + 'should_exist', + ]: + self.existing_folder_id = bl_cache.Signal.ResetEnumItems + self.existing_task_id = bl_cache.Signal.ResetEnumItems + + #################### + # - FlowKinds + #################### @property def capabilities(self) -> ct.CapabilitiesFlow: return ct.CapabilitiesFlow( @@ -122,7 +139,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): return (self.new_task_name, cloud_folder) # No Task Selected: Return None - if self.existing_task_id == 'NONE': + if self.existing_task_id is None: return None # Retrieve Cloud Task @@ -135,7 +152,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): return cloud_task - return None + return ct.FlowSignal.FlowPending #################### # - Searchers @@ -158,7 +175,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): return [] def search_cloud_tasks(self) -> list[ct.BLEnumElement]: - if self.existing_folder_id == 'NONE' or not tdcloud.IS_AUTHENTICATED: + if self.existing_folder_id is None or not tdcloud.IS_AUTHENTICATED: return [] # Get Cloud Folder @@ -221,10 +238,6 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): def on_prepare_new_task(self): self.should_exist = False - def on_cloud_updated(self): - self.existing_folder_id = bl_cache.Signal.ResetEnumItems - self.existing_task_id = bl_cache.Signal.ResetEnumItems - #################### # - UI #################### diff --git a/src/blender_maxwell/nodeps/utils/blender_type_enum.py b/src/blender_maxwell/nodeps/utils/blender_type_enum.py index 238e6d6..8f964cd 100644 --- a/src/blender_maxwell/nodeps/utils/blender_type_enum.py +++ b/src/blender_maxwell/nodeps/utils/blender_type_enum.py @@ -13,6 +13,7 @@ def prefix_values_with(prefix: str) -> type[enum.Enum]: Returns: A new StrEnum class with altered member values. """ + ## TODO: DO NOT USE FOR ENUMS WITH METHODS def _decorator(cls: enum.StrEnum): new_members = { diff --git a/src/blender_maxwell/utils/bl_cache.py b/src/blender_maxwell/utils/bl_cache.py index 3464793..3c6f794 100644 --- a/src/blender_maxwell/utils/bl_cache.py +++ b/src/blender_maxwell/utils/bl_cache.py @@ -547,6 +547,9 @@ class BLField: self._str_cb = str_cb self._enum_cb = enum_cb + ## Type Coercion + self._coerce_output_to = None + ## Vector/Matrix Identity ## -> Matrix Shape assists in the workaround for Matrix Display Bug self._is_vector = False @@ -797,7 +800,11 @@ class BLField: } ## StrEnum - elif inspect.isclass(AttrType) and issubclass(AttrType, enum.StrEnum): + elif ( + inspect.isclass(AttrType) + and issubclass(AttrType, enum.StrEnum) + and self._enum_cb is None + ): default_value = self._default_value BLProp = bpy.props.EnumProperty kwargs_prop |= { @@ -814,9 +821,14 @@ class BLField: } if self._enum_many: kwargs_prop['options'].add('ENUM_FLAG') + self._coerce_output_to = AttrType ## Dynamic Enum - elif AttrType is enum.Enum and self._enum_cb is not None: + elif ( + AttrType is enum.Enum + or (inspect.isclass(AttrType) and issubclass(AttrType, enum.StrEnum)) + and self._enum_cb is not None + ): if self._default_value is not None: msg = 'When using dynamic enum, default value must be None' raise ValueError(msg) @@ -828,6 +840,8 @@ class BLField: } if self._enum_many: kwargs_prop['options'].add('ENUM_FLAG') + if AttrType is not enum.Enum: + self._coerce_output_to = AttrType ## BL Reference elif AttrType in typ.get_args(ct.BLIDStruct): @@ -888,6 +902,9 @@ class BLField: def __get__( self, bl_instance: BLInstance | None, owner: type[BLInstance] ) -> typ.Any: + if bl_instance is None: + return None + value = self._cached_bl_property.__get__(bl_instance, owner) # enum.Enum: Cast Auto-Injected Dynamic Enum 'NONE' -> None @@ -913,7 +930,7 @@ class BLField: ## -> Reject modernity. Return to tuple[]. if self._is_vector: ## -> tuple()ify the np.array to respect tuple[] type annotation. - return tuple(np.array(value)) + return tuple(value) if self._is_matrix: # Matrix Display Bug: Correctly Read Row-Major Values w/Reshape @@ -921,6 +938,13 @@ class BLField: map(tuple, np.array(value).flatten().reshape(self._matrix_shape)) ) + # Coerce Output + ## -> Mainly useful for getting the "real" StrEnum back. + if self._coerce_output_to is not None and value is not None: + if self._enum_many: + return {self._coerce_output_to(v) for v in value} + return self._coerce_output_to(value) + return value def __set__(self, bl_instance: BLInstance | None, value: typ.Any) -> None: diff --git a/src/blender_maxwell/utils/extra_sympy_units.py b/src/blender_maxwell/utils/extra_sympy_units.py index 5af6ab7..2909848 100644 --- a/src/blender_maxwell/utils/extra_sympy_units.py +++ b/src/blender_maxwell/utils/extra_sympy_units.py @@ -25,6 +25,10 @@ from pydantic_core import core_schema as pyd_core_schema from blender_maxwell import contracts as ct +from . import logger + +log = logger.get(__name__) + SympyType = ( sp.Basic | sp.Expr @@ -47,21 +51,42 @@ class MathType(enum.StrEnum): Real = enum.auto() Complex = enum.auto() + @staticmethod def combine(*mathtypes: list[typ.Self]) -> typ.Self: if MathType.Complex in mathtypes: return MathType.Complex - elif MathType.Real in mathtypes: + if MathType.Real in mathtypes: return MathType.Real - elif MathType.Rational in mathtypes: + if MathType.Rational in mathtypes: return MathType.Rational - elif MathType.Integer in mathtypes: + if MathType.Integer in mathtypes: return MathType.Integer - elif MathType.Bool in mathtypes: + if MathType.Bool in mathtypes: return MathType.Bool + msg = f"Can't combine mathtypes {mathtypes}" + raise ValueError(msg) + + def is_compatible(self, other: typ.Self) -> bool: + MT = MathType + return ( + other + in { + MT.Bool: [MT.Bool], + MT.Integer: [MT.Integer], + MT.Rational: [MT.Integer, MT.Rational], + MT.Real: [MT.Integer, MT.Rational, MT.Real], + MT.Complex: [MT.Integer, MT.Rational, MT.Real, MT.Complex], + }[self] + ) + @staticmethod def from_expr(sp_obj: SympyType) -> type: - ## TODO: Support for sp.Matrix + if isinstance(sp_obj, sp.MatrixBase): + return MathType.combine( + *[MathType.from_expr(v) for v in sp.flatten(sp_obj)] + ) + if isinstance(sp_obj, sp.logic.boolalg.Boolean): return MathType.Bool if sp_obj.is_integer: @@ -172,7 +197,7 @@ class NumberSize1D(enum.StrEnum): None: NS.Scalar, (2,): NS.Vec2, (3,): NS.Vec3, - (4,): NS.Vec3, + (4,): NS.Vec4, }[shape] @property @@ -182,7 +207,7 @@ class NumberSize1D(enum.StrEnum): NS.Scalar: None, NS.Vec2: (2,), NS.Vec3: (3,), - NS.Vec3: (4,), + NS.Vec4: (4,), }[self] @@ -702,7 +727,6 @@ def scale_to_unit(sp_obj: SympyType, unit: spu.Quantity) -> Number: Raises: ValueError: If the result of unit-conversion and -stripping still has units, as determined by `uses_units()`. """ - ## TODO: An LFU cache could do better than an LRU. unitless_expr = spu.convert_to(sp_obj, unit) / unit if not uses_units(unitless_expr): return unitless_expr @@ -739,7 +763,7 @@ def unit_str_to_unit(unit_str: str) -> Unit | None: if unit_str in _UNIT_STR_MAP: return _UNIT_STR_MAP[unit_str] - msg = 'No valid unit for unit string {unit_str}' + msg = f'No valid unit for unit string {unit_str}' raise ValueError(msg) @@ -802,7 +826,7 @@ class PhysicalType(enum.StrEnum): # Global PT.Time: Dims.time, PT.Angle: Dims.angle, - PT.SolidAngle: Dims.steradian, ## MISSING + PT.SolidAngle: spu.steradian.dimension, ## MISSING PT.Freq: Dims.frequency, PT.AngFreq: Dims.angle * Dims.frequency, # Cartesian @@ -836,7 +860,7 @@ class PhysicalType(enum.StrEnum): PT.HField: Dims.current / Dims.length, # Luminal PT.LumIntensity: Dims.luminous_intensity, - PT.LumFlux: Dims.luminous_intensity * Dims.steradian, + PT.LumFlux: Dims.luminous_intensity * spu.steradian.dimension, PT.Illuminance: Dims.luminous_intensity / Dims.length**2, # Optics PT.OrdinaryWaveVector: Dims.frequency, @@ -1263,12 +1287,23 @@ def convert_to_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyE return spu.convert_to(sp_obj, _flat_unit_system_units(unit_system)) +def strip_unit_system(sp_obj: SympyExpr, unit_system: UnitSystem) -> SympyExpr: + """Strip units occurring in the given unit system from the expression. + + Unit stripping is a "dumb" operation: "Substitute any `sympy` object in `unit_system.values()` with `1`". + Obviously, the semantic correctness of this operation depends entirely on _the units adding no semantic meaning to the expression_. + + Notes: + You should probably use `scale_to_unit_system()` or `convert_to_unit_system()`. + """ + return sp_obj.subs({unit: 1 for unit in unit_system.values()}) + + def scale_to_unit_system( sp_obj: SympyExpr, unit_system: UnitSystem, use_jax_array: bool = False ) -> int | float | complex | tuple | jax.Array: """Convert an expression to the units of a given unit system, then strip all units of the unit system. - Unit stripping is "dumb": Substitute any `sympy` object in `unit_system.values()` with `1`. Afterwards, it is converted to an appropriate Python type. Notes: @@ -1287,8 +1322,6 @@ def scale_to_unit_system( If the returned type is array-like, and `use_jax_array` is specified, then (and **only** then) will a `jax.Array` be returned instead of a nested `tuple`. """ return sympy_to_python( - convert_to_unit_system(sp_obj, unit_system).subs( - {unit: 1 for unit in unit_system.values()} - ), + strip_unit_system(convert_to_unit_system(sp_obj, unit_system), unit_system), use_jax_array=use_jax_array, ) diff --git a/src/blender_maxwell/utils/serialize.py b/src/blender_maxwell/utils/serialize.py index cf9dac1..7d0234b 100644 --- a/src/blender_maxwell/utils/serialize.py +++ b/src/blender_maxwell/utils/serialize.py @@ -81,6 +81,7 @@ _NaivelyEncodableTypeSet = frozenset(typ.get_args(NaivelyEncodableType)) class TypeID(enum.StrEnum): Complex: str = '!type=complex' SympyType: str = '!type=sympytype' + SympyExpr: str = '!type=sympyexpr' SocketDef: str = '!type=socketdef' ManagedObj: str = '!type=managedobj'