diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py index 8887f87..c8c4d67 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/node_types.py @@ -38,8 +38,6 @@ class NodeType(blender_type_enum.BlenderTypeEnum): Scene = enum.auto() ## Inputs / Constants ExprConstant = enum.auto() - NumberConstant = enum.auto() - PhysicalConstant = enum.auto() ScientificConstant = enum.auto() UnitSystemConstant = enum.auto() BlenderConstant = enum.auto() diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/sim_types.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/sim_types.py index b30ea44..5169f1e 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/sim_types.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/sim_types.py @@ -21,9 +21,11 @@ import enum import typing as typ import jax.numpy as jnp +import sympy as sp import tidy3d as td from blender_maxwell.services import tdcloud +from blender_maxwell.utils import extra_sympy_units as spux #################### diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py index 218cd2b..2789a4e 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/managed_objs/managed_bl_image.py @@ -47,8 +47,13 @@ class ManagedBLImage(base.ManagedObj): managed_obj_type = ct.ManagedObjType.ManagedBLImage _bl_image_name: str - def __init__(self, name: str): - self._bl_image_name = name + def __init__(self, name: str, prev_name: str | None = None): + if prev_name is not None: + self._bl_image_name = prev_name + else: + self._bl_image_name = name + + self.name = name @property def name(self): @@ -57,26 +62,29 @@ class ManagedBLImage(base.ManagedObj): @name.setter def name(self, value: str): log.info( - 'Setting ManagedBLImage from "%s" to "%s"', + 'Changing ManagedBLImage from "%s" to "%s"', self.name, value, ) - current_bl_image = bpy.data.images.get(self._bl_image_name) - wanted_bl_image = bpy.data.images.get(value) + existing_bl_image = bpy.data.images.get(self.name) - # Yoink Image Name - if current_bl_image is None and wanted_bl_image is None: + # No Existing Image: Set Value to Name + if existing_bl_image is None: self._bl_image_name = value - # Alter Image Name - elif current_bl_image is not None and wanted_bl_image is None: + # Existing Image: Rename to New Name + else: + existing_bl_image.name = value self._bl_image_name = value - current_bl_image.name = value - # Overlapping Image Name - elif wanted_bl_image is not None: - msg = f'ManagedBLImage "{self._bl_image_name}" could not change its name to "{value}", since it already exists.' - raise ValueError(msg) + # Check: Blender Rename -> Synchronization Error + ## -> We can't do much else than report to the user & free(). + if existing_bl_image.name != self._bl_image_name: + log.critical( + 'BLImage: Failed to set name of %s to %s, as %s already exists.' + ) + self._bl_image_name = existing_bl_image.name + self.free() def free(self): bl_image = bpy.data.images.get(self.name) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/expr_constant.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/expr_constant.py index 3b19980..ac248cd 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/expr_constant.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/expr_constant.py @@ -16,6 +16,8 @@ import typing as typ +import sympy as sp + from .... import contracts as ct from .... import sockets from ... import base, events @@ -26,27 +28,69 @@ class ExprConstantNode(base.MaxwellSimNode): bl_label = 'Expr Constant' input_sockets: typ.ClassVar = { - 'Expr': sockets.ExprSocketDef(), + 'Expr': sockets.ExprSocketDef( + active_kind=ct.FlowKind.LazyValueFunc, + ), } output_sockets: typ.ClassVar = { - 'Expr': sockets.ExprSocketDef(), + 'Expr': sockets.ExprSocketDef( + active_kind=ct.FlowKind.LazyValueFunc, + show_info_columns=True, + ), } - ## TODO: Symbols (defined w/props?) - ## - Currently expr constant isn't excessively useful, since there are no variables. - ## - We'll define the #, type, name with props. - ## - We'll add loose-socket inputs as int/real/complex/physical socket (based on type) for Param. - ## - We the output expr would support `Value` (just the expression), `LazyValueFunc` (evaluate w/symbol support), `Param` (example values for symbols). + ## TODO: Allow immediately realizing any symbol, or just passing it along. + ## TODO: Alter output physical_type when the input PhysicalType changes. #################### - # - Callbacks + # - FlowKinds #################### @events.computes_output_socket( - 'Expr', kind=ct.FlowKind.Value, input_sockets={'Expr'} + # Trigger + 'Expr', + kind=ct.FlowKind.Value, + # Loaded + input_sockets={'Expr'}, ) def compute_value(self, input_sockets: dict) -> typ.Any: return input_sockets['Expr'] + @events.computes_output_socket( + # Trigger + 'Expr', + kind=ct.FlowKind.LazyValueFunc, + # Loaded + input_sockets={'Expr'}, + input_socket_kinds={'Expr': ct.FlowKind.LazyValueFunc}, + ) + def compute_lazy_value_func(self, input_sockets: dict) -> typ.Any: + return input_sockets['Expr'] + + #################### + # - FlowKinds: Auxiliary + #################### + @events.computes_output_socket( + # Trigger + 'Expr', + kind=ct.FlowKind.Info, + # Loaded + input_sockets={'Expr'}, + input_socket_kinds={'Expr': ct.FlowKind.Info}, + ) + def compute_info(self, input_sockets: dict) -> typ.Any: + return input_sockets['Expr'] + + @events.computes_output_socket( + # Trigger + 'Expr', + kind=ct.FlowKind.Params, + # Loaded + input_sockets={'Expr'}, + input_socket_kinds={'Expr': ct.FlowKind.Params}, + ) + def compute_params(self, input_sockets: dict) -> typ.Any: + return input_sockets['Expr'] + #################### # - Blender Registration 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 deleted file mode 100644 index 522c038..0000000 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/number_constant.py +++ /dev/null @@ -1,90 +0,0 @@ -# blender_maxwell -# Copyright (C) 2024 blender_maxwell Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import typing as typ - -import bpy - -from blender_maxwell.utils import bl_cache -from blender_maxwell.utils import extra_sympy_units as spux - -from .... import contracts as ct -from .... import sockets -from ... import base, events - - -class NumberConstantNode(base.MaxwellSimNode): - """A unitless number of configurable math type ex. integer, real, etc. . - - Attributes: - mathtype: The math type to specify the number as. - """ - - node_type = ct.NodeType.NumberConstant - bl_label = 'Numerical Constant' - - input_sockets: typ.ClassVar = { - 'Value': sockets.ExprSocketDef(), - } - output_sockets: typ.ClassVar = { - 'Value': sockets.ExprSocketDef(), - } - - #################### - # - Properties - #################### - mathtype: spux.MathType = bl_cache.BLField( - spux.MathType.Integer, - prop_ui=True, - ) - - size: spux.NumberSize1D = bl_cache.BLField( - spux.NumberSize1D.Scalar, - prop_ui=True, - ) - - #################### - # - 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={'mathtype', 'size'}, props={'mathtype', 'size'}) - 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['size'].shape - - #################### - # - FlowKind - #################### - @events.computes_output_socket('Value', input_sockets={'Value'}) - def compute_value(self, input_sockets) -> typ.Any: - return input_sockets['Value'] - - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - NumberConstantNode, -] -BL_NODES = {ct.NodeType.NumberConstant: (ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS)} 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 deleted file mode 100644 index 8897416..0000000 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/constants/physical_constant.py +++ /dev/null @@ -1,143 +0,0 @@ -# blender_maxwell -# Copyright (C) 2024 blender_maxwell Project Contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . - -import typing as typ - -import bpy -import sympy as sp - -from blender_maxwell.utils import bl_cache -from blender_maxwell.utils import extra_sympy_units as spux - -from .... import contracts, sockets -from ... import base, events - - -class PhysicalConstantNode(base.MaxwellSimNode): - """A number of configurable unit dimension, ex. time, length, etc. . - - Attributes: - physical_type: The physical type to specify. - size: The size of the physical type, if it can be a vector. - """ - - node_type = contracts.NodeType.PhysicalConstant - bl_label = 'Physical Constant' - - input_sockets: typ.ClassVar = { - 'Value': sockets.ExprSocketDef(), - } - output_sockets: typ.ClassVar = { - 'Value': sockets.ExprSocketDef(), - } - - #################### - # - Properties - #################### - physical_type: spux.PhysicalType = bl_cache.BLField( - spux.PhysicalType.Time, - prop_ui=True, - ) - - mathtype: spux.MathType = bl_cache.BLField( - enum_cb=lambda self, _: self.search_mathtypes(), - prop_ui=True, - ) - - size: spux.NumberSize1D = bl_cache.BLField( - enum_cb=lambda self, _: self.search_sizes(), - ) - - #################### - # - Searchers - #################### - def search_mathtypes(self): - return [ - mathtype.bl_enum_element(i) - for i, mathtype in enumerate(self.physical_type.valid_mathtypes) - ] - - def search_sizes(self): - return [ - spux.NumberSize1D.from_shape(shape).bl_enum_element(i) - for i, shape in enumerate(self.physical_type.valid_shapes) - if spux.NumberSize1D.has_shape(shape) - ] - - #################### - # - UI - #################### - def draw_props(self, _, col: bpy.types.UILayout) -> None: - col.prop(self, self.blfields['physical_type'], text='') - - row = col.row(align=True) - row.prop(self, self.blfields['mathtype'], text='') - row.prop(self, self.blfields['size'], text='') - - #################### - # - Events - #################### - @events.on_value_changed( - # Trigger - prop_name={'physical_type'}, - run_on_init=True, - # Loaded - props={'physical_type'}, - ) - def on_physical_type_changed(self, props) -> None: - """Change the input/output expression sockets to match the mathtype and size declared in the node.""" - # Set Input Socket Physical Type - if self.inputs['Value'].physical_type != props['physical_type']: - self.inputs['Value'].physical_type = props['physical_type'] - self.mathtype = bl_cache.Signal.ResetEnumItems - self.size = bl_cache.Signal.ResetEnumItems - - @events.on_value_changed( - # Trigger - prop_name={'mathtype', 'size'}, - run_on_init=True, - # Loaded - props={'physical_type', 'mathtype', 'size'}, - ) - def on_mathtype_or_size_changed(self, props) -> None: - # Set Input Socket Math Type - if self.inputs['Value'].mathtype != props['mathtype']: - self.inputs['Value'].mathtype = props['mathtype'] - - # Set Input Socket Shape - shape = props['size'].shape - if self.inputs['Value'].shape != shape: - self.inputs['Value'].shape = shape - - #################### - # - Callbacks - #################### - @events.computes_output_socket('Value', input_sockets={'Value'}) - def compute_value(self, input_sockets) -> sp.Expr: - return input_sockets['Value'] - - -#################### -# - Blender Registration -#################### -BL_REGISTER = [ - PhysicalConstantNode, -] -BL_NODES = { - contracts.NodeType.PhysicalConstant: ( - contracts.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS - ) -} 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 33ba169..0d5f673 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 @@ -116,13 +116,22 @@ class ExprBLSocket(base.MaxwellSimSocket): size: spux.NumberSize1D = bl_cache.BLField(spux.NumberSize1D.Scalar) mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real) physical_type: spux.PhysicalType = bl_cache.BLField(spux.PhysicalType.NonPhysical) + + # Symbols + # active_symbols: list[sim_symbols.SimSymbol] = bl_cache.BLField([]) symbols: frozenset[sp.Symbol] = bl_cache.BLField(frozenset()) + # @property + # def symbols(self) -> set[sp.Symbol]: + # """Current symbols as an unordered set.""" + # return {sim_symbol.sp_symbol for sim_symbol in self.active_symbols} + @bl_cache.cached_bl_property(depends_on={'symbols'}) def sorted_symbols(self) -> list[sp.Symbol]: - """Name-sorted symbols.""" + """Current symbols as a sorted list.""" return sorted(self.symbols, key=lambda sym: sym.name) + # Unit active_unit: enum.StrEnum = bl_cache.BLField( enum_cb=lambda self, _: self.search_valid_units(), cb_depends_on={'physical_type'}, @@ -672,16 +681,16 @@ class ExprBLSocket(base.MaxwellSimSocket): Whether information about the expression passing through a linked socket is shown is governed by `self.show_info_columns`. """ info = self.compute_data(kind=ct.FlowKind.Info) - has_dims = not ct.FlowSignal.check(info) and info.dim_names + has_info = not ct.FlowSignal.check(info) - if has_dims: + if has_info: split = row.split(factor=0.85, align=True) _row = split.row(align=False) else: _row = row _row.label(text=text) - if has_dims: + if has_info: if self.show_info_columns: _row.prop(self, self.blfields['info_columns']) @@ -735,9 +744,17 @@ class ExprBLSocket(base.MaxwellSimSocket): # - UI: Active FlowKind #################### def draw_value(self, col: bpy.types.UILayout) -> None: - """Draw the socket body for a single values/expression. + """Draw the socket body for a single value/expression. - Drawn when `self.active_kind == FlowKind.Value`. + This implements the base UI for `ExprSocket`, for when `self.size`, `self.mathtype`, `self.physical_type`, and `self.symbols` are set. + + Notes: + Drawn when `self.active_kind == FlowKind.Value`. + + Alone, `draw_value` provides no mechanism for altering expression constraints like size. + Thus, `FlowKind.Value` is a good choice for when the expression must be of a very particular type. + + However, `draw_value` may also be called by the `draw_*` methods of other `FlowKinds`, who may choose to layer more flexibility around this base UI. """ if self.symbols: col.prop(self, self.blfields['raw_value_spstr'], text='') @@ -819,23 +836,43 @@ class ExprBLSocket(base.MaxwellSimSocket): col.prop(self, self.blfields['steps'], text='') def draw_lazy_value_func(self, col: bpy.types.UILayout) -> None: - """Draw the socket body for a value/expression meant for use in a lazy function composition chain. + """Draw the socket body for a single flexible value/expression, for down-chain lazy evaluation. - Drawn when `self.active_kind == FlowKind.LazyValueFunc`. + This implements the most flexible variant of the `ExprSocket` UI, providing the user with full runtime-configuration of the exact `self.size`, `self.mathtype`, `self.physical_type`, and `self.symbols` of the expression. + + Notes: + Drawn when `self.active_kind == FlowKind.LazyValueFunc`. + + This is an ideal choice for ex. math nodes that need to accept arbitrary expressions as inputs, with an eye towards lazy evaluation of ex. symbolic terms. + + Uses `draw_value` to draw the base UI """ + # Physical Type Selector + ## -> Determines whether/which unit-dropdown will be shown. col.prop(self, self.blfields['physical_type'], text='') + + # Non-Symbolic: Size/Mathtype Selector + ## -> Symbols imply str expr input. + ## -> For arbitrary str exprs, size/mathtype are derived from the expr. + ## -> Otherwise, size/mathtype must be pre-specified for a nice UI. if not self.symbols: row = col.row(align=True) row.prop(self, self.blfields['size'], text='') row.prop(self, self.blfields['mathtype'], text='') + # Base UI + ## -> Draws the UI appropriate for the above choice of constraints. self.draw_value(col) + # Symbol UI + ## -> Draws the UI appropriate for the above choice of constraints. + ## -> TODO + #################### # - UI: InfoFlow #################### def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None: - if self.active_kind == ct.FlowKind.Array and self.show_info_columns: + if self.active_kind == ct.FlowKind.LazyValueFunc and self.show_info_columns: row = col.row() box = row.box() grid = box.grid_flow( @@ -899,7 +936,8 @@ class ExprSocketDef(base.SocketDef): physical_type: spux.PhysicalType = spux.PhysicalType.NonPhysical default_unit: spux.Unit | None = None - symbols: frozenset[spux.Symbol] = frozenset() + # symbols: list[sim_symbols.SimSymbol] = frozenset() + symbols: frozenset[spux.SympyExpr] = frozenset() # FlowKind: Value default_value: spux.SympyExpr = 0 diff --git a/src/blender_maxwell/utils/sim_symbols.py b/src/blender_maxwell/utils/sim_symbols.py new file mode 100644 index 0000000..a090c6f --- /dev/null +++ b/src/blender_maxwell/utils/sim_symbols.py @@ -0,0 +1,76 @@ +# blender_maxwell +# Copyright (C) 2024 blender_maxwell Project Contributors +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import dataclasses +import enum +import typing as typ + +import sympy as sp + +from . import extra_sympy_units as spux + + +class SimSymbolNames(enum.StrEnum): + LowerA = enum.auto() + LowerLambda = enum.auto() + + @staticmethod + def to_name(v: typ.Self) -> str: + """Convert the enum value to a human-friendly name. + + Notes: + Used to print names in `EnumProperty`s based on this enum. + + Returns: + A human-friendly name corresponding to the enum value. + """ + SSN = SimSymbolNames + return { + SSN.LowerA: 'a', + SSN.LowerLambda: 'λ', + }[v] + + @staticmethod + def to_icon(_: typ.Self) -> str: + """Convert the enum value to a Blender icon. + + Notes: + Used to print icons in `EnumProperty`s based on this enum. + + Returns: + A human-friendly name corresponding to the enum value. + """ + return '' + + +@dataclasses.dataclass(kw_only=True, frozen=True) +class SimSymbol: + name: SimSymbolNames = SimSymbolNames.LowerLambda + mathtype: spux.MathType = spux.MathType.Real + + ## TODO: + ## -> Physical Type: Track unit dimension information on the side. + ## -> Domain: Ability to constrain mathtype ex. (-pi,pi] + ## -> Shape: For using sp.MatrixSymbol w/predefined rows/cols. + + @property + def sp_symbol(self): + mathtype_kwarg = {} + match self.mathtype: + case spux.MathType.Real: + mathtype_kwarg = {} + + return sp.Symbol(self.name, **mathtype_kwarg)