fix: Broken GN unit evaluation

main
Sofus Albert Høgsbro Rose 2024-04-03 10:13:10 +02:00
parent a282d1e7ef
commit c2db40ca6d
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
25 changed files with 536 additions and 547 deletions

Binary file not shown.

View File

@ -59,7 +59,6 @@ def import_geonodes(
- Retrieve the node group and return it. - Retrieve the node group and return it.
""" """
if geonodes in bpy.data.node_groups and not force_import: if geonodes in bpy.data.node_groups and not force_import:
log.info('Found Existing GeoNodes Tree (name=%s)', geonodes)
return bpy.data.node_groups[geonodes] return bpy.data.node_groups[geonodes]
filename = geonodes filename = geonodes

View File

@ -101,13 +101,19 @@ def _socket_type_from_bl_socket(
# Parse Description for Socket Type # Parse Description for Socket Type
## The "2D" token is special; don't include it if it's there. ## The "2D" token is special; don't include it if it's there.
tokens = _tokens if (_tokens := description.split(' '))[0] != '2D' else _tokens[1:] descr_params = description.split(ct.BL_SOCKET_DESCR_ANNOT_STRING)[0]
directive = (
_tokens[0] if (_tokens := descr_params.split(' '))[0] != '2D' else _tokens[1]
)
if directive == 'Preview':
return direct_socket_type ## TODO: Preview element handling
if ( if (
socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get( socket_type := ct.BL_SOCKET_DESCR_TYPE_MAP.get(
(tokens[0], bl_socket_type, size) (directive, bl_socket_type, size)
) )
) is None: ) is None:
msg = f'Socket description "{(tokens[0], bl_socket_type, size)}" doesn\'t map to a socket type + unit' msg = f'Socket description "{(directive, bl_socket_type, size)}" doesn\'t map to a socket type + unit'
raise ValueError(msg) raise ValueError(msg)
return socket_type return socket_type
@ -129,19 +135,19 @@ def socket_def_from_bl_socket(
) -> ct.schemas.SocketDef: ) -> ct.schemas.SocketDef:
"""Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it.""" """Computes an appropriate (no-arg) SocketDef from the given `bl_interface_socket`, by parsing it."""
return _socket_def_from_bl_socket( return _socket_def_from_bl_socket(
bl_interface_socket.description, bl_interface_socket.socket_type bl_interface_socket.description, bl_interface_socket.bl_socket_idname
) )
#################### ####################
# - Extract Default Interface Socket Value # - Extract Default Interface Socket Value
#################### ####################
@functools.lru_cache(maxsize=4096)
def _read_bl_socket_default_value( def _read_bl_socket_default_value(
description: str, description: str,
bl_socket_type: BLSocketType, bl_socket_type: BLSocketType,
bl_socket_value: BLSocketValue, bl_socket_value: BLSocketValue,
unit_system: dict | None = None, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
# Parse the BL Socket Type and Value # Parse the BL Socket Type and Value
## The 'lambda' delays construction until size is determined. ## The 'lambda' delays construction until size is determined.
@ -157,21 +163,20 @@ def _read_bl_socket_default_value(
## Use the matching socket type to lookup the unit in the unit system. ## Use the matching socket type to lookup the unit in the unit system.
if unit_system is not None: if unit_system is not None:
if (unit := unit_system.get(socket_type)) is None: if (unit := unit_system.get(socket_type)) is None:
if allow_unit_not_in_unit_system:
return parsed_socket_value
msg = f'Unit system does not provide a unit for {socket_type}' msg = f'Unit system does not provide a unit for {socket_type}'
raise RuntimeError(msg) raise RuntimeError(msg)
if unit not in (valid_units := ct.SOCKET_UNITS[socket_type]['values'].values()):
msg = f'Unit system provided a unit "{unit}" that is invalid for socket type "{socket_type}" (valid units: {valid_units})'
raise RuntimeError(msg)
return parsed_socket_value * unit return parsed_socket_value * unit
return parsed_socket_value return parsed_socket_value
def read_bl_socket_default_value( def read_bl_socket_default_value(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket, bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
unit_system: dict | None = None, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
"""Reads the `default_value` of a Blender socket, guaranteeing a well-formed value consistent with the passed unit system. """Reads the `default_value` of a Blender socket, guaranteeing a well-formed value consistent with the passed unit system.
@ -185,33 +190,41 @@ def read_bl_socket_default_value(
""" """
return _read_bl_socket_default_value( return _read_bl_socket_default_value(
bl_interface_socket.description, bl_interface_socket.description,
bl_interface_socket.socket_type, bl_interface_socket.bl_socket_idname,
bl_interface_socket.default_value, bl_interface_socket.default_value,
unit_system, unit_system=unit_system,
allow_unit_not_in_unit_system=allow_unit_not_in_unit_system,
) )
@functools.lru_cache(maxsize=4096)
def _writable_bl_socket_value( def _writable_bl_socket_value(
description: str, description: str,
bl_socket_type: BLSocketType, bl_socket_type: BLSocketType,
value: typ.Any, value: typ.Any,
unit_system: dict | None = None, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
socket_type = _socket_type_from_bl_socket(description, bl_socket_type) socket_type = _socket_type_from_bl_socket(description, bl_socket_type)
# Retrieve Unit-System Unit # Retrieve Unit-System Unit
if unit_system is not None: if unit_system is not None:
if (unit := unit_system.get(socket_type)) is None: if (unit := unit_system.get(socket_type)) is None:
msg = f'Unit system does not provide a unit for {socket_type}' if allow_unit_not_in_unit_system:
raise RuntimeError(msg) _bl_socket_value = value
else:
_bl_socket_value = spux.scale_to_unit(value, unit) msg = f'Unit system does not provide a unit for {socket_type}'
raise RuntimeError(msg)
else:
_bl_socket_value = spux.scale_to_unit(value, unit)
else: else:
_bl_socket_value = value _bl_socket_value = value
# Compute Blender Socket Value # Compute Blender Socket Value
bl_socket_value = spux.sympy_to_python(_bl_socket_value) if isinstance(_bl_socket_value, sp.Basic):
bl_socket_value = spux.sympy_to_python(_bl_socket_value)
else:
bl_socket_value = _bl_socket_value
if _size_from_bl_socket(description, bl_socket_type) == 2: # noqa: PLR2004 if _size_from_bl_socket(description, bl_socket_type) == 2: # noqa: PLR2004
bl_socket_value = bl_socket_value[:2] bl_socket_value = bl_socket_value[:2]
return bl_socket_value return bl_socket_value
@ -221,6 +234,7 @@ def writable_bl_socket_value(
bl_interface_socket: bpy.types.NodeTreeInterfaceSocket, bl_interface_socket: bpy.types.NodeTreeInterfaceSocket,
value: typ.Any, value: typ.Any,
unit_system: dict | None = None, unit_system: dict | None = None,
allow_unit_not_in_unit_system: bool = False,
) -> typ.Any: ) -> typ.Any:
"""Processes a value to be ready-to-write to a Blender socket. """Processes a value to be ready-to-write to a Blender socket.
@ -234,7 +248,8 @@ def writable_bl_socket_value(
""" """
return _writable_bl_socket_value( return _writable_bl_socket_value(
bl_interface_socket.description, bl_interface_socket.description,
bl_interface_socket.bl_socket_type, bl_interface_socket.bl_socket_idname,
value, value,
unit_system, unit_system=unit_system,
allow_unit_not_in_unit_system=allow_unit_not_in_unit_system,
) )

View File

@ -37,7 +37,8 @@ UNITS_BLENDER: UnitSystem = {
} ## TODO: Load (dynamically?) from addon preferences } ## TODO: Load (dynamically?) from addon preferences
UNITS_TIDY3D: UnitSystem = { UNITS_TIDY3D: UnitSystem = {
ST.PhysicalTime: spu.picosecond, ## https://docs.flexcompute.com/projects/tidy3d/en/latest/faq/docs/faq/What-are-the-units-used-in-the-simulation.html
ST.PhysicalTime: spu.second,
ST.PhysicalAngle: spu.radian, ST.PhysicalAngle: spu.radian,
ST.PhysicalLength: spu.micrometer, ST.PhysicalLength: spu.micrometer,
ST.PhysicalArea: spu.micrometer**2, ST.PhysicalArea: spu.micrometer**2,
@ -52,6 +53,6 @@ UNITS_TIDY3D: UnitSystem = {
ST.PhysicalForceScalar: spux.micronewton, ST.PhysicalForceScalar: spux.micronewton,
ST.PhysicalAccel3D: spu.um / spu.second**2, ST.PhysicalAccel3D: spu.um / spu.second**2,
ST.PhysicalForce3D: spux.micronewton, ST.PhysicalForce3D: spux.micronewton,
ST.PhysicalFreq: spux.terahertz, ST.PhysicalFreq: spu.hertz,
ST.PhysicalPol: spu.radian, ST.PhysicalPol: spu.radian,
} }

View File

@ -1,4 +1,4 @@
from .managed_bl_empty import ManagedBLEmpty #from .managed_bl_empty import ManagedBLEmpty
from .managed_bl_image import ManagedBLImage from .managed_bl_image import ManagedBLImage
# from .managed_bl_collection import ManagedBLCollection # from .managed_bl_collection import ManagedBLCollection
@ -9,7 +9,7 @@ from .managed_bl_mesh import ManagedBLMesh
from .managed_bl_modifier import ManagedBLModifier from .managed_bl_modifier import ManagedBLModifier
__all__ = [ __all__ = [
'ManagedBLEmpty', #'ManagedBLEmpty',
'ManagedBLImage', 'ManagedBLImage',
#'ManagedBLCollection', #'ManagedBLCollection',
#'ManagedBLObject', #'ManagedBLObject',

View File

@ -1,3 +1,5 @@
import functools
import bpy import bpy
from ....utils import logger from ....utils import logger
@ -11,6 +13,7 @@ PREVIEW_COLLECTION_NAME = 'BLMaxwell Visible'
#################### ####################
# - Global Collection Handling # - Global Collection Handling
#################### ####################
@functools.cache
def collection(collection_name: str, view_layer_exclude: bool) -> bpy.types.Collection: def collection(collection_name: str, view_layer_exclude: bool) -> bpy.types.Collection:
# Init the "Managed Collection" # Init the "Managed Collection"
# Ensure Collection exists (and is in the Scene collection) # Ensure Collection exists (and is in the Scene collection)
@ -32,8 +35,8 @@ def collection(collection_name: str, view_layer_exclude: bool) -> bpy.types.Coll
def managed_collection() -> bpy.types.Collection: def managed_collection() -> bpy.types.Collection:
return collection(MANAGED_COLLECTION_NAME, view_layer_exclude=False) return collection(MANAGED_COLLECTION_NAME, view_layer_exclude=True)
def preview_collection() -> bpy.types.Collection: def preview_collection() -> bpy.types.Collection:
return collection(PREVIEW_COLLECTION_NAME, view_layer_exclude=True) return collection(PREVIEW_COLLECTION_NAME, view_layer_exclude=False)

View File

@ -31,7 +31,7 @@ class ManagedBLMesh(ct.schemas.ManagedObj):
'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value 'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value
) )
if not bpy.data.objects.get(value): if (bl_object := bpy.data.objects.get(value)) is None:
log.info( log.info(
'Desired BLMesh Name "%s" Not Taken', 'Desired BLMesh Name "%s" Not Taken',
value, value,
@ -42,7 +42,7 @@ class ManagedBLMesh(ct.schemas.ManagedObj):
'Set New BLMesh Name to "%s"', 'Set New BLMesh Name to "%s"',
value, value,
) )
elif bl_object := bpy.data.objects.get(self._bl_object_name): elif (bl_object := bpy.data.objects.get(self._bl_object_name)) is not None:
log.info( log.info(
'Changed BLMesh Name to "%s"', 'Changed BLMesh Name to "%s"',
value, value,
@ -97,28 +97,26 @@ class ManagedBLMesh(ct.schemas.ManagedObj):
If it's already included, do nothing. If it's already included, do nothing.
""" """
if ( if (bl_object := bpy.data.objects.get(self.name)) is not None:
bl_object := bpy.data.objects.get(self.name) if bl_object.name not in preview_collection().objects:
) is not None and bl_object.name not in preview_collection().objects: log.info('Moving "%s" to Preview Collection', bl_object.name)
log.info('Moving "%s" to Preview Collection', bl_object.name) preview_collection().objects.link(bl_object)
preview_collection().objects.link(bl_object) else:
msg = 'Managed BLMesh does not exist'
msg = 'Managed BLMesh does not exist' raise ValueError(msg)
raise ValueError(msg)
def hide_preview(self) -> None: def hide_preview(self) -> None:
"""Removes the managed Blender object from the preview collection. """Removes the managed Blender object from the preview collection.
If it's already removed, do nothing. If it's already removed, do nothing.
""" """
if ( if (bl_object := bpy.data.objects.get(self.name)) is not None:
bl_object := bpy.data.objects.get(self.name) if bl_object.name in preview_collection().objects:
) is not None and bl_object.name in preview_collection().objects: log.info('Removing "%s" from Preview Collection', bl_object.name)
log.info('Removing "%s" from Preview Collection', bl_object.name) preview_collection().objects.unlink(bl_object)
preview_collection.objects.unlink(bl_object) else:
msg = 'Managed BLMesh does not exist'
msg = 'Managed BLMesh does not exist' raise ValueError(msg)
raise ValueError(msg)
def bl_select(self) -> None: def bl_select(self) -> None:
"""Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport.""" """Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport."""
@ -138,7 +136,7 @@ class ManagedBLMesh(ct.schemas.ManagedObj):
if not (bl_object := bpy.data.objects.get(self.name)): if not (bl_object := bpy.data.objects.get(self.name)):
log.info( log.info(
'Creating BLMesh Object "%s"', 'Creating BLMesh Object "%s"',
bl_object.name, self.name,
) )
bl_data = bpy.data.meshes.new(self.name) bl_data = bpy.data.meshes.new(self.name)
bl_object = bpy.data.objects.new(self.name, bl_data) bl_object = bpy.data.objects.new(self.name, bl_data)

View File

@ -53,6 +53,7 @@ def write_modifier_geonodes(
bl_modifier: bpy.types.Modifier, bl_modifier: bpy.types.Modifier,
modifier_attrs: ModifierAttrsNODES, modifier_attrs: ModifierAttrsNODES,
) -> bool: ) -> bool:
modifier_altered = False
# Alter GeoNodes Group # Alter GeoNodes Group
if bl_modifier.node_group != modifier_attrs['node_group']: if bl_modifier.node_group != modifier_attrs['node_group']:
log.info( log.info(
@ -66,7 +67,7 @@ def write_modifier_geonodes(
# Alter GeoNodes Modifier Inputs # Alter GeoNodes Modifier Inputs
## First we retrieve the interface items by-Socket Name ## First we retrieve the interface items by-Socket Name
geonodes_interface = analyze_geonodes.interface( geonodes_interface = analyze_geonodes.interface(
bl_modifier.node_group, direct='INPUT' bl_modifier.node_group, direc='INPUT'
) )
for ( for (
socket_name, socket_name,
@ -74,11 +75,12 @@ def write_modifier_geonodes(
) in modifier_attrs['inputs'].items(): ) in modifier_attrs['inputs'].items():
# Compute Writable BL Socket Value # Compute Writable BL Socket Value
## Analyzes the socket and unitsys to prep a ready-to-write value. ## Analyzes the socket and unitsys to prep a ready-to-write value.
## Writte directly to the modifier dict. ## Write directly to the modifier dict.
bl_socket_value = bl_socket_map.writable_bl_socket_value( bl_socket_value = bl_socket_map.writable_bl_socket_value(
geonodes_interface[socket_name], geonodes_interface[socket_name],
value, value,
modifier_attrs['unit_system'], unit_system=modifier_attrs['unit_system'],
allow_unit_not_in_unit_system=True,
) )
# Compute Interface ID from Socket Name # Compute Interface ID from Socket Name
@ -91,6 +93,7 @@ def write_modifier_geonodes(
for i, bl_socket_subvalue in enumerate(bl_socket_value): for i, bl_socket_subvalue in enumerate(bl_socket_value):
if bl_modifier[iface_id][i] != bl_socket_subvalue: if bl_modifier[iface_id][i] != bl_socket_subvalue:
bl_modifier[iface_id][i] = bl_socket_subvalue bl_modifier[iface_id][i] = bl_socket_subvalue
modifier_altered = True
# IF int/float Mismatch: Assign Float-Cast of Integer # IF int/float Mismatch: Assign Float-Cast of Integer
## Blender is strict; only floats can set float vals. ## Blender is strict; only floats can set float vals.
@ -105,6 +108,8 @@ def write_modifier_geonodes(
bl_modifier[iface_id] = bl_socket_value bl_modifier[iface_id] = bl_socket_value
modifier_altered = True modifier_altered = True
return modifier_altered
def write_modifier( def write_modifier(
bl_modifier: bpy.types.Modifier, bl_modifier: bpy.types.Modifier,
@ -144,7 +149,7 @@ class ManagedBLModifier(ct.schemas.ManagedObj):
def name(self, value: str) -> None: def name(self, value: str) -> None:
## TODO: Handle name conflict within same BLObject ## TODO: Handle name conflict within same BLObject
log.info( log.info(
'Changing BLModifier w/Name "%s" to Name "%s"', self._bl_object_name, value 'Changing BLModifier w/Name "%s" to Name "%s"', self._modifier_name, value
) )
self._modifier_name = value self._modifier_name = value

View File

@ -2,7 +2,11 @@ import typing as typ
import bpy import bpy
from ...utils import logger
from . import contracts as ct from . import contracts as ct
from .managed_objs.managed_bl_collection import preview_collection
log = logger.get(__name__)
#################### ####################
# - Cache Management # - Cache Management
@ -73,6 +77,15 @@ class MaxwellSimTree(bpy.types.NodeTree):
for bl_socket in [*node.inputs, *node.outputs]: for bl_socket in [*node.inputs, *node.outputs]:
bl_socket.locked = False bl_socket.locked = False
def unpreview_all(self):
log.info('Disabling All 3D Previews')
for node in self.nodes:
if node.preview_active:
node.preview_active = False
for bl_object in preview_collection().objects.values():
preview_collection().objects.unlink(bl_object)
#################### ####################
# - Init Methods # - Init Methods
#################### ####################

View File

@ -87,8 +87,8 @@ class MaxwellSimNode(bpy.types.Node):
cls.__annotations__['preview_active'] = bpy.props.BoolProperty( cls.__annotations__['preview_active'] = bpy.props.BoolProperty(
name='Preview Active', name='Preview Active',
description='Whether the preview (if any) is currently active', description='Whether the preview (if any) is currently active',
default='', default=False,
update=lambda self, context: self.sync_prop('preview_active', context), update=lambda self, context: self.sync_preview_active(context),
) )
# Setup Locked Property for Node # Setup Locked Property for Node
@ -211,6 +211,21 @@ class MaxwellSimNode(bpy.types.Node):
## - If altered, set the 'sim_node_name' to the altered name. ## - If altered, set the 'sim_node_name' to the altered name.
## - This will cause recursion, but only once. ## - This will cause recursion, but only once.
def sync_preview_active(self, _: bpy.types.Context):
log.info(
'Changed Preview Active in "%s" to "%s"',
self.name,
self.preview_active,
)
for method in self._on_value_changed_methods:
if 'preview_active' in method.extra_data['changed_props']:
log.info(
'Running Previewer Callback "%s" in "%s")',
method.__name__,
self.name,
)
method(self)
#################### ####################
# - Managed Object Properties # - Managed Object Properties
#################### ####################
@ -571,6 +586,13 @@ class MaxwellSimNode(bpy.types.Node):
Invalidates (recursively) the cache of any managed object or Invalidates (recursively) the cache of any managed object or
output socket method that implicitly depends on this input socket. output socket method that implicitly depends on this input socket.
""" """
#log.debug(
# 'Action "%s" Triggered in "%s" (socket_name="%s", prop_name="%s")',
# action,
# self.name,
# socket_name,
# prop_name,
#)
# Forwards Chains # Forwards Chains
if action == 'value_changed': if action == 'value_changed':
# Run User Callbacks # Run User Callbacks
@ -589,6 +611,11 @@ class MaxwellSimNode(bpy.types.Node):
and socket_name in self.loose_input_sockets and socket_name in self.loose_input_sockets
) )
): ):
#log.debug(
# 'Running Value-Change Callback "%s" in "%s")',
# method.__name__,
# self.name,
#)
method(self) method(self)
# Propagate via Output Sockets # Propagate via Output Sockets
@ -616,6 +643,10 @@ class MaxwellSimNode(bpy.types.Node):
## ...which simply hook into the 'preview_active' property. ## ...which simply hook into the 'preview_active' property.
## By (maybe) altering 'preview_active', callbacks run as needed. ## By (maybe) altering 'preview_active', callbacks run as needed.
if not self.preview_active: if not self.preview_active:
log.info(
'Activating Preview in "%s")',
self.name,
)
self.preview_active = True self.preview_active = True
## Propagate via Input Sockets ## Propagate via Input Sockets

View File

@ -3,10 +3,13 @@ import inspect
import typing as typ import typing as typ
from types import MappingProxyType from types import MappingProxyType
from ....utils import sympy_extra_units as spux from ....utils import extra_sympy_units as spux
from ....utils import logger
from .. import contracts as ct from .. import contracts as ct
from .base import MaxwellSimNode from .base import MaxwellSimNode
log = logger.get(__name__)
UnitSystemID = str UnitSystemID = str
UnitSystem = dict[ct.SocketType, typ.Any] UnitSystem = dict[ct.SocketType, typ.Any]
@ -206,6 +209,10 @@ def event_decorator(
} }
method_kw_args |= {'loose_output_sockets': _loose_output_sockets} method_kw_args |= {'loose_output_sockets': _loose_output_sockets}
# Unit Systems
if unit_systems:
method_kw_args |= {'unit_systems': unit_systems}
# Call Method # Call Method
return method( return method(
node, node,
@ -253,19 +260,6 @@ def on_value_changed(
any_loose_input_socket: bool = False, any_loose_input_socket: bool = False,
**kwargs, **kwargs,
): ):
if (
sum(
[
int(socket_name is not None),
int(prop_name is not None),
int(any_loose_input_socket),
]
)
> 1
):
msg = 'Define only one of socket_name, prop_name or any_loose_input_socket'
raise ValueError(msg)
return event_decorator( return event_decorator(
action_type=EventCallbackType.on_value_changed, action_type=EventCallbackType.on_value_changed,
extra_data={ extra_data={

View File

@ -1,18 +1,18 @@
import bpy import typing as typ
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from .....utils import analyze_geonodes, logger from .....assets.import_geonodes import GeoNodes, import_geonodes
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
from .. import base, events from .. import base, events
log = logger.get(__name__) log = logger.get(__name__)
GEONODES_MONITOR_BOX = 'monitor_box'
class EHFieldMonitorNode(base.MaxwellSimNode): class EHFieldMonitorNode(base.MaxwellSimNode):
"""Node providing for the monitoring of electromagnetic fields within a given planar region or volume.""" """Node providing for the monitoring of electromagnetic fields within a given planar region or volume."""
@ -24,14 +24,14 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(), 'Size': sockets.PhysicalSize3DSocketDef(),
'Samples/Space': sockets.Integer3DVectorSocketDef( 'Samples/Space': sockets.Integer3DVectorSocketDef(
default_value=sp.Matrix([10, 10, 10]) default_value=sp.Matrix([10, 10, 10])
), ),
} }
input_socket_sets = { input_socket_sets: typ.ClassVar = {
'Freq Domain': { 'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef( 'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True, is_list=True,
@ -45,15 +45,17 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
), ),
}, },
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Monitor': sockets.MaxwellMonitorSocketDef(), 'Monitor': sockets.MaxwellMonitorSocketDef(),
} }
managed_obj_defs = { managed_obj_defs: typ.ClassVar = {
'monitor_box': ct.schemas.ManagedObjDef( 'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLMesh(name),
name_prefix='', ),
) 'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
@ -61,6 +63,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Monitor', 'Monitor',
props={'active_socket_set', 'sim_node_name'},
input_sockets={ input_sockets={
'Rec Start', 'Rec Start',
'Rec Stop', 'Rec Stop',
@ -70,60 +73,52 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Samples/Time', 'Samples/Time',
'Freqs', 'Freqs',
}, },
props={'active_socket_set', 'sim_node_name'}, unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
'Samples/Space': 'Tidy3DUnits',
'Rec Start': 'Tidy3DUnits',
'Rec Stop': 'Tidy3DUnits',
'Samples/Time': 'Tidy3DUnits',
},
) )
def compute_monitor( def compute_monitor(
self, input_sockets: dict, props: dict self, input_sockets: dict, props: dict, unit_systems: dict,
) -> td.FieldMonitor | td.FieldTimeMonitor: ) -> td.FieldMonitor | td.FieldTimeMonitor:
"""Computes the value of the 'Monitor' output socket, which the user can select as being either a `td.FieldMonitor` or `td.FieldTimeMonitor`."""
_center = input_sockets['Center']
_size = input_sockets['Size']
_samples_space = input_sockets['Samples/Space']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space)
if props['active_socket_set'] == 'Freq Domain': if props['active_socket_set'] == 'Freq Domain':
freqs = input_sockets['Freqs'] freqs = input_sockets['Freqs']
log.info( log.info(
'Computing FieldMonitor (name=%s) with center=%s, size=%s', 'Computing FieldMonitor (name="%s") with center="%s", size="%s"',
props['sim_node_name'], props['sim_node_name'],
center, input_sockets['Center'],
size, input_sockets['Size'],
) )
return td.FieldMonitor( return td.FieldMonitor(
center=center, center=input_sockets['Center'],
size=size, size=input_sockets['Size'],
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=samples_space, interval_space=input_sockets['Samples/Space'],
freqs=[ freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
], ],
) )
## Time Domain ## Time Domain
_rec_start = input_sockets['Rec Start']
_rec_stop = input_sockets['Rec Stop']
samples_time = input_sockets['Samples/Time']
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
log.info( log.info(
'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s', 'Computing FieldTimeMonitor (name=%s) with center=%s, size=%s',
props['sim_node_name'], props['sim_node_name'],
center, input_sockets['Center'],
size, input_sockets['Size'],
) )
return td.FieldTimeMonitor( return td.FieldTimeMonitor(
center=center, center=input_sockets['Center'],
size=size, size=input_sockets['Size'],
name=props['sim_node_name'], name=props['sim_node_name'],
start=rec_start, start=input_sockets['Rec Start'],
stop=rec_stop, stop=input_sockets['Rec Stop'],
interval=samples_time, interval=input_sockets['Samples/Time'],
interval_space=samples_space, interval_space=input_sockets['Samples/Space'],
) )
#################### ####################
@ -131,49 +126,37 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'monitor_box'}, managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
) )
def on_value_changed__center_size( def on_inputs_changed(
self, self,
input_sockets: dict, props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
): ):
"""Alters the managed 3D preview objects whenever the center or size input sockets are changed.""" # Push Input Values to GeoNodes Modifier
_center = input_sockets['Center'] managed_objs['modifier'].bl_modifier(
center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
_size = input_sockets['Size'] {
size = tuple([float(el) for el in spu.convert_to(_size, spu.um) / spu.um]) 'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
# Retrieve Hard-Coded GeoNodes and Analyze Input 'inputs': {
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX] 'Size': input_sockets['Size'],
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') },
# Sync Modifier Inputs
managed_objs['monitor_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
}, },
) )
# Push Preview State
# Sync Object Position if props['preview_active']:
managed_objs['monitor_box'].bl_object('MESH').location = center managed_objs['mesh'].show_preview()
####################
# - Preview - Show Preview
####################
@events.on_show_preview(
managed_objs={'monitor_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
"""Requests that the managed object be previewed in response to a user request to show the preview."""
managed_objs['monitor_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################

View File

@ -1,15 +1,18 @@
import typing as typ
import bpy import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from .....utils import analyze_geonodes from .....assets.import_geonodes import GeoNodes, import_geonodes
from .....utils import extra_sympy_units as spux from .....utils import extra_sympy_units as spux
from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
from .. import base, events from .. import base, events
GEONODES_MONITOR_BOX = 'monitor_flux_box' log = logger.get(__name__)
class FieldPowerFluxMonitorNode(base.MaxwellSimNode): class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
@ -20,7 +23,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(), 'Size': sockets.PhysicalSize3DSocketDef(),
'Samples/Space': sockets.Integer3DVectorSocketDef( 'Samples/Space': sockets.Integer3DVectorSocketDef(
@ -28,7 +31,7 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
), ),
'Direction': sockets.BoolSocketDef(), 'Direction': sockets.BoolSocketDef(),
} }
input_socket_sets = { input_socket_sets: typ.ClassVar = {
'Freq Domain': { 'Freq Domain': {
'Freqs': sockets.PhysicalFreqSocketDef( 'Freqs': sockets.PhysicalFreqSocketDef(
is_list=True, is_list=True,
@ -42,35 +45,25 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
), ),
}, },
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Monitor': sockets.MaxwellMonitorSocketDef(), 'Monitor': sockets.MaxwellMonitorSocketDef(),
} }
managed_obj_defs = { managed_obj_defs: typ.ClassVar = {
'monitor_box': ct.schemas.ManagedObjDef( 'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLMesh(name),
name_prefix='', ),
) 'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Properties # - Event Methods: Computation
####################
####################
# - UI
####################
def draw_props(self, context, layout):
pass
def draw_info(self, context, col):
pass
####################
# - Output Sockets
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Monitor', 'Monitor',
props={'active_socket_set', 'sim_node_name'},
input_sockets={ input_sockets={
'Rec Start', 'Rec Start',
'Rec Stop', 'Rec Stop',
@ -81,102 +74,86 @@ class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
'Freqs', 'Freqs',
'Direction', 'Direction',
}, },
props={'active_socket_set', 'sim_node_name'}, unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
'Samples/Space': 'Tidy3DUnits',
'Rec Start': 'Tidy3DUnits',
'Rec Stop': 'Tidy3DUnits',
'Samples/Time': 'Tidy3DUnits',
},
) )
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor: def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
_center = input_sockets['Center']
_size = input_sockets['Size']
_samples_space = input_sockets['Samples/Space']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space)
direction = '+' if input_sockets['Direction'] else '-' direction = '+' if input_sockets['Direction'] else '-'
if props['active_socket_set'] == 'Freq Domain': if props['active_socket_set'] == 'Freq Domain':
freqs = input_sockets['Freqs'] freqs = input_sockets['Freqs']
log.info(
'Computing FluxMonitor (name="%s") with center="%s", size="%s"',
props['sim_node_name'],
input_sockets['Center'],
input_sockets['Size'],
)
return td.FluxMonitor( return td.FluxMonitor(
center=center, center=input_sockets['Center'],
size=size, size=input_sockets['Size'],
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=samples_space, interval_space=input_sockets['Samples/Space'],
freqs=[ freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs float(spu.convert_to(freq, spu.hertz) / spu.hertz) for freq in freqs
], ],
normal_dir=direction, normal_dir=direction,
) )
else: ## Time Domain
_rec_start = input_sockets['Rec Start']
_rec_stop = input_sockets['Rec Stop']
samples_time = input_sockets['Samples/Time']
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second return td.FluxTimeMonitor(
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second center=input_sockets['Center'],
size=input_sockets['Size'],
return td.FieldTimeMonitor( name=props['sim_node_name'],
center=center, start=input_sockets['Rec Start'],
size=size, stop=input_sockets['Rec Stop'],
name=props['sim_node_name'], interval=input_sockets['Samples/Time'],
start=rec_start, interval_space=input_sockets['Samples/Space'],
stop=rec_stop, normal_dir=direction,
interval=samples_time, )
interval_space=samples_space,
)
#################### ####################
# - Preview - Changes to Input Sockets # - Preview - Changes to Input Sockets
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
input_sockets={'Center', 'Size', 'Direction'}, prop_name='preview_active',
managed_objs={'monitor_box'}, props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
) )
def on_value_changed__center_size( def on_inputs_changed(
self, self,
input_sockets: dict, props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
): ):
_center = input_sockets['Center'] # Push Input Values to GeoNodes Modifier
center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
_size = input_sockets['Size'] 'NODES',
size = tuple([float(el) for el in spu.convert_to(_size, spu.um) / spu.um]) {
## TODO: Preview unit system?? Presume um for now 'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
# Retrieve Hard-Coded GeoNodes and Analyze Input 'inputs': {
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX] 'Size': input_sockets['Size'],
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') },
# Sync Modifier Inputs
managed_objs['monitor_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
geonodes_interface['Direction'].identifier: input_sockets['Direction'],
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
# Sync Object Position if props['preview_active']:
managed_objs['monitor_box'].bl_object('MESH').location = center managed_objs['mesh'].show_preview()
####################
# - Preview - Show Preview
####################
@events.on_show_preview(
managed_objs={'monitor_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['monitor_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################

View File

@ -1,10 +1,12 @@
import typing as typ
import bpy import bpy
import sympy as sp import sympy as sp
from .....utils import logger from .....utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from ...managed_objs import managed_bl_object from ...managed_objs.managed_bl_collection import preview_collection
from .. import base, events from .. import base, events
log = logger.get(__name__) log = logger.get(__name__)
@ -46,7 +48,7 @@ class ViewerNode(base.MaxwellSimNode):
node_type = ct.NodeType.Viewer node_type = ct.NodeType.Viewer
bl_label = 'Viewer' bl_label = 'Viewer'
input_sockets = { input_sockets: typ.ClassVar = {
'Data': sockets.AnySocketDef(), 'Data': sockets.AnySocketDef(),
} }
@ -67,6 +69,12 @@ class ViewerNode(base.MaxwellSimNode):
update=lambda self, context: self.sync_prop('auto_3d_preview', context), update=lambda self, context: self.sync_prop('auto_3d_preview', context),
) )
cache__data_was_unlinked: bpy.props.BoolProperty(
name='Data Was Unlinked',
description="Whether the Data input was unlinked last time it was checked.",
default=True,
)
#################### ####################
# - UI # - UI
#################### ####################
@ -111,46 +119,39 @@ class ViewerNode(base.MaxwellSimNode):
console.print(data) console.print(data)
#################### ####################
# - Updates # - Event Methods
#################### ####################
@events.on_value_changed(
socket_name='Data',
props={'auto_plot'},
)
def on_changed_2d_data(self, props):
# Show Plot
## Don't have to un-show other plots.
if self.inputs['Data'].is_linked and props['auto_plot']:
self.trigger_action('show_plot')
@events.on_value_changed( @events.on_value_changed(
socket_name='Data', socket_name='Data',
props={'auto_3d_preview'}, props={'auto_3d_preview'},
) )
def on_value_changed__data(self, props): def on_changed_3d_data(self, props):
# Show Plot # Data Not Attached
## Don't have to un-show other plots. if not self.inputs['Data'].is_linked:
if self.auto_plot: self.cache__data_was_unlinked = True
self.trigger_action('show_plot')
# Remove Anything Previewed # Data Just Attached
preview_collection = managed_bl_object.bl_collection( elif self.cache__data_was_unlinked:
managed_bl_object.PREVIEW_COLLECTION_NAME, node_tree = self.id_data
view_layer_exclude=False,
)
for bl_object in preview_collection.objects.values():
preview_collection.objects.unlink(bl_object)
# Preview Anything that Should be Previewed (maybe) # Unpreview Everything
if props['auto_3d_preview']: node_tree.unpreview_all()
self.trigger_action('show_preview')
@events.on_value_changed( # Enable Previews in Tree
prop_name='auto_3d_preview', if props['auto_3d_preview']:
props={'auto_3d_preview'}, log.info('Enabling 3D Previews from "%s"', self.name)
) self.trigger_action('show_preview')
def on_value_changed__auto_3d_preview(self, props): self.cache__data_was_unlinked = False
# Remove Anything Previewed
preview_collection = managed_bl_object.bl_collection(
managed_bl_object.PREVIEW_COLLECTION_NAME,
view_layer_exclude=False,
)
for bl_object in preview_collection.objects.values():
preview_collection.objects.unlink(bl_object)
# Preview Anything that Should be Previewed (maybe)
if props['auto_3d_preview']:
self.trigger_action('show_preview')
#################### ####################

View File

@ -1,116 +1,102 @@
import bpy import typing as typ
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
from .....utils import analyze_geonodes from .....assets.import_geonodes import GeoNodes, import_geonodes
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
from .. import base, events from .. import base, events
GEONODES_DOMAIN_BOX = 'simdomain_box'
class SimDomainNode(base.MaxwellSimNode): class SimDomainNode(base.MaxwellSimNode):
node_type = ct.NodeType.SimDomain node_type = ct.NodeType.SimDomain
bl_label = 'Sim Domain' bl_label = 'Sim Domain'
use_sim_node_name = True
input_sockets = { input_sockets: typ.ClassVar = {
'Duration': sockets.PhysicalTimeSocketDef( 'Duration': sockets.PhysicalTimeSocketDef(
default_value=5 * spu.ps, default_value=5 * spu.ps,
default_unit=spu.ps, default_unit=spu.ps,
), ),
'Center': sockets.PhysicalSize3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(), 'Size': sockets.PhysicalSize3DSocketDef(),
'Grid': sockets.MaxwellSimGridSocketDef(), 'Grid': sockets.MaxwellSimGridSocketDef(),
'Ambient Medium': sockets.MaxwellMediumSocketDef(), 'Ambient Medium': sockets.MaxwellMediumSocketDef(),
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Domain': sockets.MaxwellSimDomainSocketDef(), 'Domain': sockets.MaxwellSimDomainSocketDef(),
} }
managed_obj_defs = { managed_obj_defs: typ.ClassVar = {
'domain_box': ct.schemas.ManagedObjDef( 'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLMesh(name),
name_prefix='', name_prefix='',
) ),
'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
# - Callbacks # - Event Methods
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Domain', 'Domain',
input_sockets={'Duration', 'Center', 'Size', 'Grid', 'Ambient Medium'}, input_sockets={'Duration', 'Center', 'Size', 'Grid', 'Ambient Medium'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Duration': 'Tidy3DUnits',
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
},
) )
def compute_sim_domain(self, input_sockets: dict) -> sp.Expr: def compute_output(self, input_sockets: dict) -> sp.Expr:
if all( return {
[ 'run_time': input_sockets['Duration'],
(_duration := input_sockets['Duration']), 'center': input_sockets['Center'],
(_center := input_sockets['Center']), 'size': input_sockets['Size'],
(_size := input_sockets['Size']), 'grid_spec': input_sockets['Grid'],
(grid := input_sockets['Grid']), 'medium': input_sockets['Ambient Medium'],
(medium := input_sockets['Ambient Medium']), }
]
):
duration = spu.convert_to(_duration, spu.second) / spu.second
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
return dict(
run_time=duration,
center=center,
size=size,
grid_spec=grid,
medium=medium,
)
####################
# - Preview
####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'domain_box'}, managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
) )
def on_value_changed__center_size( def on_input_changed(
self, self,
input_sockets: dict, props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
): ):
_center = input_sockets['Center'] # Push Input Values to GeoNodes Modifier
center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
_size = input_sockets['Size'] 'NODES',
size = tuple([float(el) for el in spu.convert_to(_size, spu.um) / spu.um]) {
## TODO: Preview unit system?? Presume um for now 'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'],
# Retrieve Hard-Coded GeoNodes and Analyze Input 'inputs': {
geo_nodes = bpy.data.node_groups[GEONODES_DOMAIN_BOX] 'Size': input_sockets['Size'],
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') },
# Sync Modifier Inputs
managed_objs['domain_box'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Size'].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
# Sync Object Position @events.on_init()
managed_objs['domain_box'].bl_object('MESH').location = center def on_init(self):
self.on_input_change()
@events.on_show_preview(
managed_objs={'domain_box'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['domain_box'].show_preview('MESH')
self.on_value_changed__center_size()
#################### ####################

View File

@ -1,17 +1,13 @@
import math import math
import typing as typ
import bpy
import sympy as sp import sympy as sp
import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from .....utils import analyze_geonodes
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
from .. import base, events from .. import base, events
GEONODES_PLANE_WAVE = 'source_plane_wave'
def convert_vector_to_spherical( def convert_vector_to_spherical(
v: sp.MatrixBase, v: sp.MatrixBase,
@ -50,17 +46,17 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(), 'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Direction': sockets.Real3DVectorSocketDef(default_value=sp.Matrix([0, 0, -1])), 'Direction': sockets.Real3DVectorSocketDef(default_value=sp.Matrix([0, 0, -1])),
'Pol Angle': sockets.PhysicalAngleSocketDef(), 'Pol Angle': sockets.PhysicalAngleSocketDef(),
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Source': sockets.MaxwellSourceSocketDef(), 'Source': sockets.MaxwellSourceSocketDef(),
} }
managed_obj_defs = { managed_obj_defs: typ.ClassVar = {
'plane_wave_source': ct.schemas.ManagedObjDef( 'plane_wave_source': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix='', name_prefix='',
@ -73,26 +69,29 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'Source', 'Source',
input_sockets={'Temporal Shape', 'Center', 'Direction', 'Pol Angle'}, input_sockets={'Temporal Shape', 'Center', 'Direction', 'Pol Angle'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
},
) )
def compute_source(self, input_sockets: dict): def compute_source(self, input_sockets: dict):
temporal_shape = input_sockets['Temporal Shape']
_center = input_sockets['Center']
direction = input_sockets['Direction'] direction = input_sockets['Direction']
pol_angle = input_sockets['Pol Angle'] pol_angle = input_sockets['Pol Angle']
injection_axis, dir_sgn, theta, phi = convert_vector_to_spherical(direction) injection_axis, dir_sgn, theta, phi = convert_vector_to_spherical(
input_sockets['Direction']
)
size = { size = {
'x': (0, math.inf, math.inf), 'x': (0, math.inf, math.inf),
'y': (math.inf, 0, math.inf), 'y': (math.inf, 0, math.inf),
'z': (math.inf, math.inf, 0), 'z': (math.inf, math.inf, 0),
}[injection_axis] }[injection_axis]
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
# Display the results # Display the results
return td.PlaneWave( return td.PlaneWave(
center=center, center=input_sockets['Center'],
source_time=temporal_shape, source_time=input_sockets['Temporal Shape'],
size=size, size=size,
direction=dir_sgn, direction=dir_sgn,
angle_theta=theta, angle_theta=theta,
@ -100,54 +99,54 @@ class PlaneWaveSourceNode(base.MaxwellSimNode):
pol_angle=pol_angle, pol_angle=pol_angle,
) )
#################### #####################
# - Preview ## - Preview
#################### #####################
@events.on_value_changed( # @events.on_value_changed(
socket_name={'Center', 'Direction'}, # socket_name={'Center', 'Direction'},
input_sockets={'Center', 'Direction'}, # input_sockets={'Center', 'Direction'},
managed_objs={'plane_wave_source'}, # managed_objs={'plane_wave_source'},
) # )
def on_value_changed__center_direction( # def on_value_changed__center_direction(
self, # self,
input_sockets: dict, # input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], # managed_objs: dict[str, ct.schemas.ManagedObj],
): # ):
_center = input_sockets['Center'] # _center = input_sockets['Center']
center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) # center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um])
_direction = input_sockets['Direction'] # _direction = input_sockets['Direction']
direction = tuple([float(el) for el in _direction]) # direction = tuple([float(el) for el in _direction])
## TODO: Preview unit system?? Presume um for now # ## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input # # Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_PLANE_WAVE] # geo_nodes = bpy.data.node_groups[GEONODES_PLANE_WAVE]
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') # geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT')
# Sync Modifier Inputs # # Sync Modifier Inputs
managed_objs['plane_wave_source'].sync_geonodes_modifier( # managed_objs['plane_wave_source'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes, # geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={ # geonodes_identifier_to_value={
geonodes_interface['Direction'].identifier: direction, # geonodes_interface['Direction'].identifier: direction,
## TODO: Use 'bl_socket_map.value_to_bl`! # ## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. . # ## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class... # ## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm. # ## - ...But it needs aligning with Blender, too. Hmm.
}, # },
) # )
# Sync Object Position # # Sync Object Position
managed_objs['plane_wave_source'].bl_object('MESH').location = center # managed_objs['plane_wave_source'].bl_object('MESH').location = center
@events.on_show_preview( # @events.on_show_preview(
managed_objs={'plane_wave_source'}, # managed_objs={'plane_wave_source'},
) # )
def on_show_preview( # def on_show_preview(
self, # self,
managed_objs: dict[str, ct.schemas.ManagedObj], # managed_objs: dict[str, ct.schemas.ManagedObj],
): # ):
managed_objs['plane_wave_source'].show_preview('MESH') # managed_objs['plane_wave_source'].show_preview('MESH')
self.on_value_changed__center_direction() # self.on_value_changed__center_direction()
#################### ####################

View File

@ -68,6 +68,10 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
'Source', 'Source',
input_sockets={'Temporal Shape', 'Center', 'Interpolate'}, input_sockets={'Temporal Shape', 'Center', 'Interpolate'},
props={'pol_axis'}, props={'pol_axis'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
},
) )
def compute_source( def compute_source(
self, input_sockets: dict[str, typ.Any], props: dict[str, typ.Any] self, input_sockets: dict[str, typ.Any], props: dict[str, typ.Any]
@ -78,53 +82,46 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
'EZ': 'Ez', 'EZ': 'Ez',
}[props['pol_axis']] }[props['pol_axis']]
temporal_shape = input_sockets['Temporal Shape'] return td.PointDipole(
_center = input_sockets['Center'] center=input_sockets['Center'],
interpolate = input_sockets['Interpolate'] source_time=input_sockets['Temporal Shape'],
interpolate=input_sockets['Interpolate'],
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
_res = td.PointDipole(
center=center,
source_time=temporal_shape,
interpolate=interpolate,
polarization=pol_axis, polarization=pol_axis,
) )
return _res
#################### #####################
# - Preview ## - Preview
#################### #####################
@events.on_value_changed( # @events.on_value_changed(
socket_name='Center', # socket_name='Center',
input_sockets={'Center'}, # input_sockets={'Center'},
managed_objs={'sphere_empty'}, # managed_objs={'sphere_empty'},
) # )
def on_value_changed__center( # def on_value_changed__center(
self, # self,
input_sockets: dict, # input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], # managed_objs: dict[str, ct.schemas.ManagedObj],
): # ):
_center = input_sockets['Center'] # _center = input_sockets['Center']
center = tuple(spu.convert_to(_center, spu.um) / spu.um) # center = tuple(spu.convert_to(_center, spu.um) / spu.um)
## TODO: Preview unit system?? Presume um for now # ## TODO: Preview unit system?? Presume um for now
mobj = managed_objs['sphere_empty'] # mobj = managed_objs['sphere_empty']
bl_object = mobj.bl_object('EMPTY') # bl_object = mobj.bl_object('EMPTY')
bl_object.location = center # tuple([float(el) for el in center]) # bl_object.location = center # tuple([float(el) for el in center])
@events.on_show_preview( # @events.on_show_preview(
managed_objs={'sphere_empty'}, # managed_objs={'sphere_empty'},
) # )
def on_show_preview( # def on_show_preview(
self, # self,
managed_objs: dict[str, ct.schemas.ManagedObj], # managed_objs: dict[str, ct.schemas.ManagedObj],
): # ):
managed_objs['sphere_empty'].show_preview( # managed_objs['sphere_empty'].show_preview(
'EMPTY', # 'EMPTY',
empty_display_type='SPHERE', # empty_display_type='SPHERE',
) # )
managed_objs['sphere_empty'].bl_object('EMPTY').empty_display_size = 0.2 # managed_objs['sphere_empty'].bl_object('EMPTY').empty_display_size = 0.2
#################### ####################

View File

@ -71,10 +71,10 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
socket_name='GeoNodes', socket_name='GeoNodes',
prop_name='preview_active', prop_name='preview_active',
any_loose_input_socket=True, any_loose_input_socket=True,
# Method Data props={'preview_active'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
input_sockets={'GeoNodes'}, input_sockets={'Center', 'GeoNodes'},
# Unit System Scaling all_loose_input_sockets=True,
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
) )
def on_input_changed( def on_input_changed(
@ -127,15 +127,22 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
## Changing socket.value invokes recursion of this function. ## Changing socket.value invokes recursion of this function.
## The else: below ensures that only one push occurs. ## The else: below ensures that only one push occurs.
## (well, one push per .value set, which simplifies to one push) ## (well, one push per .value set, which simplifies to one push)
log.debug( log.info(
'Setting Loose Input Sockets of "%s" to GeoNodes Defaults', 'Setting Loose Input Sockets of "%s" to GeoNodes Defaults',
self.bl_label, self.bl_label,
) )
for socket_name in self.loose_input_sockets: for socket_name in self.loose_input_sockets:
socket = self.inputs[socket_name] socket = self.inputs[socket_name]
socket.value = bl_socket_map.read_bl_socket_default_value( socket.value = bl_socket_map.read_bl_socket_default_value(
geonodes_interface[socket_name] geonodes_interface[socket_name],
unit_systems['BlenderUnits'],
allow_unit_not_in_unit_system=True,
) )
log.info(
'Set Loose Input Sockets of "%s" to: %s',
self.bl_label,
str(self.loose_input_sockets),
)
else: else:
# Push Loose Input Values to GeoNodes Modifier # Push Loose Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier( managed_objs['modifier'].bl_modifier(

View File

@ -4,13 +4,11 @@ import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from .....assets.import_geonodes import import_geonodes from ......assets.import_geonodes import GeoNodes, import_geonodes
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import managed_objs, sockets
from ... import base, events from ... import base, events
GEONODES_BOX = 'box'
class BoxStructureNode(base.MaxwellSimNode): class BoxStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoxStructure node_type = ct.NodeType.BoxStructure
@ -64,16 +62,15 @@ class BoxStructureNode(base.MaxwellSimNode):
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
prop_name='preview_active', prop_name='preview_active',
# Method Data props={'preview_active'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
# Unit System Scaling
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={
'Center': 'BlenderUnits', 'Center': 'BlenderUnits',
}, },
) )
def on_input_changed( def on_inputs_changed(
self, self,
props: dict, props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
@ -85,7 +82,7 @@ class BoxStructureNode(base.MaxwellSimNode):
managed_objs['mesh'].bl_object(location=input_sockets['Center']), managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES', 'NODES',
{ {
'node_group': import_geonodes(GEONODES_BOX, 'link'), 'node_group': import_geonodes(GeoNodes.PrimitiveBox, 'link'),
'unit_system': unit_systems['BlenderUnits'], 'unit_system': unit_systems['BlenderUnits'],
'inputs': { 'inputs': {
'Size': input_sockets['Size'], 'Size': input_sockets['Size'],
@ -98,7 +95,7 @@ class BoxStructureNode(base.MaxwellSimNode):
@events.on_init() @events.on_init()
def on_init(self): def on_init(self):
self.on_input_change() self.on_inputs_changed()
#################### ####################

View File

@ -1,38 +1,40 @@
import bpy import typing as typ
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from ......utils import analyze_geonodes from ......assets.import_geonodes import GeoNodes, import_geonodes
from .... import contracts as ct from .... import contracts as ct
from .... import managed_objs, sockets from .... import managed_objs, sockets
from ... import base, events from ... import base, events
GEONODES_STRUCTURE_SPHERE = 'structure_sphere'
class SphereStructureNode(base.MaxwellSimNode): class SphereStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.SphereStructure node_type = ct.NodeType.SphereStructure
bl_label = 'Sphere Structure' bl_label = 'Sphere Structure'
use_sim_node_name = True
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets: typ.ClassVar = {
'Center': sockets.PhysicalPoint3DSocketDef(), 'Center': sockets.PhysicalPoint3DSocketDef(),
'Radius': sockets.PhysicalLengthSocketDef( 'Radius': sockets.PhysicalLengthSocketDef(
default_value=150 * spu.nm, default_value=150 * spu.nm,
), ),
'Medium': sockets.MaxwellMediumSocketDef(), 'Medium': sockets.MaxwellMediumSocketDef(),
} }
output_sockets = { output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(), 'Structure': sockets.MaxwellStructureSocketDef(),
} }
managed_obj_defs = { managed_obj_defs: typ.ClassVar = {
'structure_sphere': ct.schemas.ManagedObjDef( 'mesh': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLMesh(name),
name_prefix='', ),
) 'modifier': ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLModifier(name),
),
} }
#################### ####################
@ -41,21 +43,19 @@ class SphereStructureNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'Structure', 'Structure',
input_sockets={'Center', 'Radius', 'Medium'}, input_sockets={'Center', 'Radius', 'Medium'},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Radius': 'Tidy3DUnits',
},
) )
def compute_structure(self, input_sockets: dict) -> td.Box: def compute_structure(self, input_sockets: dict) -> td.Box:
medium = input_sockets['Medium']
_center = input_sockets['Center']
_radius = input_sockets['Radius']
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
radius = spu.convert_to(_radius, spu.um) / spu.um
return td.Structure( return td.Structure(
geometry=td.Sphere( geometry=td.Sphere(
radius=radius, radius=input_sockets['Radius'],
center=center, center=input_sockets['Center'],
), ),
medium=medium, medium=input_sockets['Medium'],
) )
#################### ####################
@ -63,52 +63,42 @@ class SphereStructureNode(base.MaxwellSimNode):
#################### ####################
@events.on_value_changed( @events.on_value_changed(
socket_name={'Center', 'Radius'}, socket_name={'Center', 'Radius'},
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center', 'Radius'}, input_sockets={'Center', 'Radius'},
managed_objs={'structure_sphere'}, managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Radius': 'Tidy3DUnits',
},
) )
def on_value_changed__center_radius( def on_inputs_changed(
self, self,
input_sockets: dict, props: dict,
managed_objs: dict[str, ct.schemas.ManagedObj], managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict,
unit_systems: dict,
): ):
_center = input_sockets['Center'] # Push Input Values to GeoNodes Modifier
center = tuple([float(el) for el in spu.convert_to(_center, spu.um) / spu.um]) managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
_radius = input_sockets['Radius'] 'NODES',
radius = float(spu.convert_to(_radius, spu.um) / spu.um) {
## TODO: Preview unit system?? Presume um for now 'node_group': import_geonodes(GeoNodes.PrimitiveSphere, 'link'),
'unit_system': unit_systems['BlenderUnits'],
# Retrieve Hard-Coded GeoNodes and Analyze Input 'inputs': {
geo_nodes = bpy.data.node_groups[GEONODES_STRUCTURE_SPHERE] 'Radius': input_sockets['Radius'],
geonodes_interface = analyze_geonodes.interface(geo_nodes, direc='INPUT') },
# Sync Modifier Inputs
managed_objs['structure_sphere'].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface['Radius'].identifier: radius,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}, },
) )
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
# Sync Object Position @events.on_init()
managed_objs['structure_sphere'].bl_object('MESH').location = center def on_init(self):
self.on_inputs_changed()
####################
# - Preview - Show Preview
####################
@events.on_show_preview(
managed_objs={'structure_sphere'},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs['structure_sphere'].show_preview('MESH')
self.on_value_changed__center_radius()
#################### ####################

View File

@ -42,7 +42,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
# - Initialization # - Initialization
#################### ####################
def __init_subclass__(cls, **kwargs: typ.Any): def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs) ## Yucky superclass setup. super().__init_subclass__(**kwargs)
# Setup Blender ID for Node # Setup Blender ID for Node
if not hasattr(cls, 'socket_type'): if not hasattr(cls, 'socket_type'):
@ -266,15 +266,12 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
if kind == ct.DataFlowKind.Value: if kind == ct.DataFlowKind.Value:
if self.is_list: if self.is_list:
return self.value_list return self.value_list
else: return self.value
return self.value if kind == ct.DataFlowKind.LazyValue:
elif kind == ct.DataFlowKind.LazyValue:
if self.is_list: if self.is_list:
return self.lazy_value_list return self.lazy_value_list
else:
return self.lazy_value
return self.lazy_value return self.lazy_value
elif kind == ct.DataFlowKind.Capabilities: if kind == ct.DataFlowKind.Capabilities:
return self.capabilities return self.capabilities
return None return None

View File

@ -3,10 +3,14 @@ import numpy as np
import pydantic as pyd import pydantic as pyd
import sympy.physics.units as spu import sympy.physics.units as spu
from .....utils import logger
from .....utils import extra_sympy_units as spux
from .....utils.pydantic_sympy import SympyExpr from .....utils.pydantic_sympy import SympyExpr
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
log = logger.get(__name__)
#################### ####################
# - Blender Socket # - Blender Socket
@ -20,8 +24,8 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
# - Properties # - Properties
#################### ####################
raw_value: bpy.props.FloatProperty( raw_value: bpy.props.FloatProperty(
name='Unitless Force', name='Unitless Length',
description='Represents the unitless part of the force', description='Represents the unitless part of the length',
default=0.0, default=0.0,
precision=6, precision=6,
update=(lambda self, context: self.sync_prop('raw_value', context)), update=(lambda self, context: self.sync_prop('raw_value', context)),
@ -68,7 +72,7 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
@value.setter @value.setter
def value(self, value: SympyExpr) -> None: def value(self, value: SympyExpr) -> None:
self.raw_value = spu.convert_to(value, self.unit) / self.unit self.raw_value = spux.sympy_to_python(spux.scale_to_unit(value, self.unit))
@property @property
def value_list(self) -> list[SympyExpr]: def value_list(self) -> list[SympyExpr]:

View File

@ -12,7 +12,6 @@ with pydeps.syspath_from_bpy_prefs():
#################### ####################
# - Useful Methods # - Useful Methods
#################### ####################
@functools.lru_cache(maxsize=4096)
def uses_units(expression: sp.Expr) -> bool: def uses_units(expression: sp.Expr) -> bool:
## TODO: An LFU cache could do better than an LRU. ## TODO: An LFU cache could do better than an LRU.
"""Checks if an expression uses any units (`Quantity`).""" """Checks if an expression uses any units (`Quantity`)."""
@ -23,7 +22,6 @@ def uses_units(expression: sp.Expr) -> bool:
# Function to return a set containing all units used in the expression # Function to return a set containing all units used in the expression
@functools.lru_cache(maxsize=4096)
def get_units(expression: sp.Expr): def get_units(expression: sp.Expr):
## TODO: An LFU cache could do better than an LRU. ## TODO: An LFU cache could do better than an LRU.
"""Gets all the units of an expression (as `Quantity`).""" """Gets all the units of an expression (as `Quantity`)."""
@ -94,7 +92,6 @@ def parse_abbrev_symbols_to_units(expr: sp.Basic) -> sp.Basic:
#################### ####################
# - Units <-> Scalars # - Units <-> Scalars
#################### ####################
@functools.lru_cache(maxsize=8192)
def scale_to_unit(expr: sp.Expr, unit: spu.Quantity) -> typ.Any: def scale_to_unit(expr: sp.Expr, unit: spu.Quantity) -> typ.Any:
## TODO: An LFU cache could do better than an LRU. ## TODO: An LFU cache could do better than an LRU.
unitless_expr = spu.convert_to(expr, unit) / unit unitless_expr = spu.convert_to(expr, unit) / unit
@ -108,7 +105,6 @@ def scale_to_unit(expr: sp.Expr, unit: spu.Quantity) -> typ.Any:
#################### ####################
# - Sympy <-> Scalars # - Sympy <-> Scalars
#################### ####################
@functools.lru_cache(maxsize=8192)
def sympy_to_python(scalar: sp.Basic) -> int | float | complex | tuple | list: def sympy_to_python(scalar: sp.Basic) -> int | float | complex | tuple | list:
"""Convert a scalar sympy expression to the directly corresponding Python type. """Convert a scalar sympy expression to the directly corresponding Python type.
@ -128,7 +124,7 @@ def sympy_to_python(scalar: sp.Basic) -> int | float | complex | tuple | list:
# Detect Row / Column Vector # Detect Row / Column Vector
## When it's "actually" a 1D structure, flatten and return as tuple. ## When it's "actually" a 1D structure, flatten and return as tuple.
if 1 in scalar.shape: if 1 in scalar.shape:
return tuple(itertools.from_iterable(list_2d)) return tuple(itertools.chain.from_iterable(list_2d))
return list_2d return list_2d
if scalar.is_integer: if scalar.is_integer:

View File

@ -111,28 +111,24 @@ def ConstrSympyExpr(
allowed_sets allowed_sets
and isinstance(expr, sp.Expr) and isinstance(expr, sp.Expr)
and not any( and not any(
[ {
{ 'integer': expr.is_integer,
'integer': expr.is_integer, 'rational': expr.is_rational,
'rational': expr.is_rational, 'real': expr.is_real,
'real': expr.is_real, 'complex': expr.is_complex,
'complex': expr.is_complex, }[allowed_set]
}[allowed_set] for allowed_set in allowed_sets
for allowed_set in allowed_sets
]
) )
): ):
msgs.add( msgs.add(
f"allowed_sets={allowed_sets} does not match expression {expr} (remember to add assumptions to symbols, ex. `x = sp.Symbol('x', real=True))" f"allowed_sets={allowed_sets} does not match expression {expr} (remember to add assumptions to symbols, ex. `x = sp.Symbol('x', real=True))"
) )
if allowed_structures and not any( if allowed_structures and not any(
[ {
{ 'matrix': isinstance(expr, sp.MatrixBase),
'matrix': isinstance(expr, sp.MatrixBase), }[allowed_set]
}[allowed_set] for allowed_set in allowed_structures
for allowed_set in allowed_structures if allowed_structures != 'scalar'
if allowed_structures != 'scalar'
]
): ):
msgs.add( msgs.add(
f"allowed_structures={allowed_structures} does not match expression {expr} (remember to add assumptions to symbols, ex. `x = sp.Symbol('x', real=True))" f"allowed_structures={allowed_structures} does not match expression {expr} (remember to add assumptions to symbols, ex. `x = sp.Symbol('x', real=True))"