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()
## Inputs / Constants
ExprConstant = enum.auto()
NumberConstant = enum.auto()
PhysicalConstant = enum.auto()
ScientificConstant = enum.auto()
UnitSystemConstant = enum.auto()
BlenderConstant = enum.auto()

View File

@ -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
####################

View File

@ -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)

View File

@ -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

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)
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

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)