feat: expr constant w/viz fixes

Driven solely by the Expr socket, we've completely replaced the
dedicated NumberConstant and PhysicalConstant nodes. We also
demonstrated symbolic variable operations w/visualization and
end-node realization, validating that this approach to Expr sockets is
key.

We prepared for a new `BLPropType`, namely dynamic lists of dataclasses,
which will represent dynamic user-adjustable lists. This is the only
proper UI design for declaring symbols directly in an Expr node.
For now, we'll do without symbols, but will be a core feature for design
space exploration (aka. batch-run a bunch of sims), and for inverse
design.

Else, a few fixes: Naming of `ManagedBLImage` was updated, the expr
socket info display was fixed to display even when only output
information is available,

A few TODOs remain in the Expr Constant, but they are less important
before dynamic symbol declarations are in place.
main
Sofus Albert Høgsbro Rose 2024-05-16 13:01:37 +02:00
parent 92be84ec8a
commit 060f54bd94
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
8 changed files with 201 additions and 268 deletions

View File

@ -38,8 +38,6 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
Scene = enum.auto() Scene = enum.auto()
## Inputs / Constants ## Inputs / Constants
ExprConstant = enum.auto() ExprConstant = enum.auto()
NumberConstant = enum.auto()
PhysicalConstant = enum.auto()
ScientificConstant = enum.auto() ScientificConstant = enum.auto()
UnitSystemConstant = enum.auto() UnitSystemConstant = enum.auto()
BlenderConstant = enum.auto() BlenderConstant = enum.auto()

View File

@ -21,9 +21,11 @@ import enum
import typing as typ import typing as typ
import jax.numpy as jnp import jax.numpy as jnp
import sympy as sp
import tidy3d as td import tidy3d as td
from blender_maxwell.services import tdcloud from blender_maxwell.services import tdcloud
from blender_maxwell.utils import extra_sympy_units as spux
#################### ####################

View File

@ -47,8 +47,13 @@ class ManagedBLImage(base.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLImage managed_obj_type = ct.ManagedObjType.ManagedBLImage
_bl_image_name: str _bl_image_name: str
def __init__(self, name: str): def __init__(self, name: str, prev_name: str | None = None):
self._bl_image_name = name if prev_name is not None:
self._bl_image_name = prev_name
else:
self._bl_image_name = name
self.name = name
@property @property
def name(self): def name(self):
@ -57,26 +62,29 @@ class ManagedBLImage(base.ManagedObj):
@name.setter @name.setter
def name(self, value: str): def name(self, value: str):
log.info( log.info(
'Setting ManagedBLImage from "%s" to "%s"', 'Changing ManagedBLImage from "%s" to "%s"',
self.name, self.name,
value, value,
) )
current_bl_image = bpy.data.images.get(self._bl_image_name) existing_bl_image = bpy.data.images.get(self.name)
wanted_bl_image = bpy.data.images.get(value)
# Yoink Image Name # No Existing Image: Set Value to Name
if current_bl_image is None and wanted_bl_image is None: if existing_bl_image is None:
self._bl_image_name = value self._bl_image_name = value
# Alter Image Name # Existing Image: Rename to New Name
elif current_bl_image is not None and wanted_bl_image is None: else:
existing_bl_image.name = value
self._bl_image_name = value self._bl_image_name = value
current_bl_image.name = value
# Overlapping Image Name # Check: Blender Rename -> Synchronization Error
elif wanted_bl_image is not None: ## -> We can't do much else than report to the user & free().
msg = f'ManagedBLImage "{self._bl_image_name}" could not change its name to "{value}", since it already exists.' if existing_bl_image.name != self._bl_image_name:
raise ValueError(msg) 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): def free(self):
bl_image = bpy.data.images.get(self.name) bl_image = bpy.data.images.get(self.name)

View File

@ -16,6 +16,8 @@
import typing as typ import typing as typ
import sympy as sp
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base, events
@ -26,27 +28,69 @@ class ExprConstantNode(base.MaxwellSimNode):
bl_label = 'Expr Constant' bl_label = 'Expr Constant'
input_sockets: typ.ClassVar = { input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyValueFunc,
),
} }
output_sockets: typ.ClassVar = { output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(), 'Expr': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyValueFunc,
show_info_columns=True,
),
} }
## TODO: Symbols (defined w/props?) ## TODO: Allow immediately realizing any symbol, or just passing it along.
## - Currently expr constant isn't excessively useful, since there are no variables. ## TODO: Alter output physical_type when the input PhysicalType changes.
## - 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).
#################### ####################
# - Callbacks # - FlowKinds
#################### ####################
@events.computes_output_socket( @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: def compute_value(self, input_sockets: dict) -> typ.Any:
return input_sockets['Expr'] 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 # - Blender Registration

View File

@ -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 <http://www.gnu.org/licenses/>.
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)}

View File

@ -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 <http://www.gnu.org/licenses/>.
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
)
}

View File

@ -116,13 +116,22 @@ class ExprBLSocket(base.MaxwellSimSocket):
size: spux.NumberSize1D = bl_cache.BLField(spux.NumberSize1D.Scalar) size: spux.NumberSize1D = bl_cache.BLField(spux.NumberSize1D.Scalar)
mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real) mathtype: spux.MathType = bl_cache.BLField(spux.MathType.Real)
physical_type: spux.PhysicalType = bl_cache.BLField(spux.PhysicalType.NonPhysical) 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()) 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'}) @bl_cache.cached_bl_property(depends_on={'symbols'})
def sorted_symbols(self) -> list[sp.Symbol]: 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) return sorted(self.symbols, key=lambda sym: sym.name)
# Unit
active_unit: enum.StrEnum = bl_cache.BLField( active_unit: enum.StrEnum = bl_cache.BLField(
enum_cb=lambda self, _: self.search_valid_units(), enum_cb=lambda self, _: self.search_valid_units(),
cb_depends_on={'physical_type'}, 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`. 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) 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) split = row.split(factor=0.85, align=True)
_row = split.row(align=False) _row = split.row(align=False)
else: else:
_row = row _row = row
_row.label(text=text) _row.label(text=text)
if has_dims: if has_info:
if self.show_info_columns: if self.show_info_columns:
_row.prop(self, self.blfields['info_columns']) _row.prop(self, self.blfields['info_columns'])
@ -735,9 +744,17 @@ class ExprBLSocket(base.MaxwellSimSocket):
# - UI: Active FlowKind # - UI: Active FlowKind
#################### ####################
def draw_value(self, col: bpy.types.UILayout) -> None: 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: if self.symbols:
col.prop(self, self.blfields['raw_value_spstr'], text='') col.prop(self, self.blfields['raw_value_spstr'], text='')
@ -819,23 +836,43 @@ class ExprBLSocket(base.MaxwellSimSocket):
col.prop(self, self.blfields['steps'], text='') col.prop(self, self.blfields['steps'], text='')
def draw_lazy_value_func(self, col: bpy.types.UILayout) -> None: 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='') 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: if not self.symbols:
row = col.row(align=True) row = col.row(align=True)
row.prop(self, self.blfields['size'], text='') row.prop(self, self.blfields['size'], text='')
row.prop(self, self.blfields['mathtype'], text='') row.prop(self, self.blfields['mathtype'], text='')
# Base UI
## -> Draws the UI appropriate for the above choice of constraints.
self.draw_value(col) self.draw_value(col)
# Symbol UI
## -> Draws the UI appropriate for the above choice of constraints.
## -> TODO
#################### ####################
# - UI: InfoFlow # - UI: InfoFlow
#################### ####################
def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None: def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None:
if self.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() row = col.row()
box = row.box() box = row.box()
grid = box.grid_flow( grid = box.grid_flow(
@ -899,7 +936,8 @@ class ExprSocketDef(base.SocketDef):
physical_type: spux.PhysicalType = spux.PhysicalType.NonPhysical physical_type: spux.PhysicalType = spux.PhysicalType.NonPhysical
default_unit: spux.Unit | None = None default_unit: spux.Unit | None = None
symbols: frozenset[spux.Symbol] = frozenset() # symbols: list[sim_symbols.SimSymbol] = frozenset()
symbols: frozenset[spux.SympyExpr] = frozenset()
# FlowKind: Value # FlowKind: Value
default_value: spux.SympyExpr = 0 default_value: spux.SympyExpr = 0

View File

@ -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 <http://www.gnu.org/licenses/>.
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)