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
MonitorEHField = '_monitor_eh_field'
MonitorPowerFlux = '_monitor_power_flux'
MonitorEpsTensor = '_monitor_eps_tensor'
MonitorPermittivity = '_monitor_permittivity'
MonitorDiffraction = '_monitor_diffraction'
MonitorProjCartEHField = '_monitor_proj_eh_field'
MonitorProjAngEHField = '_monitor_proj_ang_eh_field'
@ -186,7 +186,7 @@ class GeoNodes(enum.StrEnum):
## Monitor
GN.MonitorEHField: 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.MonitorProjCartEHField: 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,
NewSimCloudTask,
SimAxisDir,
SimFieldPols,
SimSpaceAxis,
manual_amp_time,
)
@ -104,6 +105,7 @@ __all__ = [
'BoundCondType',
'NewSimCloudTask',
'SimAxisDir',
'SimFieldPols',
'SimSpaceAxis',
'manual_amp_time',
'NodeCategory',

View File

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

View File

@ -169,6 +169,52 @@ class SimAxisDir(enum.StrEnum):
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
####################

View File

@ -25,14 +25,28 @@ log = logger.get(__name__)
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
@abc.abstractmethod
def __init__(
self,
name: ct.ManagedObjName,
):
"""Initializes the managed object with a unique name."""
def __init__(self, name: ct.ManagedObjName, prev_name: str | None = None):
"""Initializes the managed object with a unique name.
Use `prev_name` to indicate that the managed object will initially be avaiable under `prev_name`, but that it should be renamed to `name`.
"""
####################
# - Properties
@ -60,7 +74,7 @@ class ManagedObj(abc.ABC):
@abc.abstractmethod
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

View File

@ -45,82 +45,57 @@ class ManagedBLMesh(base.ManagedObj):
@name.setter
def name(self, value: str) -> None:
log.info(
log.debug(
'Changing BLMesh w/Name "%s" to Name "%s"', self._bl_object_name, value
)
if self._bl_object_name == value:
## 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
existing_bl_object = bpy.data.objects.get(self.name)
if (bl_object := bpy.data.objects.get(value)) is None:
log.info(
'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
# No Existing Object: Set Value to Name
if existing_bl_object is None:
self._bl_object_name = value
# Existing Object: Rename to New Name
else:
log.info(
'Desired BLMesh Name "%s" is Taken. Using Blender Rename',
value,
)
existing_bl_object.name = value
self._bl_object_name = value
# Set Name Anyway, but Respect Blender's Renaming
## When a name already exists, Blender adds .### to prevent overlap.
## `set_name` is allowed to change the name; nodes account for this.
bl_object.name = value
self._bl_object_name = bl_object.name
log.info(
'Changed BLMesh Name to "%s"',
bl_object.name,
# Check: Blender Rename -> Synchronization Error
## -> We can't do much else than report to the user & free().
if existing_bl_object.name != self._bl_object_name:
log.critical(
'BLMesh: Failed to set name of %s to %s, as %s already exists.'
)
self._bl_object_name = existing_bl_object.name
self.free()
####################
# - 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
####################
# - Deallocation
####################
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
# Delete the Underlying Datablock
## This automatically deletes the object too
log.info('Removing "%s" BLMesh', bl_object.type)
# Delete the Mesh Datablock
## -> This automatically deletes the object too
log.info('BLMesh: Freeing "%s"', self.name)
bpy.data.meshes.remove(bl_object.data)
####################
# - Methods
####################
@property
def exists(self) -> bool:
return bpy.data.objects.get(self.name) is not None
def show_preview(self) -> None:
"""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)
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()
if bl_object.name not in preview_collection().objects:
@ -136,24 +111,19 @@ class ManagedBLMesh(base.ManagedObj):
preview_collection().objects.link(bl_object)
def hide_preview(self) -> None:
"""Removes the managed Blender object from the preview collection.
If it's already removed, do nothing.
"""
"""Hide any active preview of the managed object, if it exists, and if such an operation makes sense."""
bl_object = bpy.data.objects.get(self.name)
if bl_object is not None and bl_object.name in preview_collection().objects:
log.info('Removing "%s" from Preview Collection', bl_object.name)
preview_collection().objects.unlink(bl_object)
def bl_select(self) -> None:
"""Selects the managed Blender object, causing it to be ex. outlined in the 3D viewport."""
if (bl_object := bpy.data.objects.get(self.name)) is not None:
"""Select the managed object in Blender, if it exists, and if such an operation makes sense."""
bl_object = bpy.data.objects.get(self.name)
if bl_object is not None:
bpy.ops.object.select_all(action='DESELECT')
bl_object.select_set(True)
msg = 'Managed BLMesh does not exist'
raise ValueError(msg)
####################
# - BLMesh Management
####################

View File

@ -26,6 +26,7 @@ from blender_maxwell.utils import logger
from .. import bl_socket_map
from .. import contracts as ct
from . import base
from .managed_bl_mesh import ManagedBLMesh
log = logger.get(__name__)
@ -71,7 +72,7 @@ def read_modifier(bl_modifier: bpy.types.Modifier) -> ModifierAttrs:
return {
'node_group': bl_modifier.node_group,
}
elif bl_modifier.type == 'ARRAY':
if bl_modifier.type == 'ARRAY':
raise NotImplementedError
raise NotImplementedError
@ -162,6 +163,7 @@ def write_modifier(
class ManagedBLModifier(base.ManagedObj):
managed_obj_type = ct.ManagedObjType.ManagedBLModifier
_modifier_name: str | None = None
twin_bl_mesh: ManagedBLMesh | None = None
####################
# - BL Object Name
@ -172,94 +174,117 @@ class ManagedBLModifier(base.ManagedObj):
@name.setter
def name(self, value: str) -> None:
## TODO: Handle name conflict within same BLObject
log.info(
'Changing BLModifier w/Name "%s" to Name "%s"', self._modifier_name, value
)
log.debug('Changing BLModifier w/Name "%s" to Name "%s"', self.name, value)
twin_bl_object = bpy.data.objects.get(self.twin_bl_mesh.name)
# 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
####################
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
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:
pass
self.twin_bl_mesh.hide_preview()
####################
# - Deallocation
####################
def free(self):
"""Not needed - when the object is removed, its modifiers are also removed."""
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)
log.info('BLModifier: Freeing "%s" w/Twin BLObject of same name', self.name)
self.twin_bl_mesh.free()
####################
# - Modifiers
####################
def bl_modifier(
self,
bl_object: bpy.types.Object,
modifier_type: ct.BLModifierType,
modifier_attrs: ModifierAttrs,
location: tuple[float, float, float] = (0, 0, 0),
):
"""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>
"""
# 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
if (
bl_modifier := bl_object.modifiers.get(self.name)
) and bl_modifier.type != modifier_type:
if bl_modifier is not None and bl_modifier.type != modifier_type:
log.info(
'Removing (recreating) BLModifier "%s" on BLObject "%s" (existing modifier_type is "%s", but "%s" is requested)',
bl_modifier.name,
bl_object.name,
bl_modifier.type,
modifier_type,
'BLModifier: Clearing BLModifier "%s" from BLObject "%s"',
self.name,
twin_bl_object.name,
)
self.free_from_bl_object(bl_object)
twin_bl_object.modifiers.remove(bl_modifier)
modifier_was_removed = True
# Create Modifier
# No/Removed Modifier: Create
if bl_modifier is None or modifier_was_removed:
log.info(
'Creating BLModifier "%s" on BLObject "%s" with modifier_type "%s"',
'BLModifier: (Re)Creating BLModifier "%s" on BLObject "%s" (type=%s)',
self.name,
bl_object.name,
twin_bl_object.name,
modifier_type,
)
bl_modifier = bl_object.modifiers.new(
bl_modifier = twin_bl_object.modifiers.new(
name=self.name,
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)
if modifier_altered:
bl_object.data.update()
twin_bl_object.data.update()
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())
]
####################
# - Managed Objects
####################
@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 {}
# Managed Objects
managed_objs: dict[str, _managed_objs.ManagedObj] = bl_cache.BLField(
{}, use_prop_update=False
)
####################
# - Class Methods
@ -221,25 +204,30 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
####################
@events.on_value_changed(
prop_name='sim_node_name',
props={'sim_node_name', 'managed_objs'},
props={'sim_node_name', 'managed_objs', 'managed_obj_types'},
stop_propagation=True,
)
def _on_sim_node_name_changed(self, props):
log.info(
log.debug(
'Changed Sim Node Name of a "%s" to "%s" (self=%s)',
self.bl_idname,
props['sim_node_name'],
str(self),
)
# Set Name of Managed Objects
for mobj in props['managed_objs'].values():
mobj.name = props['sim_node_name']
## Invalidate Cache
## -> Persistance doesn't happen if we simply mutate.
## -> This ensures that the name change is picked up.
self.managed_objs = bl_cache.Signal.InvalidateCache
# (Re)Construct Managed Objects
## -> Due to 'prev_name', the new MObjs will be renamed on construction
self.managed_objs = {
mobj_name: mobj_type(
self.sim_node_name,
prev_name=(
props['managed_objs'][mobj_name].name
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')
def _on_socket_set_changed(self):
@ -1027,12 +1015,14 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
bl_socket.reset_instance_id()
# 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
# Event Methods
## Run any 'DataChanged' methods with 'run_on_init' set.
## -> Copying a node _arguably_ re-initializes the new node.
## -> Re-run any 'DataChanged' methods with 'run_on_init' set.
## -> Copying a node ~ re-initializing the new node.
for event_method in [
event_method
for event_method in self.event_methods_by_event[ct.FlowEvent.DataChanged]

View File

@ -132,7 +132,7 @@ class BlochBoundCondNode(base.MaxwellSimNode):
####################
# - 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

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 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 ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
log = logger.get(__name__)
_mat_lib_iter = iter(td.material_library)
_mat_key = ''
@ -131,7 +133,8 @@ class LibraryMediumNode(base.MaxwellSimNode):
####################
vendored_medium: VendoredMedium = bl_cache.BLField(VendoredMedium.Au)
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]:
@ -141,7 +144,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
####################
# - 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:
"""Deduce the actual medium variant from `self.vendored_medium` and `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:
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
####################
@ -264,13 +252,6 @@ class LibraryMediumNode(base.MaxwellSimNode):
def compute_medium(self, props) -> sp.Expr:
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(
'Valid Freqs',
kind=ct.FlowKind.LazyArrayRange,
@ -285,13 +266,6 @@ class LibraryMediumNode(base.MaxwellSimNode):
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(
'Valid WLs',
kind=ct.FlowKind.LazyArrayRange,
@ -320,7 +294,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
props,
):
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,
)
## 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
# 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
BL_REGISTER = [
*eh_field_monitor.BL_REGISTER,
*field_power_flux_monitor.BL_REGISTER,
# *epsilon_tensor_monitor.BL_REGISTER,
*permittivity_monitor.BL_REGISTER,
# *diffraction_monitor.BL_REGISTER,
]
BL_NODES = {
**eh_field_monitor.BL_NODES,
**field_power_flux_monitor.BL_NODES,
# **epsilon_tensor_monitor.BL_NODES,
**permittivity_monitor.BL_NODES,
# **diffraction_monitor.BL_NODES,
}

View File

@ -16,13 +16,14 @@
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
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 logger
from ... import contracts as ct
from ... import managed_objs, sockets
@ -50,11 +51,13 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
size=spux.NumberSize1D.Vec3,
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
abs_min=0,
),
'Spatial Subdivs': sockets.ExprSocketDef(
'Stride': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3,
mathtype=spux.MathType.Integer,
default_value=sp.Matrix([10, 10, 10]),
abs_min=0,
),
}
input_socket_sets: typ.ClassVar = {
@ -69,15 +72,15 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
),
},
'Time Domain': {
'Time Range': sockets.ExprSocketDef(
't Range': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange,
physical_type=spux.PhysicalType.Time,
default_unit=spu.picosecond,
default_min=0,
default_max=10,
default_steps=2,
default_steps=0,
),
'Temporal Subdivs': sockets.ExprSocketDef(
't Stride': sockets.ExprSocketDef(
mathtype=spux.MathType.Integer,
default_value=100,
),
@ -89,20 +92,30 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
}
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'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
####################
@events.computes_output_socket(
'Freq Monitor',
props={'sim_node_name'},
props={'sim_node_name', 'fields'},
input_sockets={
'Center',
'Size',
'Spatial Subdivs',
'Stride',
'Freqs',
},
input_socket_kinds={
@ -131,28 +144,29 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
interval_space=tuple(input_sockets['Spatial Subdivs']),
interval_space=tuple(input_sockets['Stride']),
freqs=input_sockets['Freqs'].realize().values,
fields=props['fields'],
)
@events.computes_output_socket(
'Time Monitor',
props={'sim_node_name'},
props={'sim_node_name', 'fields'},
input_sockets={
'Center',
'Size',
'Spatial Subdivs',
'Time Range',
'Temporal Subdivs',
'Stride',
't Range',
't Stride',
},
input_socket_kinds={
'Time Range': ct.FlowKind.LazyArrayRange,
't Range': ct.FlowKind.LazyArrayRange,
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Size': 'Tidy3DUnits',
'Time Range': 'Tidy3DUnits',
't Range': 'Tidy3DUnits',
},
)
def compute_time_monitor(
@ -171,10 +185,11 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
center=input_sockets['Center'],
size=input_sockets['Size'],
name=props['sim_node_name'],
interval_space=tuple(input_sockets['Spatial Subdivs']),
start=input_sockets['Time Range'].realize_start(),
stop=input_sockets['Time Range'].realize_stop(),
interval=input_sockets['Temporal Subdivs'],
interval_space=tuple(input_sockets['Stride']),
start=input_sockets['t Range'].realize_start(),
stop=input_sockets['t Range'].realize_stop(),
interval=input_sockets['t Stride'],
fields=props['fields'],
)
####################
@ -184,26 +199,21 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
# Trigger
prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
managed_objs={'modifier'},
props={'preview_active'},
input_sockets={'Center', 'Size'},
)
def on_preview_changed(self, managed_objs, props, input_sockets):
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
def on_preview_changed(self, managed_objs, props):
if props['preview_active']:
mesh.show_preview()
managed_objs['modifier'].show_preview()
else:
mesh.hide_preview()
managed_objs['modifier'].hide_preview()
@events.on_value_changed(
# Trigger
socket_name={'Center', 'Size'},
run_on_init=True,
# Loaded
managed_objs={'mesh', 'modifier'},
managed_objs={'modifier'},
input_sockets={'Center', 'Size'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
@ -218,7 +228,6 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.MonitorEHField),
@ -227,6 +236,7 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
'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 bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
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 logger
from ... import contracts as ct
from ... import managed_objs, sockets
@ -32,6 +33,8 @@ log = logger.get(__name__)
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
bl_label = 'Power Flux Monitor'
use_sim_node_name = True
@ -48,13 +51,14 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
size=spux.NumberSize1D.Vec3,
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([1, 1, 1]),
abs_min=0,
),
'Samples/Space': sockets.ExprSocketDef(
'Stride': sockets.ExprSocketDef(
size=spux.NumberSize1D.Vec3,
mathtype=spux.MathType.Integer,
default_value=sp.Matrix([10, 10, 10]),
abs_min=0,
),
'Direction': sockets.BoolSocketDef(),
}
input_socket_sets: typ.ClassVar = {
'Freq Domain': {
@ -68,7 +72,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
),
},
'Time Domain': {
'Time Range': sockets.ExprSocketDef(
't Range': sockets.ExprSocketDef(
active_kind=ct.FlowKind.LazyArrayRange,
physical_type=spux.PhysicalType.Time,
default_unit=spu.picosecond,
@ -76,7 +80,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
default_max=10,
default_steps=2,
),
'Samples/Time': sockets.ExprSocketDef(
't Stride': sockets.ExprSocketDef(
mathtype=spux.MathType.Integer,
default_value=100,
),
@ -92,18 +96,45 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
'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
####################
@events.computes_output_socket(
'Freq Monitor',
props={'sim_node_name'},
props={'sim_node_name', 'direction_2d'},
input_sockets={
'Center',
'Size',
'Samples/Space',
'Stride',
'Freqs',
'Direction',
},
input_socket_kinds={
'Freqs': ct.FlowKind.LazyArrayRange,
@ -133,7 +164,7 @@ class PowerFluxMonitorNode(base.MaxwellSimNode):
name=props['sim_node_name'],
interval_space=(1, 1, 1),
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
prop_name='preview_active',
# Loaded
managed_objs={'mesh'},
managed_objs={'modifier'},
props={'preview_active'},
)
def on_preview_changed(self, managed_objs, props):
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
managed_objs['modifier'].show_preview()
else:
mesh.hide_preview()
managed_objs['modifier'].hide_preview()
@events.on_value_changed(
# Trigger
socket_name={'Center', 'Size', 'Direction'},
socket_name={'Center', 'Size'},
prop_name={'direction_2d'},
run_on_init=True,
# Loaded
managed_objs={'mesh', 'modifier'},
input_sockets={'Center', 'Size', 'Direction'},
props={'direction_2d'},
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,
):
def on_inputs_changed(self, managed_objs, props, input_sockets, unit_systems):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
'NODES',
{
'node_group': import_geonodes(GeoNodes.MonitorPowerFlux),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'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
from blender_maxwell.utils import bl_cache, logger
from ... import contracts as ct
from .. import base
@ -30,19 +32,13 @@ class BoolBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
raw_value: bpy.props.BoolProperty(
name='Boolean',
description='Represents a boolean value',
default=False,
update=(lambda self, context: self.on_prop_changed('raw_value', context)),
)
raw_value: bool = bl_cache.BLField(False)
####################
# - Socket UI
####################
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, 'raw_value', text='')
label_col_row.prop(self, self.blfields['raw_value'], text=text, toggle=True)
####################
# - Computation of Default Value

View File

@ -22,7 +22,6 @@ import typing as typ
import bpy
import pydantic as pyd
import sympy as sp
import sympy.physics.units as spu
from blender_maxwell.utils import bl_cache, logger
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(
enum_cb=lambda self, _: self.search_valid_units(),
use_prop_update=False,
cb_depends_on={'physical_type'},
)

View File

@ -19,11 +19,14 @@ import scipy as sc
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
from blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct
from .. import base
log = logger.get(__name__)
VAC_SPEED_OF_LIGHT = sc.constants.speed_of_light * spu.meter / spu.second
FIXED_WL = 500 * spu.nm
@ -36,16 +39,7 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
####################
# - Properties
####################
rel_permittivity: bpy.props.FloatVectorProperty(
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)
),
)
rel_permittivity: tuple[float, float] = bl_cache.BLField((1.0, 0.0), float_prec=2)
####################
# - FlowKinds
@ -83,7 +77,7 @@ class MaxwellMediumBLSocket(base.MaxwellSimSocket):
col.label(text='ϵ_r ()')
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):
return obj_type(raw_value)
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.
## -> 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(
bl_instance.instance_id
):
self.suppress_next_write(bl_instance)
self.bl_prop.write(bl_instance, self.getter_method(bl_instance))
else: