fix: Inching closer.

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

26
TODO.md
View File

@ -1,5 +1,5 @@
# Working TODO
- [ ] 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

View File

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

View File

@ -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_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'):

View File

@ -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):
if kind == cls.Value:
return spux.sympy_to_python(
spux.scale_to_unit(
def scale_to_unit_system(
cls,
kind: typ.Self,
value,
unit_system[socket_type],
)
unit_system: spux.UnitSystem,
):
if kind == cls.Value:
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
####################

View File

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

View File

@ -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:
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)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
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:
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)
else:
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
def bl_select(self) -> None:
"""Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport."""

View File

@ -206,6 +206,8 @@ class MaxwellSimTree(bpy.types.NodeTree):
"""Unlock all nodes in the node tree, making them editable."""
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 = {}

View File

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

View File

@ -401,7 +401,7 @@ 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):
# if self.operation not in MapOperation.by_element_shape(self.expr_output_shape):
self.operation = bl_cache.Signal.ResetEnumItems
@events.on_value_changed(

View File

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

View File

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

View File

@ -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:
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),
bl_socket.socket_type,
unit_system,
)
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)
####################

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
from . import file_exporters, viewer, web_exporters
#from . import file_exporters, viewer, web_exporters
from . import viewer
BL_REGISTER = [
*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,
}

View File

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

View File

@ -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 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.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):
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,11 +830,12 @@ 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 * self.default_unit
else:
bl_socket.value = self.default_value
# FlowKind: LazyArrayRange

View File

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

View File

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

View File

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

View File

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

View File

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