feat: perm monitor, enh. monitors, sim node naming

We greatly enhanced the field, flux monitors, and added the permittivity
monitor.

mobj naming is still a bit borked; we need a node-tree bound namespace
to go further with it.
For now, don't duplicate nodes, and don't use multiple node trees.
main
Sofus Albert Høgsbro Rose 2024-05-16 11:06:18 +02:00
parent af358a4d32
commit 92be84ec8a
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
21 changed files with 487 additions and 290 deletions

View File

@ -75,7 +75,7 @@ class GeoNodes(enum.StrEnum):
## Monitor ## Monitor
MonitorEHField = '_monitor_eh_field' MonitorEHField = '_monitor_eh_field'
MonitorPowerFlux = '_monitor_power_flux' MonitorPowerFlux = '_monitor_power_flux'
MonitorEpsTensor = '_monitor_eps_tensor' MonitorPermittivity = '_monitor_permittivity'
MonitorDiffraction = '_monitor_diffraction' MonitorDiffraction = '_monitor_diffraction'
MonitorProjCartEHField = '_monitor_proj_eh_field' MonitorProjCartEHField = '_monitor_proj_eh_field'
MonitorProjAngEHField = '_monitor_proj_ang_eh_field' MonitorProjAngEHField = '_monitor_proj_ang_eh_field'
@ -186,7 +186,7 @@ class GeoNodes(enum.StrEnum):
## Monitor ## Monitor
GN.MonitorEHField: GN_INTERNAL_MONITORS_PATH, GN.MonitorEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorPowerFlux: GN_INTERNAL_MONITORS_PATH, GN.MonitorPowerFlux: GN_INTERNAL_MONITORS_PATH,
GN.MonitorEpsTensor: GN_INTERNAL_MONITORS_PATH, GN.MonitorPermittivity: GN_INTERNAL_MONITORS_PATH,
GN.MonitorDiffraction: GN_INTERNAL_MONITORS_PATH, GN.MonitorDiffraction: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjCartEHField: GN_INTERNAL_MONITORS_PATH, GN.MonitorProjCartEHField: GN_INTERNAL_MONITORS_PATH,
GN.MonitorProjAngEHField: GN_INTERNAL_MONITORS_PATH, GN.MonitorProjAngEHField: GN_INTERNAL_MONITORS_PATH,

Binary file not shown.

View File

@ -61,6 +61,7 @@ from .sim_types import (
BoundCondType, BoundCondType,
NewSimCloudTask, NewSimCloudTask,
SimAxisDir, SimAxisDir,
SimFieldPols,
SimSpaceAxis, SimSpaceAxis,
manual_amp_time, manual_amp_time,
) )
@ -104,6 +105,7 @@ __all__ = [
'BoundCondType', 'BoundCondType',
'NewSimCloudTask', 'NewSimCloudTask',
'SimAxisDir', 'SimAxisDir',
'SimFieldPols',
'SimSpaceAxis', 'SimSpaceAxis',
'manual_amp_time', 'manual_amp_time',
'NodeCategory', 'NodeCategory',

View File

@ -120,7 +120,7 @@ class FlowKind(enum.StrEnum):
FlowKind.Value: 'Value', FlowKind.Value: 'Value',
FlowKind.Array: 'Array', FlowKind.Array: 'Array',
FlowKind.LazyArrayRange: 'Range', FlowKind.LazyArrayRange: 'Range',
FlowKind.LazyValueFunc: 'Lazy Value', FlowKind.LazyValueFunc: 'Func',
FlowKind.Params: 'Parameters', FlowKind.Params: 'Parameters',
FlowKind.Info: 'Information', FlowKind.Info: 'Information',
}[v] }[v]

View File

@ -169,6 +169,52 @@ class SimAxisDir(enum.StrEnum):
return {SAD.Plus: True, SAD.Minus: False}[self] return {SAD.Plus: True, SAD.Minus: False}[self]
####################
# - Simulation Fields
####################
class SimFieldPols(enum.StrEnum):
"""Positive or negative direction along an injection axis."""
Ex = 'Ex'
Ey = 'Ey'
Ez = 'Ez'
Hx = 'Hx'
Hy = 'Hy'
Hz = 'Hz'
@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.
"""
SFP = SimFieldPols
return {
SFP.Ex: 'Ex',
SFP.Ey: 'Ey',
SFP.Ez: 'Ez',
SFP.Hx: 'Hx',
SFP.Hy: 'Hy',
SFP.Hz: 'Hz',
}[v]
@staticmethod
def to_icon(_: typ.Self) -> str:
"""Convert the enum value to a Blender icon.
Notes:
Used to print icons in `EnumProperty`s based on this enum.
Returns:
A human-friendly name corresponding to the enum value.
"""
return ''
#################### ####################
# - Boundary Condition Type # - Boundary Condition Type
#################### ####################

View File

@ -25,14 +25,28 @@ log = logger.get(__name__)
class ManagedObj(abc.ABC): class ManagedObj(abc.ABC):
"""A weak name-based reference to some kind of object external to this software.
While the object doesn't have to come from Blender's `bpy.types`, that is admittedly the driving motivation for this class: To encapsulate access to the powerful visual tools granted by Blender's 3D viewport, image editor, and UI.
Through extensive testing, the functionality of an implicitly-cached, semi-strictly immediate-mode interface, demanding only a weakly-referenced name as persistance, has emerged (with all of the associated tradeoffs).
While not suited to all use cases, the `ManagedObj` paradigm is perfect for many situations where a node needs to "loosely own" something external and non-trivial.
Intriguingly, the precise definition of "loose" has grown to vary greatly between subclasses, as it ends of demonstrating itself to be a matter of taste more than determinism.
This abstract base class serves to provide a few of the most basic of commonly-available - especially the `dump_as_msgspec`/`parse_as_msgspec` methods that allow it to be persisted using `blender_maxwell.utils.serialize`.
Parameters:
managed_obj_type: Enum identifier indicating which of the `ct.ManagedObjType` the instance should declare itself as.
"""
managed_obj_type: ct.ManagedObjType managed_obj_type: ct.ManagedObjType
@abc.abstractmethod @abc.abstractmethod
def __init__( def __init__(self, name: ct.ManagedObjName, prev_name: str | None = None):
self, """Initializes the managed object with a unique name.
name: ct.ManagedObjName,
): Use `prev_name` to indicate that the managed object will initially be avaiable under `prev_name`, but that it should be renamed to `name`.
"""Initializes the managed object with a unique name.""" """
#################### ####################
# - Properties # - Properties
@ -60,7 +74,7 @@ class ManagedObj(abc.ABC):
@abc.abstractmethod @abc.abstractmethod
def hide_preview(self) -> None: def hide_preview(self) -> None:
"""Select the managed object in Blender, if such an operation makes sense.""" """Hide any active preview of the managed object, if it exists, and if such an operation makes sense."""
#################### ####################
# - Serialization # - Serialization

View File

@ -45,82 +45,57 @@ class ManagedBLMesh(base.ManagedObj):
@name.setter @name.setter
def name(self, value: str) -> None: def name(self, value: str) -> None:
log.info( log.debug(
'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 self._bl_object_name == value: existing_bl_object = bpy.data.objects.get(self.name)
## TODO: This is a workaround.
## Really, we can't tell if a name is valid by searching objects.
## Since, after all, other managedobjs may have taken a name..
## ...but not yet made an object that has it.
return
if (bl_object := bpy.data.objects.get(value)) is None: # No Existing Object: Set Value to Name
log.info( if existing_bl_object is None:
'Desired BLMesh Name "%s" Not Taken',
value,
)
if self._bl_object_name is None:
log.info(
'Set New BLMesh Name to "%s"',
value,
)
elif (bl_object := bpy.data.objects.get(self._bl_object_name)) is not None:
log.info(
'Changed BLMesh Name to "%s"',
value,
)
bl_object.name = value
else:
msg = f'ManagedBLMesh with name "{self._bl_object_name}" was deleted'
raise RuntimeError(msg)
# Set Internal Name
self._bl_object_name = value self._bl_object_name = value
# Existing Object: Rename to New Name
else: else:
log.info( existing_bl_object.name = value
'Desired BLMesh Name "%s" is Taken. Using Blender Rename', self._bl_object_name = value
value,
)
# Set Name Anyway, but Respect Blender's Renaming # Check: Blender Rename -> Synchronization Error
## When a name already exists, Blender adds .### to prevent overlap. ## -> We can't do much else than report to the user & free().
## `set_name` is allowed to change the name; nodes account for this. if existing_bl_object.name != self._bl_object_name:
bl_object.name = value log.critical(
self._bl_object_name = bl_object.name 'BLMesh: Failed to set name of %s to %s, as %s already exists.'
)
log.info( self._bl_object_name = existing_bl_object.name
'Changed BLMesh Name to "%s"', self.free()
bl_object.name,
)
#################### ####################
# - Allocation # - Allocation
#################### ####################
def __init__(self, name: str): def __init__(self, name: str, prev_name: str | None = None):
if prev_name is not None:
self._bl_object_name = prev_name
else:
self._bl_object_name = name
self.name = name self.name = name
#################### ####################
# - Deallocation # - Deallocation
#################### ####################
def free(self): def free(self):
if (bl_object := bpy.data.objects.get(self.name)) is None: bl_object = bpy.data.objects.get(self.name)
if bl_object is None:
return return
# Delete the Underlying Datablock # Delete the Mesh Datablock
## This automatically deletes the object too ## -> This automatically deletes the object too
log.info('Removing "%s" BLMesh', bl_object.type) log.info('BLMesh: Freeing "%s"', self.name)
bpy.data.meshes.remove(bl_object.data) bpy.data.meshes.remove(bl_object.data)
#################### ####################
# - Methods # - Methods
#################### ####################
@property
def exists(self) -> bool:
return bpy.data.objects.get(self.name) is not None
def show_preview(self) -> None: def show_preview(self) -> None:
"""Moves the managed Blender object to the preview collection. """Moves the managed Blender object to the preview collection.
@ -128,7 +103,7 @@ class ManagedBLMesh(base.ManagedObj):
""" """
bl_object = bpy.data.objects.get(self.name) bl_object = bpy.data.objects.get(self.name)
if bl_object is None: if bl_object is None:
log.info('Created previewable ManagedBLMesh "%s"', bl_object.name) log.info('%s (ManagedBLMesh): Created BLObject for Preview', bl_object.name)
bl_object = self.bl_object() bl_object = self.bl_object()
if bl_object.name not in preview_collection().objects: if bl_object.name not in preview_collection().objects:
@ -136,24 +111,19 @@ class ManagedBLMesh(base.ManagedObj):
preview_collection().objects.link(bl_object) preview_collection().objects.link(bl_object)
def hide_preview(self) -> None: def hide_preview(self) -> None:
"""Removes the managed Blender object from the preview collection. """Hide any active preview of the managed object, if it exists, and if such an operation makes sense."""
If it's already removed, do nothing.
"""
bl_object = bpy.data.objects.get(self.name) bl_object = bpy.data.objects.get(self.name)
if bl_object is not None and bl_object.name in preview_collection().objects: if bl_object 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)
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.""" """Select the managed object in Blender, if it exists, and if such an operation makes sense."""
if (bl_object := bpy.data.objects.get(self.name)) is not None: bl_object = bpy.data.objects.get(self.name)
if bl_object is not None:
bpy.ops.object.select_all(action='DESELECT') bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True) bl_object.select_set(True)
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
#################### ####################
# - BLMesh Management # - BLMesh Management
#################### ####################

View File

@ -26,6 +26,7 @@ from blender_maxwell.utils import logger
from .. import bl_socket_map from .. import bl_socket_map
from .. import contracts as ct from .. import contracts as ct
from . import base from . import base
from .managed_bl_mesh import ManagedBLMesh
log = logger.get(__name__) log = logger.get(__name__)
@ -71,7 +72,7 @@ def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs:
return { return {
'node_group': bl_modifier.node_group, 'node_group': bl_modifier.node_group,
} }
elif bl_modifier.type == 'ARRAY': if bl_modifier.type == 'ARRAY':
raise NotImplementedError raise NotImplementedError
raise NotImplementedError raise NotImplementedError
@ -162,6 +163,7 @@ def write_modifier(
class ManagedBLModifier(base.ManagedObj): class ManagedBLModifier(base.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLModifier managed_obj_type = ct.ManagedObjType.ManagedBLModifier
_modifier_name: str | None = None _modifier_name: str | None = None
twin_bl_mesh: ManagedBLMesh | None = None
#################### ####################
# - BL Object Name # - BL Object Name
@ -172,94 +174,117 @@ class ManagedBLModifier(base.ManagedObj):
@name.setter @name.setter
def name(self, value: str) -> None: def name(self, value: str) -> None:
## TODO: Handle name conflict within same BLObject log.debug('Changing BLModifier w/Name "%s" to Name "%s"', self.name, value)
log.info(
'Changing BLModifier w/Name "%s" to Name "%s"', self._modifier_name, value twin_bl_object = bpy.data.objects.get(self.twin_bl_mesh.name)
)
self._modifier_name = value # No Existing Twin BLObject
## -> Since no modifier-holding object exists, we're all set.
if twin_bl_object is None:
self._modifier_name = value
# Existing Twin BLObject
else:
# No Existing Modifier: Set Value to Name
## -> We'll rename the bl_object; otherwise we're set.
bl_modifier = twin_bl_object.modifiers.get(self.name)
if bl_modifier is None:
self.twin_bl_mesh.name = value
self._modifier_name = value
# Existing Modifier: Rename to New Name
## -> We'll rename the bl_modifier, then the bl_object.
else:
bl_modifier.name = value
self.twin_bl_mesh.name = value
self._modifier_name = value
#################### ####################
# - Allocation # - Allocation
#################### ####################
def __init__(self, name: str): def __init__(self, name: str, prev_name: str | None = None):
self.twin_bl_mesh = ManagedBLMesh(name, prev_name=prev_name)
if prev_name is not None:
self._modifier_name = prev_name
else:
self._modifier_name = name
self.name = name self.name = name
def bl_select(self) -> None: def bl_select(self) -> None:
pass self.twin_bl_mesh.bl_select()
def show_preview(self) -> None:
self.twin_bl_mesh.show_preview()
def hide_preview(self) -> None: def hide_preview(self) -> None:
pass self.twin_bl_mesh.hide_preview()
#################### ####################
# - Deallocation # - Deallocation
#################### ####################
def free(self): def free(self):
"""Not needed - when the object is removed, its modifiers are also removed.""" log.info('BLModifier: Freeing "%s" w/Twin BLObject of same name', self.name)
self.twin_bl_mesh.free()
def free_from_bl_object(
self,
bl_object: bpy.types.Object,
) -> None:
"""Remove the managed BL modifier from the passed Blender object.
Parameters:
bl_object: The Blender object to remove the modifier from.
"""
if (bl_modifier := bl_object.modifiers.get(self.name)) is not None:
log.info(
'Removing (recreating) BLModifier "%s" on BLObject "%s" (existing modifier_type is "%s")',
bl_modifier.name,
bl_object.name,
bl_modifier.type,
)
bl_modifier = bl_object.modifiers.remove(bl_modifier)
else:
msg = f'Tried to free bl_modifier "{self.name}", but bl_object "{bl_object.name}" has no modifier of that name'
raise ValueError(msg)
#################### ####################
# - Modifiers # - Modifiers
#################### ####################
def bl_modifier( def bl_modifier(
self, self,
bl_object: bpy.types.Object,
modifier_type: ct.BLModifierType, modifier_type: ct.BLModifierType,
modifier_attrs: ModifierAttrs, modifier_attrs: ModifierAttrs,
location: tuple[float, float, float] = (0, 0, 0),
): ):
"""Creates a new modifier for the current `bl_object`. """Creates a new modifier for the current `bl_object`.
- Modifier Type Names: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items> - Modifier Type Names: <https://docs.blender.org/api/current/bpy_types_enum_items/object_modifier_type_items.html#rna-enum-object-modifier-type-items>
""" """
# Remove Mismatching Modifier # Retrieve Twin BLObject
twin_bl_object = self.twin_bl_mesh.bl_object(location=location)
if twin_bl_object is None:
msg = f'BLModifier: No BLObject twin "{self.name}" exists to attach a modifier to.'
raise ValueError(msg)
bl_modifier = twin_bl_object.modifiers.get(self.name)
# Existing Modifier: Maybe Remove
modifier_was_removed = False modifier_was_removed = False
if ( if bl_modifier is not None and bl_modifier.type != modifier_type:
bl_modifier := bl_object.modifiers.get(self.name)
) and bl_modifier.type != modifier_type:
log.info( log.info(
'Removing (recreating) BLModifier "%s" on BLObject "%s" (existing modifier_type is "%s", but "%s" is requested)', 'BLModifier: Clearing BLModifier "%s" from BLObject "%s"',
bl_modifier.name, self.name,
bl_object.name, twin_bl_object.name,
bl_modifier.type,
modifier_type,
) )
self.free_from_bl_object(bl_object) twin_bl_object.modifiers.remove(bl_modifier)
modifier_was_removed = True modifier_was_removed = True
# Create Modifier # No/Removed Modifier: Create
if bl_modifier is None or modifier_was_removed: if bl_modifier is None or modifier_was_removed:
log.info( log.info(
'Creating BLModifier "%s" on BLObject "%s" with modifier_type "%s"', 'BLModifier: (Re)Creating BLModifier "%s" on BLObject "%s" (type=%s)',
self.name, self.name,
bl_object.name, twin_bl_object.name,
modifier_type, modifier_type,
) )
bl_modifier = bl_object.modifiers.new( bl_modifier = twin_bl_object.modifiers.new(
name=self.name, name=self.name,
type=modifier_type, type=modifier_type,
) )
# Write Modifier Attrs
## -> For GeoNodes modifiers, this is the critical component.
## -> From 'write_modifier', we only need to know if something changed.
## -> If so, we make sure to update the object data.
modifier_altered = write_modifier(bl_modifier, modifier_attrs) modifier_altered = write_modifier(bl_modifier, modifier_attrs)
if modifier_altered: if modifier_altered:
bl_object.data.update() twin_bl_object.data.update()
return bl_modifier return bl_modifier
####################
# - Mesh Data
####################
@property
def mesh_as_arrays(self) -> dict:
return self.twin_bl_mesh.mesh_as_arrays

View File

@ -131,27 +131,10 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
for i, (preset_name, preset_def) in enumerate(cls.presets.items()) for i, (preset_name, preset_def) in enumerate(cls.presets.items())
] ]
#################### # Managed Objects
# - Managed Objects managed_objs: dict[str, _managed_objs.ManagedObj] = bl_cache.BLField(
#################### {}, use_prop_update=False
@bl_cache.cached_bl_property(depends_on={'sim_node_name'}) )
def managed_objs(self) -> dict[str, _managed_objs.ManagedObj]:
"""Access the constructed managed objects defined in `self.managed_obj_types`.
Managed objects are special in that they **don't keep any non-reproducible state**.
In fact, all managed object state can generally be derived entirely from the managed object's `name` attribute.
As a result, **consistency in namespacing is of the utmost importance**, if reproducibility of managed objects is to be guaranteed.
This name must be in sync with the name of the managed "thing", which is where this computed property comes in.
The node's half of the responsibility is to push a new name whenever `self.sim_node_name` changes.
"""
if self.managed_obj_types:
return {
mobj_name: mobj_type(self.sim_node_name)
for mobj_name, mobj_type in self.managed_obj_types.items()
}
return {}
#################### ####################
# - Class Methods # - Class Methods
@ -221,25 +204,30 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
#################### ####################
@events.on_value_changed( @events.on_value_changed(
prop_name='sim_node_name', prop_name='sim_node_name',
props={'sim_node_name', 'managed_objs'}, props={'sim_node_name', 'managed_objs', 'managed_obj_types'},
stop_propagation=True, stop_propagation=True,
) )
def _on_sim_node_name_changed(self, props): def _on_sim_node_name_changed(self, props):
log.info( log.debug(
'Changed Sim Node Name of a "%s" to "%s" (self=%s)', 'Changed Sim Node Name of a "%s" to "%s" (self=%s)',
self.bl_idname, self.bl_idname,
props['sim_node_name'], props['sim_node_name'],
str(self), str(self),
) )
# Set Name of Managed Objects # (Re)Construct Managed Objects
for mobj in props['managed_objs'].values(): ## -> Due to 'prev_name', the new MObjs will be renamed on construction
mobj.name = props['sim_node_name'] self.managed_objs = {
mobj_name: mobj_type(
## Invalidate Cache self.sim_node_name,
## -> Persistance doesn't happen if we simply mutate. prev_name=(
## -> This ensures that the name change is picked up. props['managed_objs'][mobj_name].name
self.managed_objs = bl_cache.Signal.InvalidateCache if mobj_name in props['managed_objs']
else None
),
)
for mobj_name, mobj_type in props['managed_obj_types'].items()
}
@events.on_value_changed(prop_name='active_socket_set') @events.on_value_changed(prop_name='active_socket_set')
def _on_socket_set_changed(self): def _on_socket_set_changed(self):
@ -1027,12 +1015,14 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
bl_socket.reset_instance_id() bl_socket.reset_instance_id()
# Generate New Sim Node Name # Generate New Sim Node Name
## Blender will automatically add .001 so that `self.name` is unique. ## -> Blender will adds .00# so that `self.name` is unique.
## -> We can shamelessly piggyback on this for unique managed objs.
## -> ...But to avoid stealing the old node's mobjs, we first rename.
self.sim_node_name = self.name self.sim_node_name = self.name
# Event Methods # Event Methods
## Run any 'DataChanged' methods with 'run_on_init' set. ## -> Re-run any 'DataChanged' methods with 'run_on_init' set.
## -> Copying a node _arguably_ re-initializes the new node. ## -> Copying a node ~ re-initializing the new node.
for event_method in [ for event_method in [
event_method event_method
for event_method in self.event_methods_by_event[ct.FlowEvent.DataChanged] for event_method in self.event_methods_by_event[ct.FlowEvent.DataChanged]

View File

@ -132,7 +132,7 @@ class BlochBoundCondNode(base.MaxwellSimNode):
#################### ####################
# - Properties # - Properties
#################### ####################
valid_sim_axis: ct.SimSpaceAxis = bl_cache.BLField(ct.SimSpaceAxis.X, prop_ui=True) valid_sim_axis: ct.SimSpaceAxis = bl_cache.BLField(ct.SimSpaceAxis.X)
#################### ####################
# - UI # - UI

View File

@ -24,13 +24,15 @@ import tidy3d as td
from tidy3d.material_library.material_library import MaterialItem as Tidy3DMediumItem from tidy3d.material_library.material_library import MaterialItem as Tidy3DMediumItem
from tidy3d.material_library.material_library import VariantItem as Tidy3DMediumVariant from tidy3d.material_library.material_library import VariantItem as Tidy3DMediumVariant
from blender_maxwell.utils import bl_cache, sci_constants from blender_maxwell.utils import bl_cache, logger, sci_constants
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
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__)
_mat_lib_iter = iter(td.material_library) _mat_lib_iter = iter(td.material_library)
_mat_key = '' _mat_key = ''
@ -131,7 +133,8 @@ class LibraryMediumNode(base.MaxwellSimNode):
#################### ####################
vendored_medium: VendoredMedium = bl_cache.BLField(VendoredMedium.Au) vendored_medium: VendoredMedium = bl_cache.BLField(VendoredMedium.Au)
variant_name: enum.StrEnum = bl_cache.BLField( variant_name: enum.StrEnum = bl_cache.BLField(
enum_cb=lambda self, _: self.search_variants() enum_cb=lambda self, _: self.search_variants(),
cb_depends_on={'vendored_medium'},
) )
def search_variants(self) -> list[ct.BLEnumElement]: def search_variants(self) -> list[ct.BLEnumElement]:
@ -141,7 +144,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
#################### ####################
# - Computed # - Computed
#################### ####################
@bl_cache.cached_bl_property(depends_on={'vendored_medium', 'variant_name'}) @bl_cache.cached_bl_property(depends_on={'variant_name'})
def variant(self) -> Tidy3DMediumVariant: def variant(self) -> Tidy3DMediumVariant:
"""Deduce the actual medium variant from `self.vendored_medium` and `self.variant_name`.""" """Deduce the actual medium variant from `self.vendored_medium` and `self.variant_name`."""
return self.vendored_medium.medium_variants[self.variant_name] return self.vendored_medium.medium_variants[self.variant_name]
@ -239,21 +242,6 @@ class LibraryMediumNode(base.MaxwellSimNode):
if self.data_url is not None: if self.data_url is not None:
box.operator('wm.url_open', text='Link to Data').url = self.data_url box.operator('wm.url_open', text='Link to Data').url = self.data_url
####################
# - Events
####################
@events.on_value_changed(
prop_name={'vendored_medium', 'variant_name'},
run_on_init=True,
props={'vendored_medium'},
)
def on_medium_changed(self, props):
if self.variant_name not in props['vendored_medium'].medium_variants:
self.variant_name = bl_cache.Signal.ResetEnumItems
self.ui_freq_range = bl_cache.Signal.InvalidateCache
self.ui_wl_range = bl_cache.Signal.InvalidateCache
#################### ####################
# - Output # - Output
#################### ####################
@ -264,13 +252,6 @@ class LibraryMediumNode(base.MaxwellSimNode):
def compute_medium(self, props) -> sp.Expr: def compute_medium(self, props) -> sp.Expr:
return props['medium'] return props['medium']
@events.computes_output_socket(
'Valid Freqs',
props={'freq_range'},
)
def compute_valid_freqs(self, props) -> sp.Expr:
return props['freq_range']
@events.computes_output_socket( @events.computes_output_socket(
'Valid Freqs', 'Valid Freqs',
kind=ct.FlowKind.LazyArrayRange, kind=ct.FlowKind.LazyArrayRange,
@ -285,13 +266,6 @@ class LibraryMediumNode(base.MaxwellSimNode):
unit=spux.THz, unit=spux.THz,
) )
@events.computes_output_socket(
'Valid WLs',
props={'wl_range'},
)
def compute_valid_wls(self, props) -> sp.Expr:
return props['wl_range']
@events.computes_output_socket( @events.computes_output_socket(
'Valid WLs', 'Valid WLs',
kind=ct.FlowKind.LazyArrayRange, kind=ct.FlowKind.LazyArrayRange,
@ -320,7 +294,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
props, props,
): ):
managed_objs['plot'].mpl_plot_to_image( managed_objs['plot'].mpl_plot_to_image(
lambda ax: self.medium.plot(props['medium'].frequency_range, ax=ax), lambda ax: props['medium'].plot(props['medium'].frequency_range, ax=ax),
bl_select=True, bl_select=True,
) )
## TODO: Plot based on Wl, not freq. ## TODO: Plot based on Wl, not freq.

View File

@ -14,20 +14,19 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from . import eh_field_monitor, field_power_flux_monitor from . import eh_field_monitor, field_power_flux_monitor, permittivity_monitor
# from . import epsilon_tensor_monitor
# from . import diffraction_monitor # from . import diffraction_monitor
BL_REGISTER = [ BL_REGISTER = [
*eh_field_monitor.BL_REGISTER, *eh_field_monitor.BL_REGISTER,
*field_power_flux_monitor.BL_REGISTER, *field_power_flux_monitor.BL_REGISTER,
# *epsilon_tensor_monitor.BL_REGISTER, *permittivity_monitor.BL_REGISTER,
# *diffraction_monitor.BL_REGISTER, # *diffraction_monitor.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**eh_field_monitor.BL_NODES, **eh_field_monitor.BL_NODES,
**field_power_flux_monitor.BL_NODES, **field_power_flux_monitor.BL_NODES,
# **epsilon_tensor_monitor.BL_NODES, **permittivity_monitor.BL_NODES,
# **diffraction_monitor.BL_NODES, # **diffraction_monitor.BL_NODES,
} }

View File

@ -16,13 +16,14 @@
import typing as typ import typing as typ
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 blender_maxwell.assets.geonodes import GeoNodes, import_geonodes from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
@ -50,11 +51,13 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
size=spux.NumberSize1D.Vec3, size=spux.NumberSize1D.Vec3,
physical_type=spux.PhysicalType.Length, physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]), default_value=sp.Matrix([1, 1, 1]),
abs_min=0,
), ),
'Spatial Subdivs': sockets.ExprSocketDef( 'Stride': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3, size=spux.NumberSize1D.Vec3,
mathtype=spux.MathType.Integer, mathtype=spux.MathType.Integer,
default_value=sp.Matrix([10, 10, 10]), default_value=sp.Matrix([10, 10, 10]),
abs_min=0,
), ),
} }
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
@ -69,15 +72,15 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
), ),
}, },
'Time Domain': { 'Time Domain': {
'Time Range': sockets.ExprSocketDef( 't Range': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange, active_kind=ct.FlowKind.LazyArrayRange,
physical_type=spux.PhysicalType.Time, physical_type=spux.PhysicalType.Time,
default_unit=spu.picosecond, default_unit=spu.picosecond,
default_min=0, default_min=0,
default_max=10, default_max=10,
default_steps=2, default_steps=0,
), ),
'Temporal Subdivs': sockets.ExprSocketDef( 't Stride': sockets.ExprSocketDef(
mathtype=spux.MathType.Integer, mathtype=spux.MathType.Integer,
default_value=100, default_value=100,
), ),
@ -89,20 +92,30 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
} }
managed_obj_types: typ.ClassVar = { managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier, 'modifier': managed_objs.ManagedBLModifier,
} }
####################
# - Properties
####################
fields: set[ct.SimFieldPols] = bl_cache.BLField(set(ct.SimFieldPols))
####################
# - UI
####################
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
layout.prop(self, self.blfields['fields'], expand=True)
#################### ####################
# - Output # - Output
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Freq Monitor', 'Freq Monitor',
props={'sim_node_name'}, props={'sim_node_name', 'fields'},
input_sockets={ input_sockets={
'Center', 'Center',
'Size', 'Size',
'Spatial Subdivs', 'Stride',
'Freqs', 'Freqs',
}, },
input_socket_kinds={ input_socket_kinds={
@ -131,28 +144,29 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
center=input_sockets['Center'], center=input_sockets['Center'],
size=input_sockets['Size'], size=input_sockets['Size'],
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=tuple(input_sockets['Spatial Subdivs']), interval_space=tuple(input_sockets['Stride']),
freqs=input_sockets['Freqs'].realize().values, freqs=input_sockets['Freqs'].realize().values,
fields=props['fields'],
) )
@events.computes_output_socket( @events.computes_output_socket(
'Time Monitor', 'Time Monitor',
props={'sim_node_name'}, props={'sim_node_name', 'fields'},
input_sockets={ input_sockets={
'Center', 'Center',
'Size', 'Size',
'Spatial Subdivs', 'Stride',
'Time Range', 't Range',
'Temporal Subdivs', 't Stride',
}, },
input_socket_kinds={ input_socket_kinds={
'Time Range': ct.FlowKind.LazyArrayRange, 't Range': ct.FlowKind.LazyArrayRange,
}, },
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D}, unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={ scale_input_sockets={
'Center': 'Tidy3DUnits', 'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits', 'Size': 'Tidy3DUnits',
'Time Range': 'Tidy3DUnits', 't Range': 'Tidy3DUnits',
}, },
) )
def compute_time_monitor( def compute_time_monitor(
@ -171,10 +185,11 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
center=input_sockets['Center'], center=input_sockets['Center'],
size=input_sockets['Size'], size=input_sockets['Size'],
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=tuple(input_sockets['Spatial Subdivs']), interval_space=tuple(input_sockets['Stride']),
start=input_sockets['Time Range'].realize_start(), start=input_sockets['t Range'].realize_start(),
stop=input_sockets['Time Range'].realize_stop(), stop=input_sockets['t Range'].realize_stop(),
interval=input_sockets['Temporal Subdivs'], interval=input_sockets['t Stride'],
fields=props['fields'],
) )
#################### ####################
@ -184,26 +199,21 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
# Trigger # Trigger
prop_name='preview_active', prop_name='preview_active',
# Loaded # Loaded
managed_objs={'mesh'}, managed_objs={'modifier'},
props={'preview_active'}, props={'preview_active'},
input_sockets={'Center', 'Size'},
) )
def on_preview_changed(self, managed_objs, props, input_sockets): 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']: if props['preview_active']:
mesh.show_preview() managed_objs['modifier'].show_preview()
else: else:
mesh.hide_preview() managed_objs['modifier'].hide_preview()
@events.on_value_changed( @events.on_value_changed(
# Trigger # Trigger
socket_name={'Center', 'Size'}, socket_name={'Center', 'Size'},
run_on_init=True, run_on_init=True,
# Loaded # Loaded
managed_objs={'mesh', 'modifier'}, managed_objs={'modifier'},
input_sockets={'Center', 'Size'}, input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={
@ -218,7 +228,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
): ):
# Push Input Values to GeoNodes Modifier # Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier( managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES', 'NODES',
{ {
'node_group': import_geonodes(GeoNodes.MonitorEHField), 'node_group': import_geonodes(GeoNodes.MonitorEHField),
@ -227,6 +236,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'Size': input_sockets['Size'], 'Size': input_sockets['Size'],
}, },
}, },
location=input_sockets['Center'],
) )

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

@ -16,13 +16,14 @@
import typing as typ import typing as typ
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 blender_maxwell.assets.geonodes import GeoNodes, import_geonodes from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from ... import contracts as ct from ... import contracts as ct
from ... import managed_objs, sockets from ... import managed_objs, sockets
@ -32,6 +33,8 @@ log = logger.get(__name__)
class PowerFluxMonitorNode(base.MaxwellSimNode): class PowerFluxMonitorNode(base.MaxwellSimNode):
"""Node providing for the monitoring of electromagnetic field flux a given planar region or volume, in either the frequency or the time domain."""
node_type = ct.NodeType.PowerFluxMonitor node_type = ct.NodeType.PowerFluxMonitor
bl_label = 'Power Flux Monitor' bl_label = 'Power Flux Monitor'
use_sim_node_name = True use_sim_node_name = True
@ -48,13 +51,14 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
size=spux.NumberSize1D.Vec3, size=spux.NumberSize1D.Vec3,
physical_type=spux.PhysicalType.Length, physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]), default_value=sp.Matrix([1, 1, 1]),
abs_min=0,
), ),
'Samples/Space': sockets.ExprSocketDef( 'Stride': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3, size=spux.NumberSize1D.Vec3,
mathtype=spux.MathType.Integer, mathtype=spux.MathType.Integer,
default_value=sp.Matrix([10, 10, 10]), default_value=sp.Matrix([10, 10, 10]),
abs_min=0,
), ),
'Direction': sockets.BoolSocketDef(),
} }
input_socket_sets: typ.ClassVar = { input_socket_sets: typ.ClassVar = {
'Freq Domain': { 'Freq Domain': {
@ -68,7 +72,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
), ),
}, },
'Time Domain': { 'Time Domain': {
'Time Range': sockets.ExprSocketDef( 't Range': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange, active_kind=ct.FlowKind.LazyArrayRange,
physical_type=spux.PhysicalType.Time, physical_type=spux.PhysicalType.Time,
default_unit=spu.picosecond, default_unit=spu.picosecond,
@ -76,7 +80,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
default_max=10, default_max=10,
default_steps=2, default_steps=2,
), ),
'Samples/Time': sockets.ExprSocketDef( 't Stride': sockets.ExprSocketDef(
mathtype=spux.MathType.Integer, mathtype=spux.MathType.Integer,
default_value=100, default_value=100,
), ),
@ -92,18 +96,45 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
'modifier': managed_objs.ManagedBLModifier, 'modifier': managed_objs.ManagedBLModifier,
} }
####################
# - Properties
####################
direction_2d: ct.SimAxisDir = bl_cache.BLField(ct.SimAxisDir.Plus)
include_3d: set[ct.SimSpaceAxis] = bl_cache.BLField(set(ct.SimSpaceAxis))
include_3d_x: set[ct.SimAxisDir] = bl_cache.BLField(set(ct.SimAxisDir))
include_3d_y: set[ct.SimAxisDir] = bl_cache.BLField(set(ct.SimAxisDir))
include_3d_z: set[ct.SimAxisDir] = bl_cache.BLField(set(ct.SimAxisDir))
####################
# - UI
####################
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
# 2D Monitor
if 0 in self._compute_input('Size'):
layout.prop(self, self.blfields['direction_2d'], expand=True)
# 3D Monitor
else:
layout.prop(self, self.blfields['include_3d'], expand=True)
row = layout.row(align=False)
if ct.SimSpaceAxis.X in self.include_3d:
row.prop(self, self.blfields['include_3d_x'], expand=True)
if ct.SimSpaceAxis.Y in self.include_3d:
row.prop(self, self.blfields['include_3d_y'], expand=True)
if ct.SimSpaceAxis.Z in self.include_3d:
row.prop(self, self.blfields['include_3d_z'], expand=True)
#################### ####################
# - Event Methods: Computation # - Event Methods: Computation
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Freq Monitor', 'Freq Monitor',
props={'sim_node_name'}, props={'sim_node_name', 'direction_2d'},
input_sockets={ input_sockets={
'Center', 'Center',
'Size', 'Size',
'Samples/Space', 'Stride',
'Freqs', 'Freqs',
'Direction',
}, },
input_socket_kinds={ input_socket_kinds={
'Freqs': ct.FlowKind.LazyArrayRange, 'Freqs': ct.FlowKind.LazyArrayRange,
@ -133,7 +164,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
name=props['sim_node_name'], name=props['sim_node_name'],
interval_space=(1, 1, 1), interval_space=(1, 1, 1),
freqs=input_sockets['Freqs'].realize_array.values, freqs=input_sockets['Freqs'].realize_array.values,
normal_dir='+' if input_sockets['Direction'] else '-', normal_dir=props['direction_2d'].plus_or_minus,
) )
#################### ####################
@ -143,49 +174,42 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
# Trigger # Trigger
prop_name='preview_active', prop_name='preview_active',
# Loaded # Loaded
managed_objs={'mesh'}, managed_objs={'modifier'},
props={'preview_active'}, props={'preview_active'},
) )
def on_preview_changed(self, managed_objs, props): 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']: if props['preview_active']:
mesh.show_preview() managed_objs['modifier'].show_preview()
else: else:
mesh.hide_preview() managed_objs['modifier'].hide_preview()
@events.on_value_changed( @events.on_value_changed(
# Trigger # Trigger
socket_name={'Center', 'Size', 'Direction'}, socket_name={'Center', 'Size'},
prop_name={'direction_2d'},
run_on_init=True, run_on_init=True,
# Loaded # Loaded
managed_objs={'mesh', 'modifier'}, managed_objs={'mesh', 'modifier'},
input_sockets={'Center', 'Size', 'Direction'}, props={'direction_2d'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER}, unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={ scale_input_sockets={
'Center': 'BlenderUnits', 'Center': 'BlenderUnits',
}, },
) )
def on_inputs_changed( def on_inputs_changed(self, managed_objs, props, input_sockets, unit_systems):
self,
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):
# Push Input Values to GeoNodes Modifier # Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier( managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES', 'NODES',
{ {
'node_group': import_geonodes(GeoNodes.MonitorPowerFlux), 'node_group': import_geonodes(GeoNodes.MonitorPowerFlux),
'unit_system': unit_systems['BlenderUnits'], 'unit_system': unit_systems['BlenderUnits'],
'inputs': { 'inputs': {
'Size': input_sockets['Size'], 'Size': input_sockets['Size'],
'Direction': input_sockets['Direction'], 'Direction': props['direction_2d'].true_or_false,
}, },
}, },
location=input_sockets['Center'],
) )

View File

@ -0,0 +1,173 @@
# blender_maxwell
# Copyright (C) 2024 blender_maxwell Project Contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import typing as typ
import sympy as sp
import tidy3d as td
from blender_maxwell.assets.geonodes import GeoNodes, import_geonodes
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
log = logger.get(__name__)
class PermittivityMonitorNode(base.MaxwellSimNode):
"""Provides a bounded 1D/2D/3D recording region for the diagonal of the complex-valued permittivity tensor."""
node_type = ct.NodeType.PermittivityMonitor
bl_label = 'Permittivity Monitor'
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets: typ.ClassVar = {
'Center': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3,
physical_type=spux.PhysicalType.Length,
),
'Size': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3,
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
abs_min=0,
),
'Stride': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3,
mathtype=spux.MathType.Integer,
default_value=sp.Matrix([10, 10, 10]),
abs_min=0,
),
'Freqs': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange,
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_min=374.7406, ## 800nm
default_max=1498.962, ## 200nm
default_steps=100,
),
}
output_sockets: typ.ClassVar = {
'Permittivity Monitor': sockets.MaxwellMonitorSocketDef()
}
managed_obj_types: typ.ClassVar = {
'modifier': managed_objs.ManagedBLModifier,
}
####################
# - Output
####################
@events.computes_output_socket(
'Permittivity Monitor',
props={'sim_node_name'},
input_sockets={
'Center',
'Size',
'Stride',
'Freqs',
},
input_socket_kinds={
'Freqs': ct.FlowKind.LazyArrayRange,
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
'Freqs': 'Tidy3DUnits',
},
)
def compute_permittivity_monitor(
self,
input_sockets: dict,
props: dict,
unit_systems: dict,
) -> td.FieldMonitor:
log.info(
'Computing PermittivityMonitor (name="%s") with center="%s", size="%s"',
props['sim_node_name'],
input_sockets['Center'],
input_sockets['Size'],
)
return td.PermittivityMonitor(
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
interval_space=tuple(input_sockets['Stride']),
freqs=input_sockets['Freqs'].realize().values,
)
####################
# - Preview
####################
@events.on_value_changed(
# Trigger
prop_name='preview_active',
# Loaded
managed_objs={'modifier'},
props={'preview_active'},
)
def on_preview_changed(self, managed_objs, props):
if props['preview_active']:
managed_objs['modifier'].show_preview()
else:
managed_objs['modifier'].hide_preview()
@events.on_value_changed(
# Trigger
socket_name={'Center', 'Size'},
run_on_init=True,
# Loaded
managed_objs={'modifier'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_inputs_changed(
self,
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
'NODES',
{
'node_group': import_geonodes(GeoNodes.MonitorPermittivity),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
location=input_sockets['Center'],
)
####################
# - Blender Registration
####################
BL_REGISTER = [
PermittivityMonitorNode,
]
BL_NODES = {ct.NodeType.PermittivityMonitor: (ct.NodeCategory.MAXWELLSIM_MONITORS)}

View File

@ -16,6 +16,8 @@
import bpy import bpy
from blender_maxwell.utils import bl_cache, logger
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
@ -30,19 +32,13 @@ class BoolBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Properties # - Properties
#################### ####################
raw_value: bpy.props.BoolProperty( raw_value: bool = bl_cache.BLField(False)
name='Boolean',
description='Represents a boolean value',
default=False,
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
#################### ####################
# - Socket UI # - Socket UI
#################### ####################
def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None: def draw_label_row(self, label_col_row: bpy.types.UILayout, text: str) -> None:
label_col_row.label(text=text) label_col_row.prop(self, self.blfields['raw_value'], text=text, toggle=True)
label_col_row.prop(self, 'raw_value', text='')
#################### ####################
# - Computation of Default Value # - Computation of Default Value

View File

@ -22,7 +22,6 @@ import typing as typ
import bpy import bpy
import pydantic as pyd import pydantic as pyd
import sympy as sp import sympy as sp
import sympy.physics.units as spu
from blender_maxwell.utils import bl_cache, logger from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
@ -126,7 +125,6 @@ class ExprBLSocket(base.MaxwellSimSocket):
active_unit: enum.StrEnum = bl_cache.BLField( active_unit: enum.StrEnum = bl_cache.BLField(
enum_cb=lambda self, _: self.search_valid_units(), enum_cb=lambda self, _: self.search_valid_units(),
use_prop_update=False,
cb_depends_on={'physical_type'}, cb_depends_on={'physical_type'},
) )

View File

@ -19,11 +19,14 @@ import scipy as sc
import sympy.physics.units as spu import sympy.physics.units as spu
import tidy3d as td import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct from ... import contracts as ct
from .. import base from .. import base
log = logger.get(__name__)
VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second
FIXED_WL = 500 * spu.nm FIXED_WL = 500 * spu.nm
@ -36,16 +39,7 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Properties # - Properties
#################### ####################
rel_permittivity: bpy.props.FloatVectorProperty( rel_permittivity: tuple[float, float] = bl_cache.BLField((1.0, 0.0), float_prec=2)
name='Relative Permittivity',
description='Represents a simple, complex permittivity',
size=2,
default=(1.0, 0.0),
precision=2,
update=(
lambda self, context: self.on_prop_changed('rel_permittivity', context)
),
)
#################### ####################
# - FlowKinds # - FlowKinds
@ -83,7 +77,7 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
col.label(text='ϵ_r ()') col.label(text='ϵ_r ()')
col = split.column(align=True) col = split.column(align=True)
col.prop(self, 'rel_permittivity', text='') col.prop(self, self.blfields['rel_permittivity'], text='')
#################### ####################

View File

@ -649,7 +649,8 @@ class BLPropType(enum.StrEnum):
case BPT.SingleEnum if isinstance(raw_value, str): case BPT.SingleEnum if isinstance(raw_value, str):
return obj_type(raw_value) return obj_type(raw_value)
case BPT.SetEnum if isinstance(raw_value, set): case BPT.SetEnum if isinstance(raw_value, set):
return {obj_type(v) for v in raw_value} SubStrEnum = typ.get_args(obj_type)[0]
return {SubStrEnum(v) for v in raw_value}
## Dynamic Enums: Nothing to coerce to. ## Dynamic Enums: Nothing to coerce to.
## -> The critical distinction is that dynamic enums can't be coerced beyond str. ## -> The critical distinction is that dynamic enums can't be coerced beyond str.

View File

@ -154,7 +154,6 @@ class CachedBLProperty:
if self.persist and not self.suppress_write.get( if self.persist and not self.suppress_write.get(
bl_instance.instance_id bl_instance.instance_id
): ):
self.suppress_next_write(bl_instance)
self.bl_prop.write(bl_instance, self.getter_method(bl_instance)) self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
else: else: