feat: merged temporal shapes w/symbolic envelope

We now have a single node for all temporal shapes, which is extremely
usable. Note that we found a bug where input socket caching seems to
survive changes to loose inputs / socket set-driven alterations, which
prevents output from being able to switch with the socket set. We'll
make an issue for it whenever convenient.

Work also continued very briskly with the `SimSymbol` abstraction, which
is really, really working out.

Closes #67.
main
Sofus Albert Høgsbro Rose 2024-05-16 18:02:01 +02:00
parent e51ec8f43f
commit 2d26ea6ce8
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
14 changed files with 475 additions and 437 deletions

View File

@ -388,6 +388,12 @@ class LazyArrayRangeFlow:
self.stop.subs({sym: symbol_values[sym.name] for sym in self.symbols})
)
def realize_step_size(
self,
symbol_values: dict[spux.Symbol, typ.Any] = MappingProxyType({}),
) -> ArrayFlow | LazyValueFuncFlow:
return (self.realize_stop() - self.realize_start()) / self.steps
def realize(
self,
symbol_values: dict[spux.Symbol, typ.Any] = MappingProxyType({}),

View File

@ -55,10 +55,7 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
JSONFileExporter = enum.auto()
# Sources
## Sources / Temporal Shapes
PulseTemporalShape = enum.auto()
WaveTemporalShape = enum.auto()
ExprTemporalShape = enum.auto()
TemporalShape = enum.auto()
## Sources /
PointDipoleSource = enum.auto()
PlaneWaveSource = enum.auto()

View File

@ -374,7 +374,7 @@ class MapMathNode(base.MaxwellSimNode):
# - Properties
####################
operation: MapOperation = bl_cache.BLField(
prop_ui=True, enum_cb=lambda self, _: self.search_operations()
enum_cb=lambda self, _: self.search_operations()
)
@property

View File

@ -22,7 +22,7 @@ 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 bl_cache, image_ops, logger, sim_symbols
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct
@ -210,8 +210,8 @@ class VizNode(base.MaxwellSimNode):
input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyValueFunc,
symbols={_x := sp.Symbol('x', real=True)},
default_value=2 * _x,
default_symbols=[sim_symbols.x],
default_value=2 * sim_symbols.x.sp_symbol,
),
}
output_sockets: typ.ClassVar = {
@ -295,8 +295,8 @@ class VizNode(base.MaxwellSimNode):
####################
@events.on_value_changed(
socket_name='Expr',
input_sockets={'Expr'},
run_on_init=True,
input_sockets={'Expr'},
input_socket_kinds={'Expr': {ct.FlowKind.Info, ct.FlowKind.Params}},
input_sockets_optional={'Expr': True},
)

View File

@ -19,11 +19,11 @@ from . import (
gaussian_beam_source,
plane_wave_source,
point_dipole_source,
temporal_shapes,
temporal_shape,
)
BL_REGISTER = [
*temporal_shapes.BL_REGISTER,
*temporal_shape.BL_REGISTER,
*plane_wave_source.BL_REGISTER,
*point_dipole_source.BL_REGISTER,
# *uniform_current_source.BL_REGISTER,
@ -32,7 +32,7 @@ BL_REGISTER = [
# *tfsf_source.BL_REGISTER,
]
BL_NODES = {
**temporal_shapes.BL_NODES,
**temporal_shape.BL_NODES,
**plane_wave_source.BL_NODES,
**point_dipole_source.BL_NODES,
# **uniform_current_source.BL_NODES,

View File

@ -28,6 +28,8 @@ from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
log = logger.get(__name__)
class PlaneWaveSourceNode(base.MaxwellSimNode):
"""An infinite-extent angled source simulating an plane wave with linear polarization.
@ -73,7 +75,6 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
}
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
@ -132,18 +133,14 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
# Trigger
prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
managed_objs={'modifier'},
props={'preview_active'},
)
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()
managed_objs['modifier'].show_preview()
else:
mesh.hide_preview()
managed_objs['modifier'].hide_preview()
@events.on_value_changed(
# Trigger
@ -151,7 +148,7 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
prop_name={'injection_axis', 'injection_direction'},
run_on_init=True,
# Loaded
managed_objs={'mesh', 'modifier'},
managed_objs={'modifier'},
props={'injection_axis', 'injection_direction'},
input_sockets={'Temporal Shape', 'Center', 'Spherical', 'Pol ∡'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
@ -162,7 +159,6 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
def on_inputs_changed(self, managed_objs, props, input_sockets, unit_systems):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.SourcePlaneWave),
@ -175,6 +171,7 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
'Pol Angle': input_sockets['Pol ∡'],
},
},
location=input_sockets['Center'],
)

View File

@ -0,0 +1,197 @@
# 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/>.
"""Implements the `TemporalShapeNode`."""
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger, sim_symbols
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
log = logger.get(__name__)
_max_e_socket_def = sockets.ExprSocketDef(
mathtype=spux.MathType.Complex,
physical_type=spux.PhysicalType.EField,
default_value=1 + 0j,
)
_offset_socket_def = sockets.ExprSocketDef(default_value=5, abs_min=2.5)
class TemporalShapeNode(base.MaxwellSimNode):
"""Declare a source-time dependence for use in simulation source nodes."""
node_type = ct.NodeType.TemporalShape
bl_label = 'Temporal Shape'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'μ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=500,
),
'σ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=200,
),
}
input_socket_sets: typ.ClassVar = {
'Pulse': {
'max E': _max_e_socket_def,
'Offset Time': _offset_socket_def,
'Remove DC': sockets.BoolSocketDef(default_value=True),
},
'Constant': {
'max E': _max_e_socket_def,
'Offset Time': _offset_socket_def,
},
'Symbolic': {
't Range': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange,
physical_type=spux.PhysicalType.Time,
default_unit=spu.picosecond,
default_min=0,
default_max=10,
default_steps=100,
),
'Envelope': sockets.ExprSocketDef(
default_symbols=[sim_symbols.t_ps],
default_value=10 * sim_symbols.t_ps.sp_symbol,
),
},
}
output_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
}
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
if self.active_socket_set != 'Symbolic':
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.3, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Off t:')
## RHS: Parameter Units
col = split.column()
col.label(text='1 / 2π·σ(𝑓)')
####################
# - FlowKind: Value
####################
@events.computes_output_socket(
'Temporal Shape',
# Loaded
props={'active_socket_set'},
input_sockets={
'max E',
'μ Freq',
'σ Freq',
'Offset Time',
'Remove DC',
't Range',
'Envelope',
},
input_socket_kinds={
't Range': ct.FlowKind.LazyArrayRange,
'Envelope': ct.FlowKind.LazyValueFunc,
},
input_sockets_optional={
'max E': True,
'Offset Time': True,
'Remove DC': True,
't Range': True,
'Envelope': True,
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'max E': 'Tidy3DUnits',
'μ Freq': 'Tidy3DUnits',
'σ Freq': 'Tidy3DUnits',
't Range': 'Tidy3DUnits',
'Offset Time': 'Tidy3DUnits',
},
)
def compute_temporal_shape(
self, props, input_sockets, unit_systems
) -> td.GaussianPulse:
match props['active_socket_set']:
case 'Pulse':
return td.GaussianPulse(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
remove_dc_component=input_sockets['Remove DC'],
)
case 'Constant':
return td.ContinuousWave(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
)
case 'Symbolic':
lzrange = input_sockets['t Range']
envelope_ps = input_sockets['Envelope'].func_jax
return td.CustomSourceTime.from_values(
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
values=envelope_ps(
lzrange.rescale_to_unit(spu.ps).realize_array.values
),
dt=input_sockets['t Range'].realize_step_size(),
)
####################
# - Blender Registration
####################
BL_REGISTER = [
TemporalShapeNode,
]
BL_NODES = {ct.NodeType.TemporalShape: (ct.NodeCategory.MAXWELLSIM_SOURCES)}

View File

@ -1,29 +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/>.
# from . import expr_temporal_shape, pulse_temporal_shape, wave_temporal_shape
from . import pulse_temporal_shape, wave_temporal_shape
BL_REGISTER = [
*pulse_temporal_shape.BL_REGISTER,
*wave_temporal_shape.BL_REGISTER,
# *expr_temporal_shape.BL_REGISTER,
]
BL_NODES = {
**pulse_temporal_shape.BL_NODES,
**wave_temporal_shape.BL_NODES,
# **expr_temporal_shape.BL_NODES,
}

View File

@ -1,21 +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/>.
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,177 +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/>.
"""Implements the `PulseTemporalShapeNode`."""
import functools
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
class PulseTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.PulseTemporalShape
bl_label = 'Gaussian Pulse Temporal Shape'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'max E': sockets.ExprSocketDef(
mathtype=spux.MathType.Complex,
physical_type=spux.PhysicalType.EField,
default_value=1 + 0j,
),
'μ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=500,
),
'σ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=200,
),
'Offset Time': sockets.ExprSocketDef(default_value=5, abs_min=2.5),
'Remove DC': sockets.BoolSocketDef(
default_value=True,
),
}
output_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'E(t)': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
####################
# - UI
####################
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.3, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Off t:')
## RHS: Parameter Units
col = split.column()
col.label(text='1 / 2π·σ(𝑓)')
####################
# - FlowKind: Value
####################
@events.computes_output_socket(
'Temporal Shape',
input_sockets={
'max E',
'μ Freq',
'σ Freq',
'Offset Time',
'Remove DC',
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'max E': 'Tidy3DUnits',
'μ Freq': 'Tidy3DUnits',
'σ Freq': 'Tidy3DUnits',
},
)
def compute_temporal_shape(self, input_sockets, unit_systems) -> td.GaussianPulse:
return td.GaussianPulse(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
remove_dc_component=input_sockets['Remove DC'],
)
####################
# - FlowKind: LazyValueFunc / Info / Params
####################
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.LazyValueFunc,
output_sockets={'Temporal Shape'},
)
def compute_time_to_efield_lazy(self, output_sockets) -> td.GaussianPulse:
temporal_shape = output_sockets['Temporal Shape']
jax_amp_time = functools.partial(ct.manual_amp_time, temporal_shape)
## TODO: Don't just partial() it up, do it property in the ParamsFlow!
## -> Right now it's recompiled every time.
return ct.LazyValueFuncFlow(
func=jax_amp_time,
func_args=[spux.PhysicalType.Time],
supports_jax=True,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Info,
)
def compute_time_to_efield_info(self) -> td.GaussianPulse:
return ct.InfoFlow(
dim_names=['t'],
dim_idx={
't': ct.LazyArrayRangeFlow(
start=sp.S(0), stop=sp.oo, steps=0, unit=spu.second
)
},
output_name='E',
output_shape=None,
output_mathtype=spux.MathType.Complex,
output_unit=spu.volt / spu.um,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Params,
)
def compute_time_to_efield_params(self) -> td.GaussianPulse:
sym_time = sp.Symbol('t', real=True, nonnegative=True)
return ct.ParamsFlow(func_args=[sym_time], symbols={sym_time})
####################
# - Blender Registration
####################
BL_REGISTER = [
PulseTemporalShapeNode,
]
BL_NODES = {
ct.NodeType.PulseTemporalShape: (ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES)
}

View File

@ -1,169 +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/>.
"""Implements the `WaveTemporalShapeNode`."""
import functools
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import extra_sympy_units as spux
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
class WaveTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.WaveTemporalShape
bl_label = 'Continuous Wave Temporal Shape'
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'max E': sockets.ExprSocketDef(
mathtype=spux.MathType.Complex,
physical_type=spux.PhysicalType.EField,
default_value=1 + 0j,
),
'μ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=500,
),
'σ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=200,
),
'Offset Time': sockets.ExprSocketDef(default_value=5, abs_min=2.5),
}
output_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'E(t)': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
####################
# - UI
####################
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
# Split
split = box.split(factor=0.3, align=False)
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Off t:')
## RHS: Parameter Units
col = split.column()
col.label(text='1 / 2π·σ(𝑓)')
####################
# - FlowKind: Value
####################
@events.computes_output_socket(
'Temporal Shape',
input_sockets={
'max E',
'μ Freq',
'σ Freq',
'Offset Time',
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'max E': 'Tidy3DUnits',
'μ Freq': 'Tidy3DUnits',
'σ Freq': 'Tidy3DUnits',
},
)
def compute_temporal_shape(self, input_sockets, unit_systems) -> td.GaussianPulse:
return td.ContinuousWave(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
)
####################
# - FlowKind: LazyValueFunc / Info / Params
####################
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.LazyValueFunc,
output_sockets={'Temporal Shape'},
)
def compute_time_to_efield_lazy(self, output_sockets) -> td.GaussianPulse:
temporal_shape = output_sockets['Temporal Shape']
jax_amp_time = functools.partial(ct.manual_amp_time, temporal_shape)
return ct.LazyValueFuncFlow(
func=jax_amp_time,
func_args=[spux.PhysicalType.Time],
supports_jax=True,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Info,
)
def compute_time_to_efield_info(self) -> td.GaussianPulse:
return ct.InfoFlow(
dim_names=['t'],
dim_idx={
't': ct.LazyArrayRangeFlow(
start=sp.S(0), stop=sp.oo, steps=0, unit=spu.second
)
},
output_name='E',
output_shape=None,
output_mathtype=spux.MathType.Complex,
output_unit=spu.volt / spu.um,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Params,
)
def compute_time_to_efield_params(self) -> td.GaussianPulse:
sym_time = sp.Symbol('t', real=True, nonnegative=True)
return ct.ParamsFlow(func_args=[sym_time], symbols={sym_time})
####################
# - Blender Registration
####################
BL_REGISTER = [
WaveTemporalShapeNode,
]
BL_NODES = {
ct.NodeType.WaveTemporalShape: (ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES)
}

View File

@ -23,7 +23,7 @@ import bpy
import pydantic as pyd
import sympy as sp
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import bl_cache, logger, sim_symbols
from blender_maxwell.utils import extra_sympy_units as spux
from .. import contracts as ct
@ -936,8 +936,11 @@ class ExprSocketDef(base.SocketDef):
physical_type: spux.PhysicalType = spux.PhysicalType.NonPhysical
default_unit: spux.Unit | None = None
# symbols: list[sim_symbols.SimSymbol] = frozenset()
symbols: frozenset[spux.SympyExpr] = frozenset()
default_symbols: list[sim_symbols.SimSymbol] = pyd.Field(default_factory=list)
@property
def symbols(self) -> set[sp.Symbol]:
return {sim_symbol.sp_symbol for sim_symbol in self.default_symbols}
# FlowKind: Value
default_value: spux.SympyExpr = 0

View File

@ -173,6 +173,17 @@ class MathType(enum.StrEnum):
MT.Complex: complex,
}[self]
@property
def symbolic_set(self) -> type:
MT = MathType
return {
MT.Bool: sp.Set([sp.S(False), sp.S(True)]),
MT.Integer: sp.Integers,
MT.Rational: sp.Rationals,
MT.Real: sp.Reals,
MT.Complex: sp.Complexes,
}[self]
@staticmethod
def to_str(value: typ.Self) -> type:
return {

View File

@ -16,6 +16,7 @@
import dataclasses
import enum
import sys
import typing as typ
import sympy as sp
@ -23,9 +24,19 @@ import sympy as sp
from . import extra_sympy_units as spux
class SimSymbolNames(enum.StrEnum):
####################
# - Simulation Symbols
####################
class SimSymbolName(enum.StrEnum):
LowerA = enum.auto()
LowerLambda = enum.auto()
LowerT = enum.auto()
LowerX = enum.auto()
LowerY = enum.auto()
LowerZ = enum.auto()
# Physics
Wavelength = enum.auto()
Frequency = enum.auto()
@staticmethod
def to_name(v: typ.Self) -> str:
@ -37,11 +48,28 @@ class SimSymbolNames(enum.StrEnum):
Returns:
A human-friendly name corresponding to the enum value.
"""
SSN = SimSymbolNames
return SimSymbolName(v).name
@property
def name(self) -> str:
SSN = SimSymbolName
return {
SSN.LowerA: 'a',
SSN.LowerLambda: 'λ',
}[v]
SSN.LowerT: 't',
SSN.LowerX: 'x',
SSN.LowerY: 'y',
SSN.LowerZ: 'z',
SSN.Wavelength: 'wl',
SSN.Frequency: 'freq',
}[self]
@property
def name_pretty(self) -> str:
SSN = SimSymbolName
return {
SSN.Wavelength: 'λ',
SSN.Frequency: '𝑓',
}.get(self, self.name)
@staticmethod
def to_icon(_: typ.Self) -> str:
@ -58,19 +86,214 @@ class SimSymbolNames(enum.StrEnum):
@dataclasses.dataclass(kw_only=True, frozen=True)
class SimSymbol:
name: SimSymbolNames = SimSymbolNames.LowerLambda
"""A declarative representation of a symbolic variable.
`sympy`'s symbols aren't quite flexible enough for our needs: The symbols that we're transporting often need exact domain information, an associated unit dimension, and a great deal of determinism in checks thereof.
This dataclass is UI-friendly, as it only uses field type annotations/defaults supported by `bl_cache.BLProp`.
It's easy to persist, easy to transport, and has many helpful properties which greatly simplify working with symbols.
"""
sim_node_name: SimSymbolName = SimSymbolName.LowerX
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.
physical_type: spux.PhysicalType = spux.PhysicalType.NonPhysical
## TODO: Shape/size support? Incl. MatrixSymbol.
# Domain
interval_finite: tuple[float, float] = (0, 1)
interval_inf: tuple[bool, bool] = (True, True)
interval_closed: tuple[bool, bool] = (False, False)
####################
# - Properties
####################
@property
def domain(self) -> sp.Interval | sp.Set:
"""Return the domain of valid values for the symbol.
For integer/rational/real symbols, the domain is an interval defined using the `interval_*` properties.
This interval **must** have the property`start <= stop`.
Otherwise, the domain is the symbolic set corresponding to `self.mathtype`.
"""
if self.mathtype in [
spux.MathType.Integer,
spux.MathType.Rational,
spux.MathType.Real,
]:
return sp.Interval(
start=self.interval_finite[0] if not self.interval_inf[0] else -sp.oo,
end=self.interval_finite[1] if not self.interval_inf[1] else sp.oo,
left_open=(
True if self.interval_inf[0] else not self.interval_closed[0]
),
right_open=(
True if self.interval_inf[1] else not self.interval_closed[1]
),
)
return self.mathtype.symbolic_set
####################
# - Properties
####################
@property
def sp_symbol(self) -> sp.Symbol:
"""Return a symbolic variable corresponding to this `SimSymbol`.
As much as possible, appropriate `assumptions` are set in the constructor of `sp.Symbol`, insofar as they can be determined.
However, the assumptions system alone is rather limited, and implementations should therefore also strongly consider transporting `SimSymbols` directly, instead of `sp.Symbol`.
This allows making use of other properties like `self.domain`, when appropriate.
"""
# MathType Domain Constraint
## -> We must feed the assumptions system.
mathtype_kwargs = {}
match self.mathtype:
case spux.MathType.Integer:
mathtype_kwargs |= {'integer': True}
case spux.MathType.Rational:
mathtype_kwargs |= {'rational': True}
case spux.MathType.Real:
mathtype_kwargs |= {'real': True}
case spux.MathType.Complex:
mathtype_kwargs |= {'complex': True}
# Interval Constraints
if isinstance(self.domain, sp.Interval):
# Assumption: Non-Zero
if (
(
self.domain.left == 0
and self.domain.left_open
or self.domain.right == 0
and self.domain.right_open
)
or self.domain.left > 0
or self.domain.right < 0
):
mathtype_kwargs |= {'nonzero': True}
# Assumption: Positive/Negative
if self.domain.left >= 0:
mathtype_kwargs |= {'positive': True}
elif self.domain.right <= 0:
mathtype_kwargs |= {'negative': True}
# Construct the Symbol
return sp.Symbol(self.sim_node_name.name, **mathtype_kwargs)
####################
# - Common Sim Symbols
####################
class CommonSimSymbol(enum.StrEnum):
"""A set of pre-defined symbols that might commonly be used in the context of physical simulation.
Each entry maps directly to a particular `SimSymbol`.
The enum is compatible with `BLField`, making it easy to declare a UI-driven dropdown of symbols that behave as expected.
Attributes:
Wavelength: A symbol representing a real-valued wavelength.
Implicitly, this symbol often represents "vacuum wavelength" in particular.
Wavelength: A symbol representing a real-valued frequency.
Generally, this is the non-angular frequency.
"""
X = enum.auto()
TimePS = enum.auto()
WavelengthNM = enum.auto()
FrequencyTHZ = 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.
"""
return CommonSimSymbol(v).sim_symbol_name.name
@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 ''
####################
# - Properties
####################
@property
def sim_symbol_name(self) -> str:
SSN = SimSymbolName
CSS = CommonSimSymbol
return {
CSS.X: SSN.LowerX,
CSS.TimePS: SSN.LowerT,
CSS.WavelengthNM: SSN.Wavelength,
CSS.FrequencyTHZ: SSN.Frequency,
}[self]
@property
def sp_symbol(self):
mathtype_kwarg = {}
match self.mathtype:
case spux.MathType.Real:
mathtype_kwarg = {}
def sim_symbol(self) -> SimSymbol:
"""Retrieve the `SimSymbol` associated with the `CommonSimSymbol`."""
CSS = CommonSimSymbol
return {
CSS.X: SimSymbol(
sim_node_name=self.sim_symbol_name,
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.NonPhysical,
## TODO: Unit of Picosecond
interval_finite=(sys.float_info.min, sys.float_info.max),
interval_inf=(True, True),
interval_closed=(False, False),
),
CSS.TimePS: SimSymbol(
sim_node_name=self.sim_symbol_name,
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Time,
## TODO: Unit of Picosecond
interval_finite=(0, sys.float_info.max),
interval_inf=(False, True),
interval_closed=(True, False),
),
CSS.WavelengthNM: SimSymbol(
sim_node_name=self.sim_symbol_name,
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
## TODO: Unit of Picosecond
interval_finite=(0, sys.float_info.max),
interval_inf=(False, True),
interval_closed=(False, False),
),
CSS.FrequencyTHZ: SimSymbol(
sim_node_name=self.sim_symbol_name,
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Freq,
## TODO: Unit of THz
interval_finite=(0, sys.float_info.max),
interval_inf=(False, True),
interval_closed=(False, False),
),
}[self]
return sp.Symbol(self.name, **mathtype_kwarg)
####################
# - Selected Direct Access
####################
x = CommonSimSymbol.X.sim_symbol
t_ps = CommonSimSymbol.TimePS.sim_symbol
wl_nm = CommonSimSymbol.WavelengthNM.sim_symbol
freq_thz = CommonSimSymbol.FrequencyTHZ.sim_symbol