feat: Feature-parity with past.

We've added enough nodes to run simulations, and there is now only an
"air gap" remaining between the math nodes and the sim design nodes.

Of particular importance right now are:
- Finalizing the exporter w/Sim Data output.
- Finishing the plane wave, as well as other key source nodes (TFSF,
  Gaussian Beam, Plane Wave stand out in particular).
- Reduce node, as we need to be able to reconstruct total flux
  through a field monitor, as well as generally do statistics.
- Transform node, particularly w/pure-InfoFlow reindexing (we need to be
  able to reindex frequency -> vacwl), fourier transform (we need to be
  able to cast a time-domain field recording to frequency domain).
- Finish debugging the basic structures, in particular the Cylinder
  primitive, but also the generic GeoNodes node, so we can build the
  membrane properly.
- Ah yes, also, the grid definition. The default can sometimes act a
  little suspiciously.
main
Sofus Albert Høgsbro Rose 2024-05-03 00:19:02 +02:00
parent 9df0d20c68
commit 695eedca98
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
33 changed files with 709 additions and 604 deletions

View File

@ -1,7 +1,7 @@
# Working TODO
- [x] Wave Constant
- Sources
- [ ] Temporal Shapes / Continuous Wave Temporal Shape
- [x] Temporal Shapes / Continuous Wave Temporal Shape
- [ ] Temporal Shapes / Symbolic Temporal Shape
- [ ] Plane Wave Source
- [ ] TFSF Source
@ -149,9 +149,10 @@
## Sources
- [x] Temporal Shapes / Gaussian Pulse Temporal Shape
- [x] Temporal Shapes / Continuous Wave Temporal Shape
- [ ] Merge Gaussian Pulse and Continuous Wave w/a socket set thing, since the I/O is effectively identical.
- [ ] Temporal Shapes / Expr Temporal Shape
- [ ] Specify a Sympy function / data to generate appropriate array based on
- [ ] Temporal Shapes / Data Temporal Shape
- [ ] Specify a Sympy function / data to generate envelope data.
- [ ] Merge with the above.
- [x] Point Dipole Source
- [ ] Use a viz mesh, not empty (empty doesn't play well with alpha hashing).

Binary file not shown.

View File

@ -24,3 +24,10 @@ class OperatorType(enum.StrEnum):
# Node: Tidy3DWebImporter
NodeLoadCloudSim = enum.auto()
# Node: Tidy3DWebExporter
NodeUploadSimulation = enum.auto()
NodeRunSimulation = enum.auto()
NodeReloadTrackedTask = enum.auto()
NodeEstCostTrackedTask = enum.auto()
ReleaseTrackedTask = enum.auto()

View File

@ -40,7 +40,7 @@ from .flow_signals import FlowSignal
from .icons import Icon
from .mobj_types import ManagedObjType
from .node_types import NodeType
from .sim_types import BoundCondType, SimSpaceAxis
from .sim_types import BoundCondType, SimSpaceAxis, manual_amp_time
from .socket_colors import SOCKET_COLORS
from .socket_types import SocketType
from .tree_types import TreeType
@ -80,6 +80,7 @@ __all__ = [
'NodeType',
'BoundCondType',
'SimSpaceAxis',
'manual_amp_time',
'NodeCategory',
'NODE_CAT_LABELS',
'ManagedObjType',

View File

@ -5,11 +5,13 @@ import typing as typ
import bpy
import sympy as sp
from blender_maxwell.utils import blender_type_enum
from blender_maxwell.utils import extra_sympy_units as spux
from blender_maxwell.utils import logger
from .socket_types import SocketType
log = logger.get(__name__)
BL_SOCKET_DESCR_ANNOT_STRING = ':: '
@ -190,13 +192,14 @@ class BLSocketType(enum.StrEnum):
return {
BLST.Color: S.Vec4,
BLST.Rotation: S.Vec3,
BLST.Vector: S.Vec3,
BLST.VectorAcceleration: S.Vec3,
BLST.VectorDirection: S.Vec3,
BLST.VectorEuler: S.Vec3,
BLST.VectorTranslation: S.Vec3,
BLST.VectorVelocity: S.Vec3,
BLST.VectorXYZ: S.Vec3,
}.get(self, {S.Scalar})
}.get(self, S.Scalar)
@property
def unambiguous_physical_type(self) -> spux.PhysicalType | None:

View File

@ -111,7 +111,10 @@ class CapabilitiesFlow:
for name in self.must_match
)
# ∀b (b ∈ A) Constraint
and self.present_any.issubset(other.allow_any)
and (
self.present_any & other.allow_any
or (not self.present_any and not self.allow_any)
)
)
@ -777,7 +780,7 @@ class ParamsFlow:
raise ValueError(msg)
return [
spux.convert_to_unit_system(arg, unit_system, use_jax_array=True)
spux.scale_to_unit_system(arg, unit_system, use_jax_array=True)
if arg not in symbol_values
else symbol_values[arg]
for arg in self.func_args

View File

@ -3,11 +3,45 @@
import enum
import typing as typ
import jax.numpy as jnp
import tidy3d as td
def manual_amp_time(self, time: float) -> complex:
"""Copied implementation of `pulse.amp_time` for `tidy3d` temporal shapes, which replaces use of `numpy` with `jax.numpy` for `jit`-ability.
Since the function is detached from the method, `self` is not implicitly available. It should be pre-defined from a real source time object using `functools.partial`, before `jax.jit`ing.
## License
**This function is directly copied from `tidy3d`**.
As such, it should be considered available under the `tidy3d` license (as of writing, LGPL 2.1): <https://github.com/flexcompute/tidy3d/blob/develop/LICENSE>
## Reference
Permalink to GitHub source code: <https://github.com/flexcompute/tidy3d/blob/3ee34904eb6687a86a5fb3f4ed6d3295c228cd83/tidy3d/components/source.py#L143C1-L163C25>
"""
twidth = 1.0 / (2 * jnp.pi * self.fwidth)
omega0 = 2 * jnp.pi * self.freq0
time_shifted = time - self.offset * twidth
offset = jnp.exp(1j * self.phase)
oscillation = jnp.exp(-1j * omega0 * time)
amp = jnp.exp(-(time_shifted**2) / 2 / twidth**2) * self.amplitude
pulse_amp = offset * oscillation * amp
# subtract out DC component
if self.remove_dc_component:
pulse_amp = pulse_amp * (1j + time_shifted / twidth**2 / omega0)
else:
# 1j to make it agree in large omega0 limit
pulse_amp = pulse_amp * 1j
return pulse_amp
## TODO: Sim Domain type, w/pydantic checks!
class SimSpaceAxis(enum.StrEnum):
"""The axis labels of the global simulation coordinate system."""
@ -61,13 +95,13 @@ class BoundCondType(enum.StrEnum):
Attributes:
Pml: "Perfectly Matched Layer" models infinite free space.
**Should be placed sufficiently far** (ex. $\frac{\lambda}{2}) from any active structures to mitigate divergence.
Periodic: Denotes Bloch-basedrepetition
Periodic: Denotes naive Bloch boundaries (aka. periodic w/phase shift of 0).
Pec: "Perfect Electrical Conductor" models a surface that perfectly reflects electric fields.
Pmc: "Perfect Magnetic Conductor" models a surface that perfectly reflects the magnetic fields.
"""
Pml = enum.auto()
Periodic = enum.auto()
NaiveBloch = enum.auto()
Pec = enum.auto()
Pmc = enum.auto()
@ -86,7 +120,7 @@ class BoundCondType(enum.StrEnum):
BCT.Pml: 'PML',
BCT.Pec: 'PEC',
BCT.Pmc: 'PMC',
BCT.Periodic: 'Periodic',
BCT.NaiveBloch: 'NaiveBloch',
}[v]
@staticmethod
@ -115,5 +149,5 @@ class BoundCondType(enum.StrEnum):
BCT.Pml: td.PML(),
BCT.Pec: td.PECBoundary(),
BCT.Pmc: td.PMCBoundary(),
BCT.Periodic: td.Periodic(),
BCT.NaiveBloch: td.Periodic(),
}[self]

View File

@ -5,10 +5,9 @@ from . import (
mediums,
monitors,
outputs,
# simulations,
simulations,
sources,
# structures,
# utilities,
structures,
)
BL_REGISTER = [
@ -17,11 +16,10 @@ BL_REGISTER = [
*outputs.BL_REGISTER,
*sources.BL_REGISTER,
*mediums.BL_REGISTER,
# *structures.BL_REGISTER,
*structures.BL_REGISTER,
*bounds.BL_REGISTER,
*monitors.BL_REGISTER,
# *simulations.BL_REGISTER,
# *utilities.BL_REGISTER,
*simulations.BL_REGISTER,
]
BL_NODES = {
**analysis.BL_NODES,
@ -29,9 +27,8 @@ BL_NODES = {
**outputs.BL_NODES,
**sources.BL_NODES,
**mediums.BL_NODES,
# **structures.BL_NODES,
**structures.BL_NODES,
**bounds.BL_NODES,
**monitors.BL_NODES,
# **simulations.BL_NODES,
# **utilities.BL_NODES,
**simulations.BL_NODES,
}

View File

@ -45,7 +45,7 @@ class ExtractDataNode(base.MaxwellSimNode):
}
output_socket_sets: typ.ClassVar = {
'Sim Data': {'Monitor Data': sockets.MaxwellMonitorDataSocketDef()},
'Monitor Data': {'Expr': sockets.ExprSocketDef()},
'Monitor Data': {'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array)},
}
####################
@ -409,7 +409,7 @@ class ExtractDataNode(base.MaxwellSimNode):
####################
@events.computes_output_socket(
# Trigger
'Data',
'Expr',
kind=ct.FlowKind.Info,
# Loaded
props={'monitor_data_type', 'extract_filter'},

View File

@ -110,10 +110,10 @@ class FilterMathNode(base.MaxwellSimNode):
bl_label = 'Filter Math'
input_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(),
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(),
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
####################
@ -187,7 +187,6 @@ class FilterMathNode(base.MaxwellSimNode):
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
layout.prop(self, self.blfields['operation'], text='')
if self.active_socket_set == 'Dimensions':
if self.operation in [FilterOperation.PinLen1, FilterOperation.Pin]:
layout.prop(self, self.blfields['dim_0'], text='')
@ -292,7 +291,7 @@ class FilterMathNode(base.MaxwellSimNode):
return lazy_value_func.compose_within(
operation.jax_func(axis_0, axis_1),
enclosing_func_args=[int] if operation == 'PIN' else [],
enclosing_func_args=[int] if operation == FilterOperation.Pin else [],
supports_jax=True,
)
return ct.FlowSignal.FlowPending
@ -383,7 +382,7 @@ class FilterMathNode(base.MaxwellSimNode):
pinned_value = input_sockets['Value']
has_pinned_value = not ct.FlowSignal.check(pinned_value)
if props['operation'] == 'PIN' and has_pinned_value:
if props['operation'] == FilterOperation.Pin and has_pinned_value:
nearest_idx_to_value = info.dim_idx[dim_0].nearest_idx_of(
pinned_value, require_sorted=True
)

View File

@ -56,11 +56,11 @@ class OperateMathNode(base.MaxwellSimNode):
bl_label = 'Operate Math'
input_sockets: typ.ClassVar = {
'Expr L': sockets.ExprSocketDef(),
'Expr R': sockets.ExprSocketDef(),
'Expr L': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
'Expr R': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
output_sockets: typ.ClassVar = {
'Expr': sockets.ExprSocketDef(),
'Expr': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
####################

View File

@ -477,6 +477,7 @@ class MaxwellSimNode(bpy.types.Node):
for socket_name, socket_def in created_sockets.items():
socket_def.preinit(all_bl_sockets[socket_name])
socket_def.init(all_bl_sockets[socket_name])
socket_def.postinit(all_bl_sockets[socket_name])
def _sync_sockets(self) -> None:
"""Synchronize the node's sockets with the active sockets.

View File

@ -208,7 +208,7 @@ class BlochBoundCondNode(base.MaxwellSimNode):
return td.Periodic()
# Source-Derived
if props['active_socket_set'] == 'Naive':
if props['active_socket_set'] == 'Source-Derived':
sim_domain = input_sockets['Sim Domain']
valid_sim_axis = props['valid_sim_axis']

View File

@ -301,15 +301,16 @@ class LibraryMediumNode(base.MaxwellSimNode):
####################
@events.on_show_plot(
managed_objs={'plot'},
props={'material'},
props={'medium'},
stop_propagation=True,
)
def on_show_plot(
self,
managed_objs: dict,
managed_objs,
props,
):
managed_objs['plot'].mpl_plot_to_image(
lambda ax: self.medium.plot(self.medium.frequency_range, ax=ax),
lambda ax: self.medium.plot(props['medium'].frequency_range, ax=ax),
bl_select=True,
)
## TODO: Plot based on Wl, not freq.

View File

@ -1,13 +1,13 @@
#from . import file_exporters, viewer, web_exporters
from . import viewer
# from . import file_exporters, viewer, web_exporters
from . import viewer, web_exporters
BL_REGISTER = [
*viewer.BL_REGISTER,
#*file_exporters.BL_REGISTER,
#*web_exporters.BL_REGISTER,
# *file_exporters.BL_REGISTER,
*web_exporters.BL_REGISTER,
]
BL_NODES = {
**viewer.BL_NODES,
#**file_exporters.BL_NODES,
#**web_exporters.BL_NODES,
# **file_exporters.BL_NODES,
**web_exporters.BL_NODES,
}

View File

@ -1,16 +1,23 @@
import bpy
import typing as typ
import bpy
import tidy3d as td
from blender_maxwell.services import tdcloud
from blender_maxwell.utils import bl_cache, logger
from ......services import tdcloud
from .... import contracts as ct
from .... import sockets
from ... import base, events
log = logger.get(__name__)
####################
# - Web Uploader / Loader / Runner / Releaser
####################
class UploadSimulation(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__upload_simulation'
bl_idname = ct.OperatorType.NodeUploadSimulation
bl_label = 'Upload Tidy3D Simulation'
bl_description = 'Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud'
@ -23,7 +30,7 @@ class UploadSimulation(bpy.types.Operator):
and context.node.lock_tree
and tdcloud.IS_AUTHENTICATED
and not context.node.tracked_task_id
and context.node.inputs['FDTD Sim'].is_linked
and context.node.inputs['Sim'].is_linked
)
def execute(self, context):
@ -33,7 +40,7 @@ class UploadSimulation(bpy.types.Operator):
class RunSimulation(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__run_simulation'
bl_idname = ct.OperatorType.NodeRunSimulation
bl_label = 'Run Tracked Tidy3D Sim'
bl_description = 'Run the currently tracked simulation task'
@ -61,7 +68,7 @@ class RunSimulation(bpy.types.Operator):
class ReloadTrackedTask(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__reload_tracked_task'
bl_idname = ct.OperatorType.NodeReloadTrackedTask
bl_label = 'Reload Tracked Tidy3D Cloud Task'
bl_description = 'Reload the currently tracked simulation task'
@ -86,7 +93,7 @@ class ReloadTrackedTask(bpy.types.Operator):
class EstCostTrackedTask(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__est_cost_tracked_task'
bl_idname = ct.OperatorType.NodeEstCostTrackedTask
bl_label = 'Est Cost of Tracked Tidy3D Cloud Task'
bl_description = 'Reload the currently tracked simulation task'
@ -113,7 +120,7 @@ class EstCostTrackedTask(bpy.types.Operator):
class ReleaseTrackedTask(bpy.types.Operator):
bl_idname = 'blender_maxwell.nodes__release_tracked_task'
bl_idname = ct.OperatorType.ReleaseTrackedTask
bl_label = 'Release Tracked Tidy3D Cloud Task'
bl_description = 'Release the currently tracked simulation task'
@ -140,92 +147,60 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.Tidy3DWebExporter
bl_label = 'Tidy3D Web Exporter'
input_sockets = {
'FDTD Sim': sockets.MaxwellFDTDSimSocketDef(),
input_sockets: typ.ClassVar = {
'Sim': sockets.MaxwellFDTDSimSocketDef(),
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=False,
),
}
output_sockets: typ.ClassVar = {
'Sim Data': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
}
####################
# - Properties
####################
lock_tree: bpy.props.BoolProperty(
name='Whether to lock the attached tree',
description='Whether or not to lock the attached tree',
default=False,
update=lambda self, context: self.sync_lock_tree(context),
)
tracked_task_id: bpy.props.StringProperty(
name='Tracked Task ID',
description='The currently tracked task ID',
default='',
update=lambda self, context: self.sync_tracked_task_id(context),
)
# Cache
cache_total_monitor_data: bpy.props.FloatProperty(
name='(Cached) Total Monitor Data',
description='Required storage space by all monitors',
default=0.0,
)
cache_est_cost: bpy.props.FloatProperty(
name='(Cached) Estimated Total Cost',
description='Est. Cost in FlexCompute units',
default=-1.0,
)
lock_tree: bool = bl_cache.BLField(False, prop_ui=True)
tracked_task_id: str = bl_cache.BLField('', prop_ui=True)
####################
# - Sync Methods
# - Computed
####################
def sync_lock_tree(self, context):
if self.lock_tree:
self.trigger_event(ct.FlowEvent.EnableLock)
self.locked = False
for bl_socket in self.inputs:
if bl_socket.name == 'FDTD Sim':
continue
bl_socket.locked = False
else:
self.trigger_event(ct.FlowEvent.DisableLock)
self.on_prop_changed('lock_tree', context)
def sync_tracked_task_id(self, context):
# Select Tracked Task
if self.tracked_task_id:
cloud_task = tdcloud.TidyCloudTasks.task(self.tracked_task_id)
task_info = tdcloud.TidyCloudTasks.task_info(self.tracked_task_id)
self.loose_output_sockets = {
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
should_exist=True,
),
}
self.inputs['Cloud Task'].locked = True
# Release Tracked Task
else:
self.cache_est_cost = -1.0
self.loose_output_sockets = {}
self.inputs['Cloud Task'].on_prepare_new_task()
self.inputs['Cloud Task'].locked = False
self.on_prop_changed('tracked_task_id', context)
####################
# - Output Socket Callbacks
####################
def validate_sim(self):
if (sim := self._compute_input('FDTD Sim')) is None:
msg = 'Tried to validate simulation, but none is attached'
raise ValueError(msg)
@bl_cache.cached_bl_property(persist=False)
def sim(self) -> td.Simulation | None:
sim = self._compute_input('Sim')
has_sim = not ct.FlowSignal.check(sim)
if has_sim:
sim.validate_pre_upload(source_required=True)
return sim
return None
@bl_cache.cached_bl_property(persist=False)
def total_monitor_data(self) -> float:
if self.sim is not None:
return sum(self.sim.monitors_data_size.values())
return 0.0
@bl_cache.cached_bl_property(persist=False)
def est_cost(self) -> float | None:
if self.tracked_task_id != '':
task_info = tdcloud.TidyCloudTasks.task_info(self.tracked_task_id)
if task_info is not None:
return task_info.cost_est()
return None
####################
# - Methods
####################
def upload_sim(self):
if (sim := self._compute_input('FDTD Sim')) is None:
if self.sim is None:
msg = 'Tried to upload simulation, but none is attached'
raise ValueError(msg)
@ -242,7 +217,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
cloud_task = tdcloud.TidyCloudTasks.mk_task(
task_name=new_task[0],
cloud_folder=new_task[1],
sim=sim,
sim=self.sim,
verbose=True,
)
@ -271,22 +246,24 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
# Row: Upload Sim Buttons
row = layout.row(align=True)
row.operator(
UploadSimulation.bl_idname,
ct.OperatorType.NodeUploadSimulation,
text='Upload',
)
tree_lock_icon = 'LOCKED' if self.lock_tree else 'UNLOCKED'
row.prop(self, 'lock_tree', toggle=True, icon=tree_lock_icon, text='')
row.prop(
self, self.blfields['lock_tree'], toggle=True, icon=tree_lock_icon, text=''
)
# Row: Run Sim Buttons
row = layout.row(align=True)
row.operator(
RunSimulation.bl_idname,
ct.OperatorType.NodeRunSimulation,
text='Run',
)
if self.tracked_task_id:
tree_lock_icon = 'LOOP_BACK'
row.operator(
ReleaseTrackedTask.bl_idname,
ct.OperatorType.ReleaseTrackedTask,
icon='LOOP_BACK',
text='',
)
@ -313,7 +290,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
col.label(icon=conn_icon)
# Simulation Info
if self.inputs['FDTD Sim'].is_linked:
if self.inputs['Sim'].is_linked:
row = layout.row()
row.alignment = 'CENTER'
row.label(text='Sim Info')
@ -327,7 +304,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
## Split: Right Column
col = split.column(align=False)
col.alignment = 'RIGHT'
col.label(text=f'{self.cache_total_monitor_data / 1_000_000:.2f}MB')
col.label(text=f'{self.total_monitor_data / 1_000_000:.2f}MB')
# Cloud Task Info
if self.tracked_task_id and tdcloud.IS_AUTHENTICATED:
@ -348,12 +325,12 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
text=f'Status: {task_info.status.capitalize()}',
)
row.operator(
ReloadTrackedTask.bl_idname,
ct.OperatorType.NodeReloadTrackedTask,
text='',
icon='FILE_REFRESH',
)
row.operator(
EstCostTrackedTask.bl_idname,
ct.OperatorType.NodeEstCostTrackedTask,
text='',
icon='SORTTIME',
)
@ -369,9 +346,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
col.label(text='Real Cost')
## Split: Right Column
cost_est = (
f'{self.cache_est_cost:.2f}' if self.cache_est_cost >= 0 else 'TBD'
)
cost_est = f'{self.est_cost:.2f}' if self.est_cost >= 0 else 'TBD'
cost_real = (
f'{task_info.cost_real:.2f}'
if task_info.cost_real is not None
@ -386,9 +361,37 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
# Connection Information
####################
# - Events
####################
@events.on_value_changed(prop_name='lock_tree', props={'lock_tree'})
def on_lock_tree_changed(self, props):
if props['lock_tree']:
self.trigger_event(ct.FlowEvent.EnableLock)
self.locked = False
for bl_socket in self.inputs:
if bl_socket.name == 'Sim':
continue
bl_socket.locked = False
else:
self.trigger_event(ct.FlowEvent.DisableLock)
@events.on_value_changed(prop_name='tracked_task_id', props={'tracked_task_id'})
def on_tracked_task_id_changed(self, props):
if props['tracked_task_id']:
self.inputs['Cloud Task'].locked = True
else:
self.total_monitor_data = bl_cache.Signal.InvalidateCache
self.est_cost = bl_cache.Signal.InvalidateCache
self.inputs['Cloud Task'].on_prepare_new_task()
self.inputs['Cloud Task'].locked = False
####################
# - Output Methods
####################
## TODO: Retrieve simulation data if/when the simulation is done
@events.computes_output_socket(
'Cloud Task',
input_sockets={'Cloud Task'},
@ -399,21 +402,6 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
return None
####################
# - Output Methods
####################
@events.on_value_changed(
socket_name='FDTD Sim',
input_sockets={'FDTD Sim'},
)
def on_value_changed__fdtd_sim(self, input_sockets):
if (sim := self._compute_input('FDTD Sim')) is None:
self.cache_total_monitor_data = 0
return
sim.validate_pre_upload(source_required=True)
self.cache_total_monitor_data = sum(sim.monitors_data_size.values())
####################
# - Blender Registration

View File

@ -1,14 +1,16 @@
# from . import sim_grid
# from . import sim_grid_axes
from . import fdtd_sim, sim_domain
from . import fdtd_sim, sim_domain, combine
BL_REGISTER = [
*combine.BL_REGISTER,
*sim_domain.BL_REGISTER,
# *sim_grid.BL_REGISTER,
# *sim_grid_axes.BL_REGISTER,
*fdtd_sim.BL_REGISTER,
]
BL_NODES = {
**combine.BL_NODES,
**sim_domain.BL_NODES,
# **sim_grid.BL_NODES,
# **sim_grid_axes.BL_NODES,

View File

@ -1,8 +1,9 @@
import typing as typ
import bpy
import sympy as sp
from blender_maxwell.utils import bl_cache
from ... import contracts as ct
from ... import sockets
from .. import base, events
@ -19,24 +20,8 @@ class CombineNode(base.MaxwellSimNode):
'Maxwell Sources': {},
'Maxwell Structures': {},
'Maxwell Monitors': {},
'Real 3D Vector': {f'x_{i}': sockets.RealNumberSocketDef() for i in range(3)},
# "Point 3D": {
# axis: sockets.PhysicalLengthSocketDef()
# for i, axis in zip(
# range(3),
# ["x", "y", "z"]
# )
# },
# "Size 3D": {
# axis_key: sockets.PhysicalLengthSocketDef()
# for i, axis_key, axis_label in zip(
# range(3),
# ["x_size", "y_size", "z_size"],
# ["X Size", "Y Size", "Z Size"],
# )
# },
}
output_socket_sets = {
output_socket_sets: typ.ClassVar = {
'Maxwell Sources': {
'Sources': sockets.MaxwellSourceSocketDef(
is_list=True,
@ -52,43 +37,69 @@ class CombineNode(base.MaxwellSimNode):
is_list=True,
),
},
'Real 3D Vector': {
'Real 3D Vector': sockets.Real3DVectorSocketDef(),
},
# "Point 3D": {
# "3D Point": sockets.PhysicalPoint3DSocketDef(),
# },
# "Size 3D": {
# "3D Size": sockets.PhysicalSize3DSocketDef(),
# },
}
amount: bpy.props.IntProperty(
name='# Objects to Combine',
description='Amount of Objects to Combine',
default=1,
min=1,
# max=MAX_AMOUNT,
update=lambda self, context: self.on_prop_changed('amount', context),
)
####################
# - Draw
####################
amount: int = bl_cache.BLField(2, abs_min=1, prop_ui=True)
####################
# - Draw
####################
def draw_props(self, context, layout):
layout.prop(self, 'amount', text='#')
layout.prop(self, self.blfields['amount'], text='')
####################
# - Events
####################
@events.on_value_changed(
# Trigger
prop_name={'active_socket_set', 'amount'},
props={'active_socket_set', 'amount'},
run_on_init=True,
)
def on_inputs_changed(self, props):
if props['active_socket_set'] == 'Maxwell Sources':
if (
not self.loose_input_sockets
or not next(iter(self.loose_input_sockets)).startswith('Source')
or len(self.loose_input_sockets) != props['amount']
):
self.loose_input_sockets = {
f'Source #{i}': sockets.MaxwellSourceSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Structures':
if (
not self.loose_input_sockets
or not next(iter(self.loose_input_sockets)).startswith('Structure')
or len(self.loose_input_sockets) != props['amount']
):
self.loose_input_sockets = {
f'Structure #{i}': sockets.MaxwellStructureSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Monitors':
if (
not self.loose_input_sockets
or not next(iter(self.loose_input_sockets)).startswith('Monitor')
or len(self.loose_input_sockets) != props['amount']
):
self.loose_input_sockets = {
f'Monitor #{i}': sockets.MaxwellMonitorSocketDef()
for i in range(props['amount'])
}
elif self.loose_input_sockets:
self.loose_input_sockets = {}
####################
# - Output Socket Computation
####################
@events.computes_output_socket(
'Real 3D Vector', input_sockets={'x_0', 'x_1', 'x_2'}
)
def compute_real_3d_vector(self, input_sockets) -> sp.Expr:
return sp.Matrix([input_sockets[f'x_{i}'] for i in range(3)])
@events.computes_output_socket(
'Sources',
kind=ct.FlowKind.Array,
all_loose_input_sockets=True,
props={'amount'},
)
@ -97,6 +108,7 @@ class CombineNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Structures',
kind=ct.FlowKind.Array,
all_loose_input_sockets=True,
props={'amount'},
)
@ -105,45 +117,13 @@ class CombineNode(base.MaxwellSimNode):
@events.computes_output_socket(
'Monitors',
kind=ct.FlowKind.Array,
all_loose_input_sockets=True,
props={'amount'},
)
def compute_monitors(self, loose_input_sockets, props) -> sp.Expr:
return [loose_input_sockets[f'Monitor #{i}'] for i in range(props['amount'])]
####################
# - Input Socket Compilation
####################
@events.on_value_changed(
prop_name='active_socket_set',
props={'active_socket_set', 'amount'},
run_on_init=True,
)
def on_value_changed__active_socket_set(self, props):
if props['active_socket_set'] == 'Maxwell Sources':
self.loose_input_sockets = {
f'Source #{i}': sockets.MaxwellSourceSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Structures':
self.loose_input_sockets = {
f'Structure #{i}': sockets.MaxwellStructureSocketDef()
for i in range(props['amount'])
}
elif props['active_socket_set'] == 'Maxwell Monitors':
self.loose_input_sockets = {
f'Monitor #{i}': sockets.MaxwellMonitorSocketDef()
for i in range(props['amount'])
}
else:
self.loose_input_sockets = {}
@events.on_value_changed(
prop_name='amount',
)
def on_value_changed__amount(self):
self.on_value_changed__active_socket_set()
####################
# - Blender Registration
@ -151,4 +131,4 @@ class CombineNode(base.MaxwellSimNode):
BL_REGISTER = [
CombineNode,
]
BL_NODES = {ct.NodeType.Combine: (ct.NodeCategory.MAXWELLSIM_UTILITIES)}
BL_NODES = {ct.NodeType.Combine: (ct.NodeCategory.MAXWELLSIM_SIMS)}

View File

@ -1,3 +1,5 @@
import typing as typ
import sympy as sp
import tidy3d as td
@ -13,9 +15,9 @@ class FDTDSimNode(base.MaxwellSimNode):
####################
# - Sockets
####################
input_sockets = {
'Domain': sockets.MaxwellSimDomainSocketDef(),
input_sockets: typ.ClassVar = {
'BCs': sockets.MaxwellBoundCondsSocketDef(),
'Domain': sockets.MaxwellSimDomainSocketDef(),
'Sources': sockets.MaxwellSourceSocketDef(
is_list=True,
),
@ -26,32 +28,33 @@ class FDTDSimNode(base.MaxwellSimNode):
is_list=True,
),
}
output_sockets = {
'FDTD Sim': sockets.MaxwellFDTDSimSocketDef(),
output_sockets: typ.ClassVar = {
'Sim': sockets.MaxwellFDTDSimSocketDef(),
}
####################
# - Output Socket Computation
####################
@events.computes_output_socket(
'FDTD Sim',
'Sim',
kind=ct.FlowKind.Value,
input_sockets={'Sources', 'Structures', 'Domain', 'BCs', 'Monitors'},
input_socket_kinds={
'Sources': ct.FlowKind.Array,
'Structures': ct.FlowKind.Array,
'Domain': ct.FlowKind.Value,
'BCs': ct.FlowKind.Value,
'Monitors': ct.FlowKind.Array,
},
)
def compute_fdtd_sim(self, input_sockets: dict) -> sp.Expr:
## TODO: Visualize the boundary conditions on top of the sim domain
sim_domain = input_sockets['Domain']
sources = input_sockets['Sources']
structures = input_sockets['Structures']
bounds = input_sockets['BCs']
monitors = input_sockets['Monitors']
# if not isinstance(sources, list):
# sources = [sources]
# if not isinstance(structures, list):
# structures = [structures]
# if not isinstance(monitors, list):
# monitors = [monitors]
return td.Simulation(
**sim_domain, ## run_time=, size=, grid=, medium=
structures=structures,

View File

@ -4,11 +4,15 @@ import sympy as sp
import sympy.physics.units as spu
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 SimDomainNode(base.MaxwellSimNode):
node_type = ct.NodeType.SimDomain
@ -16,12 +20,27 @@ class SimDomainNode(base.MaxwellSimNode):
use_sim_node_name = True
input_sockets: typ.ClassVar = {
'Duration': sockets.PhysicalTimeSocketDef(
default_value=5 * spu.ps,
default_unit=spu.ps,
'Duration': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Time,
default_unit=spu.picosecond,
default_value=5,
abs_min=0,
),
'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
'Size': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([1, 1, 1]),
abs_min=0.001,
),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(),
'Grid': sockets.MaxwellSimGridSocketDef(),
'Ambient Medium': sockets.MaxwellMediumSocketDef(),
}
@ -34,44 +53,6 @@ class SimDomainNode(base.MaxwellSimNode):
'modifier': managed_objs.ManagedBLModifier,
}
####################
# - Events
####################
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_input_changed(
self,
props: dict,
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
):
# 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.SimulationSimDomain),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
####################
# - Outputs
####################
@ -94,6 +75,59 @@ class SimDomainNode(base.MaxwellSimNode):
'medium': input_sockets['Ambient Medium'],
}
####################
# - Preview
####################
@events.on_value_changed(
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
else:
mesh.hide_preview()
@events.on_value_changed(
## Trigger
socket_name={'Center', 'Size'},
run_on_init=True,
# Loaded
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'BlenderUnits',
},
)
def on_input_changed(
self,
managed_objs,
input_sockets,
unit_systems,
):
mesh = managed_objs['mesh']
modifier = managed_objs['modifier']
center = input_sockets['Center']
size = input_sockets['Size']
unit_system = unit_systems['BlenderUnits']
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES',
{
'node_group': import_geonodes(GeoNodes.SimulationSimDomain),
'inputs': {'Size': size},
'unit_system': unit_system,
},
)
####################
# - Blender Registration

View File

@ -2,7 +2,7 @@ from . import (
# astigmatic_gaussian_beam_source,
# gaussian_beam_source,
# plane_wave_source,
# point_dipole_source,
point_dipole_source,
temporal_shapes,
# tfsf_source,
# uniform_current_source,
@ -10,18 +10,18 @@ from . import (
BL_REGISTER = [
*temporal_shapes.BL_REGISTER,
#*point_dipole_source.BL_REGISTER,
*point_dipole_source.BL_REGISTER,
# *uniform_current_source.BL_REGISTER,
#*plane_wave_source.BL_REGISTER,
# *plane_wave_source.BL_REGISTER,
# *gaussian_beam_source.BL_REGISTER,
# *astigmatic_gaussian_beam_source.BL_REGISTER,
# *tfsf_source.BL_REGISTER,
]
BL_NODES = {
**temporal_shapes.BL_NODES,
#**point_dipole_source.BL_NODES,
**point_dipole_source.BL_NODES,
# **uniform_current_source.BL_NODES,
#**plane_wave_source.BL_NODES,
# **plane_wave_source.BL_NODES,
# **gaussian_beam_source.BL_NODES,
# **astigmatic_gaussian_beam_source.BL_NODES,
# **tfsf_source.BL_NODES,

View File

@ -1,64 +1,62 @@
import typing as typ
import bpy
import sympy as sp
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 ... import contracts as ct
from ... import managed_objs, sockets
from .. import base, events
log = logger.get(__name__)
class PointDipoleSourceNode(base.MaxwellSimNode):
node_type = ct.NodeType.PointDipoleSource
bl_label = 'Point Dipole Source'
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets = {
input_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_value=sp.Matrix([0, 0, 0]),
),
'Interpolate': sockets.BoolSocketDef(
default_value=True,
),
}
output_sockets = {
output_sockets: typ.ClassVar = {
'Source': sockets.MaxwellSourceSocketDef(),
}
managed_obj_types = {
managed_obj_types: typ.ClassVar = {
'mesh': managed_objs.ManagedBLMesh,
'modifier': managed_objs.ManagedBLModifier,
}
####################
# - Properties
####################
pol_axis: bpy.props.EnumProperty(
name='Polarization Axis',
description='Polarization Axis',
items=[
('EX', 'Ex', 'Electric field in x-dir'),
('EY', 'Ey', 'Electric field in y-dir'),
('EZ', 'Ez', 'Electric field in z-dir'),
],
default='EX',
update=(lambda self, context: self.on_prop_changed('pol_axis', context)),
)
pol_axis: ct.SimSpaceAxis = bl_cache.BLField(ct.SimSpaceAxis.X, prop_ui=True)
####################
# - UI
####################
def draw_props(self, context, layout):
split = layout.split(factor=0.6)
col = split.column()
col.label(text='Pol Axis')
col = split.column()
col.prop(self, 'pol_axis', text='')
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout):
layout.prop(self, self.blfields['pol_axis'], expand=True)
####################
# - Output Socket Computation
# - Outputs
####################
@events.computes_output_socket(
'Source',
@ -76,9 +74,9 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
unit_systems: dict,
) -> td.PointDipole:
pol_axis = {
'EX': 'Ex',
'EY': 'Ey',
'EZ': 'Ez',
ct.SimSpaceAxis.X: 'Ex',
ct.SimSpaceAxis.Y: 'Ey',
ct.SimSpaceAxis.Z: 'Ez',
}[props['pol_axis']]
return td.PointDipole(
@ -88,39 +86,59 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
polarization=pol_axis,
)
#####################
## - Preview
#####################
# @events.on_value_changed(
# socket_name='Center',
# input_sockets={'Center'},
# managed_objs={'sphere_empty'},
# )
# def on_value_changed__center(
# self,
# input_sockets: dict,
# managed_objs: dict[str, ct.schemas.ManagedObj],
# ):
# _center = input_sockets['Center']
# center = tuple(spu.convert_to(_center, spu.um) / spu.um)
# ## TODO: Preview unit system?? Presume um for now
####################
# - Preview
####################
@events.on_value_changed(
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh']
# mobj = managed_objs['sphere_empty']
# bl_object = mobj.bl_object('EMPTY')
# bl_object.location = center # tuple([float(el) for el in center])
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
else:
mesh.hide_preview()
# @events.on_show_preview(
# managed_objs={'sphere_empty'},
# )
# def on_show_preview(
# self,
# managed_objs: dict[str, ct.schemas.ManagedObj],
# ):
# managed_objs['sphere_empty'].show_preview(
# 'EMPTY',
# empty_display_type='SPHERE',
# )
# managed_objs['sphere_empty'].bl_object('EMPTY').empty_display_size = 0.2
@events.on_value_changed(
socket_name={'Center'},
prop_name='pol_axis',
run_on_init=True,
# Pass Data
managed_objs={'mesh', 'modifier'},
props={'pol_axis'},
input_sockets={'Center'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={'Center': 'BlenderUnits'},
)
def on_inputs_changed(
self, managed_objs, props, input_sockets, unit_systems
) -> None:
mesh = managed_objs['mesh']
modifier = managed_objs['modifier']
center = input_sockets['Center']
unit_system = unit_systems['BlenderUnits']
axis = {
ct.SimSpaceAxis.X: 0,
ct.SimSpaceAxis.Y: 1,
ct.SimSpaceAxis.Z: 2,
}[props['pol_axis']]
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES',
{
'node_group': import_geonodes(GeoNodes.SourcePointDipole),
'inputs': {'Axis': axis},
'unit_system': unit_system,
},
)
####################

View File

@ -1,13 +1,13 @@
# from . import expr_temporal_shape, pulse_temporal_shape, wave_temporal_shape
from . import pulse_temporal_shape
from . import pulse_temporal_shape, wave_temporal_shape
BL_REGISTER = [
*pulse_temporal_shape.BL_REGISTER,
# *wave_temporal_shape.BL_REGISTER,
*wave_temporal_shape.BL_REGISTER,
# *expr_temporal_shape.BL_REGISTER,
]
BL_NODES = {
**pulse_temporal_shape.BL_NODES,
# **wave_temporal_shape.BL_NODES,
**wave_temporal_shape.BL_NODES,
# **expr_temporal_shape.BL_NODES,
}

View File

@ -4,7 +4,6 @@ import functools
import typing as typ
import bpy
import jax.numpy as jnp
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
@ -16,38 +15,6 @@ from .... import managed_objs, sockets
from ... import base, events
def _manual_amp_time(self, time: float) -> complex:
"""Copied implementation of `pulse.amp_time` for `tidy3d` temporal shapes, which replaces use of `numpy` with `jax.numpy` for `jit`-ability.
Since the function is detached from the method, `self` is not implicitly available. It should be pre-defined from a real source time object using `functools.partial`, before `jax.jit`ing.
## License
**This function is directly copied from `tidy3d`**.
As such, it should be considered available under the `tidy3d` license (as of writing, LGPL 2.1): <https://github.com/flexcompute/tidy3d/blob/develop/LICENSE>
## Reference
Permalink to GitHub source code: <https://github.com/flexcompute/tidy3d/blob/3ee34904eb6687a86a5fb3f4ed6d3295c228cd83/tidy3d/components/source.py#L143C1-L163C25>
"""
twidth = 1.0 / (2 * jnp.pi * self.fwidth)
omega0 = 2 * jnp.pi * self.freq0
time_shifted = time - self.offset * twidth
offset = jnp.exp(1j * self.phase)
oscillation = jnp.exp(-1j * omega0 * time)
amp = jnp.exp(-(time_shifted**2) / 2 / twidth**2) * self.amplitude
pulse_amp = offset * oscillation * amp
# subtract out DC component
if self.remove_dc_component:
pulse_amp = pulse_amp * (1j + time_shifted / twidth**2 / omega0)
else:
# 1j to make it agree in large omega0 limit
pulse_amp = pulse_amp * 1j
return pulse_amp
class PulseTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.PulseTemporalShape
bl_label = 'Gaussian Pulse Temporal Shape'
@ -145,7 +112,7 @@ class PulseTemporalShapeNode(base.MaxwellSimNode):
)
def compute_time_to_efield_lazy(self, output_sockets) -> td.GaussianPulse:
temporal_shape = output_sockets['Temporal Shape']
jax_amp_time = functools.partial(_manual_amp_time, temporal_shape)
jax_amp_time = functools.partial(ct.manual_amp_time, temporal_shape)
## TODO: Don't just partial() it up, do it property in the ParamsFlow!
## -> Right now it's recompiled every time.

View File

@ -1,75 +1,156 @@
"""Implements the `WaveTemporalShapeNode`."""
import functools
import typing as typ
import bpy
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from .... import contracts, sockets
from blender_maxwell.utils import extra_sympy_units as spux
from .... import contracts as ct
from .... import managed_objs, sockets
from ... import base, events
class ContinuousWaveTemporalShapeNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.ContinuousWaveTemporalShape
class WaveTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.WaveTemporalShape
bl_label = 'Continuous Wave Temporal Shape'
# bl_icon = ...
####################
# - Sockets
####################
input_sockets = {
# "amplitude": sockets.RealNumberSocketDef(
# label="Temporal Shape",
# ), ## Should have a unit of some kind...
'phase': sockets.PhysicalAngleSocketDef(
label='Phase',
input_sockets: typ.ClassVar = {
'max E': sockets.ExprSocketDef(
mathtype=spux.MathType.Complex,
physical_type=spux.PhysicalType.EField,
default_value=1 + 0j,
),
'freq_center': sockets.PhysicalFreqSocketDef(
label='Freq Center',
'μ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=500,
),
'freq_std': sockets.PhysicalFreqSocketDef(
label='Freq STD',
),
'time_delay_rel_ang_freq': sockets.RealNumberSocketDef(
label='Time Delay rel. Ang. Freq',
default_value=5.0,
'σ Freq': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Freq,
default_unit=spux.THz,
default_value=200,
),
'Offset Time': sockets.ExprSocketDef(default_value=5, abs_min=2.5),
}
output_sockets = {
'temporal_shape': sockets.MaxwellTemporalShapeSocketDef(
label='Temporal Shape',
),
output_sockets: typ.ClassVar = {
'Temporal Shape': sockets.MaxwellTemporalShapeSocketDef(),
'E(t)': sockets.ExprSocketDef(active_kind=ct.FlowKind.Array),
}
managed_obj_types: typ.ClassVar = {
'plot': managed_objs.ManagedBLImage,
}
####################
# - Output Socket Computation
# - UI
####################
@events.computes_output_socket('temporal_shape')
def compute_source(self: contracts.NodeTypeProtocol) -> td.PointDipole:
_phase = self.compute_input('phase')
_freq_center = self.compute_input('freq_center')
_freq_std = self.compute_input('freq_std')
time_delay_rel_ang_freq = self.compute_input('time_delay_rel_ang_freq')
def draw_info(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
box = layout.box()
row = box.row()
row.alignment = 'CENTER'
row.label(text='Parameter Scale')
cheating_amplitude = 1.0
phase = spu.convert_to(_phase, spu.radian) / spu.radian
freq_center = spu.convert_to(_freq_center, spu.hertz) / spu.hertz
freq_std = spu.convert_to(_freq_std, spu.hertz) / spu.hertz
# Split
split = box.split(factor=0.3, align=False)
return td.ContinuousWave(
amplitude=cheating_amplitude,
phase=phase,
freq0=freq_center,
fwidth=freq_std,
offset=time_delay_rel_ang_freq,
## LHS: Parameter Names
col = split.column()
col.alignment = 'RIGHT'
col.label(text='Off t:')
## RHS: Parameter Units
col = split.column()
col.label(text='1 / 2π·σ(𝑓)')
####################
# - FlowKind: Value
####################
@events.computes_output_socket(
'Temporal Shape',
input_sockets={
'max E',
'μ Freq',
'σ Freq',
'Offset Time',
},
unit_systems={'Tidy3DUnits': ct.UNITS_TIDY3D},
scale_input_sockets={
'max E': 'Tidy3DUnits',
'μ Freq': 'Tidy3DUnits',
'σ Freq': 'Tidy3DUnits',
},
)
def compute_temporal_shape(self, input_sockets, unit_systems) -> td.GaussianPulse:
return td.ContinuousWave(
amplitude=sp.re(input_sockets['max E']),
phase=sp.im(input_sockets['max E']),
freq0=input_sockets['μ Freq'],
fwidth=input_sockets['σ Freq'],
offset=input_sockets['Offset Time'],
)
####################
# - FlowKind: LazyValueFunc / Info / Params
####################
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.LazyValueFunc,
output_sockets={'Temporal Shape'},
)
def compute_time_to_efield_lazy(self, output_sockets) -> td.GaussianPulse:
temporal_shape = output_sockets['Temporal Shape']
jax_amp_time = functools.partial(ct.manual_amp_time, temporal_shape)
## TODO: Don't just partial() it up, do it property in the ParamsFlow!
## -> Right now it's recompiled every time.
return ct.LazyValueFuncFlow(
func=jax_amp_time,
func_args=[spux.PhysicalType.Time],
supports_jax=True,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Info,
)
def compute_time_to_efield_info(self) -> td.GaussianPulse:
return ct.InfoFlow(
dim_names=['t'],
dim_idx={
't': ct.LazyArrayRangeFlow(
start=sp.S(0), stop=sp.oo, steps=0, unit=spu.second
)
},
output_name='E',
output_shape=None,
output_mathtype=spux.MathType.Complex,
output_unit=spu.volt / spu.um,
)
@events.computes_output_socket(
'E(t)',
kind=ct.FlowKind.Params,
)
def compute_time_to_efield_params(self) -> td.GaussianPulse:
sym_time = sp.Symbol('t', real=True, nonnegative=True)
return ct.ParamsFlow(func_args=[sym_time], symbols={sym_time})
####################
# - Blender Registration
####################
BL_REGISTER = [
ContinuousWaveTemporalShapeNode,
WaveTemporalShapeNode,
]
BL_NODES = {
contracts.NodeType.ContinuousWaveTemporalShape: (
contracts.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES
)
ct.NodeType.WaveTemporalShape: (ct.NodeCategory.MAXWELLSIM_SOURCES_TEMPORALSHAPES)
}

View File

@ -1,8 +1,12 @@
import typing as typ
import sympy as sp
import sympy.physics.units as spu
import tidy3d as td
from blender_maxwell.utils import bl_cache, logger
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 bl_socket_map, managed_objs, sockets
from ... import contracts as ct
@ -22,7 +26,13 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
input_sockets: typ.ClassVar = {
'GeoNodes': sockets.BlenderGeoNodesSocketDef(),
'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
}
output_sockets: typ.ClassVar = {
'Structure': sockets.MaxwellStructureSocketDef(),
@ -34,7 +44,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
}
####################
# - Output
# - Outputs
####################
@events.computes_output_socket(
'Structure',
@ -43,14 +53,10 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
)
def compute_structure(
self,
input_sockets: dict,
managed_objs: dict,
input_sockets,
managed_objs,
) -> td.Structure:
"""Computes a triangle-mesh based Tidy3D structure, by manually copying mesh data from Blender to a `td.TriangleMesh`."""
# Simulate Input Value Change
## This ensures that the mesh has been re-computed.
self.on_input_socket_changed()
## TODO: mesh_as_arrays might not take the Center into account.
## - Alternatively, Tidy3D might have a way to transform?
mesh_as_arrays = managed_objs['mesh'].mesh_as_arrays
@ -68,20 +74,12 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
@events.on_value_changed(
prop_name='preview_active',
props={'preview_active'},
input_sockets={'Center'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, input_sockets) -> None:
def on_preview_changed(self, props) -> None:
"""Enables/disables previewing of the GeoNodes-driven mesh, regardless of whether a particular GeoNodes tree is chosen."""
mesh = managed_objs['mesh']
# No Mesh: Create Empty Object
## Ensures that when there is mesh data, it'll be correctly previewed.
## Bit of a workaround - the idea is usually to make the MObj as needed.
if not mesh.exists:
center = input_sockets['Center']
_ = mesh.bl_object(location=center)
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
@ -139,8 +137,8 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
)
def on_input_changed(
self,
managed_objs: dict,
input_sockets: dict,
managed_objs,
input_sockets,
) -> None:
"""Declares new loose input sockets in response to a new GeoNodes tree (if any)."""
geonodes = input_sockets['GeoNodes']

View File

@ -5,11 +5,15 @@ import sympy.physics.units as spu
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 BoxStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoxStructure
@ -21,9 +25,20 @@ class BoxStructureNode(base.MaxwellSimNode):
####################
input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Size': sockets.PhysicalSize3DSocketDef(
default_value=sp.Matrix([500, 500, 500]) * spu.nm
'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
'Size': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.nanometer,
default_value=sp.Matrix([500, 500, 500]),
abs_min=0.001,
),
}
output_sockets: typ.ClassVar = {
@ -36,7 +51,7 @@ class BoxStructureNode(base.MaxwellSimNode):
}
####################
# - Event Methods
# - Outputs
####################
@events.computes_output_socket(
'Structure',
@ -47,7 +62,7 @@ class BoxStructureNode(base.MaxwellSimNode):
'Size': 'Tidy3DUnits',
},
)
def compute_structure(self, input_sockets: dict, unit_systems: dict) -> td.Box:
def compute_structure(self, input_sockets, unit_systems) -> td.Box:
return td.Structure(
geometry=td.Box(
center=input_sockets['Center'],
@ -56,11 +71,27 @@ class BoxStructureNode(base.MaxwellSimNode):
medium=input_sockets['Medium'],
)
####################
# - Preview
####################
@events.on_value_changed(
socket_name={'Center', 'Size'},
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
else:
mesh.hide_preview()
@events.on_value_changed(
socket_name={'Center', 'Size'},
run_on_init=True,
input_sockets={'Center', 'Size'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
@ -70,26 +101,26 @@ class BoxStructureNode(base.MaxwellSimNode):
)
def on_inputs_changed(
self,
props: dict,
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
managed_objs,
input_sockets,
unit_systems,
):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
mesh = managed_objs['mesh']
modifier = managed_objs['modifier']
center = input_sockets['Center']
size = input_sockets['Size']
unit_system = unit_systems['BlenderUnits']
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES',
{
'node_group': import_geonodes(GeoNodes.StructurePrimitiveBox),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Size': input_sockets['Size'],
},
'inputs': {'Size': size},
'unit_system': unit_system,
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
####################

View File

@ -1,14 +1,19 @@
import typing as typ
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 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 SphereStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.SphereStructure
@ -20,9 +25,17 @@ class SphereStructureNode(base.MaxwellSimNode):
####################
input_sockets: typ.ClassVar = {
'Medium': sockets.MaxwellMediumSocketDef(),
'Center': sockets.PhysicalPoint3DSocketDef(),
'Radius': sockets.PhysicalLengthSocketDef(
default_value=150 * spu.nm,
'Center': sockets.ExprSocketDef(
shape=(3,),
mathtype=spux.MathType.Real,
physical_type=spux.PhysicalType.Length,
default_unit=spu.micrometer,
default_value=sp.Matrix([0, 0, 0]),
),
'Radius': sockets.ExprSocketDef(
physical_type=spux.PhysicalType.Length,
default_unit=spu.nanometer,
default_value=150,
),
}
output_sockets: typ.ClassVar = {
@ -35,7 +48,7 @@ class SphereStructureNode(base.MaxwellSimNode):
}
####################
# - Output Socket Computation
# - Outputs
####################
@events.computes_output_socket(
'Structure',
@ -46,7 +59,7 @@ class SphereStructureNode(base.MaxwellSimNode):
'Radius': 'Tidy3DUnits',
},
)
def compute_structure(self, input_sockets: dict) -> td.Box:
def compute_structure(self, input_sockets, unit_systems) -> td.Box:
return td.Structure(
geometry=td.Sphere(
radius=input_sockets['Radius'],
@ -56,43 +69,55 @@ class SphereStructureNode(base.MaxwellSimNode):
)
####################
# - Preview - Changes to Input Sockets
# - Preview
####################
@events.on_value_changed(
socket_name={'Center', 'Radius'},
prop_name='preview_active',
run_on_init=True,
props={'preview_active'},
managed_objs={'mesh'},
)
def on_preview_changed(self, props, managed_objs) -> None:
mesh = managed_objs['mesh']
# Push Preview State to Managed Mesh
if props['preview_active']:
mesh.show_preview()
else:
mesh.hide_preview()
@events.on_value_changed(
socket_name={'Center', 'Radius'},
run_on_init=True,
input_sockets={'Center', 'Radius'},
managed_objs={'mesh', 'modifier'},
unit_systems={'BlenderUnits': ct.UNITS_BLENDER},
scale_input_sockets={
'Center': 'Tidy3DUnits',
'Radius': 'Tidy3DUnits',
'Center': 'BlenderUnits',
},
)
def on_inputs_changed(
self,
props: dict,
managed_objs: dict,
input_sockets: dict,
unit_systems: dict,
managed_objs,
input_sockets,
unit_systems,
):
# Push Input Values to GeoNodes Modifier
managed_objs['modifier'].bl_modifier(
managed_objs['mesh'].bl_object(location=input_sockets['Center']),
mesh = managed_objs['mesh']
modifier = managed_objs['modifier']
center = input_sockets['Center']
radius = input_sockets['Radius']
unit_system = unit_systems['BlenderUnits']
# Push Loose Input Values to GeoNodes Modifier
modifier.bl_modifier(
mesh.bl_object(location=center),
'NODES',
{
'node_group': import_geonodes(GeoNodes.StructurePrimitiveSphere),
'unit_system': unit_systems['BlenderUnits'],
'inputs': {
'Radius': input_sockets['Radius'],
},
'inputs': {'Radius': radius},
'unit_system': unit_system,
},
)
# Push Preview State
if props['preview_active']:
managed_objs['mesh'].show_preview()
####################

View File

@ -1,5 +0,0 @@
####################
# - Blender Registration
####################
BL_REGISTER = []
BL_NODES = {}

View File

@ -1,80 +0,0 @@
import scipy as sc
import sympy as sp
import sympy.physics.units as spu
from .... import contracts, sockets
from ... import base, events
class WaveConverterNode(base.MaxwellSimTreeNode):
node_type = contracts.NodeType.WaveConverter
bl_label = 'Wave Converter'
# bl_icon = ...
####################
# - Sockets
####################
input_sockets = {}
input_socket_sets = {
'freq_to_vacwl': {
'freq': sockets.PhysicalFreqSocketDef(
label='Freq',
),
},
'vacwl_to_freq': {
'vacwl': sockets.PhysicalVacWLSocketDef(
label='Vac WL',
),
},
}
output_sockets = {}
output_socket_sets = {
'freq_to_vacwl': {
'vacwl': sockets.PhysicalVacWLSocketDef(
label='Vac WL',
),
},
'vacwl_to_freq': {
'freq': sockets.PhysicalFreqSocketDef(
label='Freq',
),
},
}
####################
# - Output Socket Computation
####################
@events.computes_output_socket('freq')
def compute_freq(self: contracts.NodeTypeProtocol) -> sp.Expr:
vac_speed_of_light = sc.constants.speed_of_light * spu.meter / spu.second
vacwl = self.compute_input('vacwl')
return spu.convert_to(
vac_speed_of_light / vacwl,
spu.hertz,
)
@events.computes_output_socket('vacwl')
def compute_vacwl(self: contracts.NodeTypeProtocol) -> sp.Expr:
vac_speed_of_light = sc.constants.speed_of_light * spu.meter / spu.second
freq = self.compute_input('freq')
return spu.convert_to(
vac_speed_of_light / freq,
spu.meter,
)
####################
# - Blender Registration
####################
BL_REGISTER = [
WaveConverterNode,
]
BL_NODES = {
contracts.NodeType.WaveConverter: (
contracts.NodeCategory.MAXWELLSIM_UTILITIES_CONVERTERS
)
}

View File

@ -38,6 +38,14 @@ class SocketDef(pyd.BaseModel, abc.ABC):
"""
bl_socket.reset_instance_id()
def postinit(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Pre-initialize a real Blender node socket from this socket definition.
Parameters:
bl_socket: The Blender node socket to alter using data from this SocketDef.
"""
bl_socket.initializing = False
@abc.abstractmethod
def init(self, bl_socket: bpy.types.NodeSocket) -> None:
"""Initializes a real Blender node socket from this socket definition.
@ -195,6 +203,9 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
## Identifiers
cls.bl_idname: str = str(cls.socket_type.value)
cls.set_prop('instance_id', bpy.props.StringProperty, no_update=True)
cls.set_prop(
'initializing', bpy.props.BoolProperty, default=True, no_update=True
)
## Special States
cls.set_prop('locked', bpy.props.BoolProperty, no_update=True, default=False)
@ -249,6 +260,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Attributes:
prop_name: The name of the property that was changed.
"""
## TODO: Evaluate this properly
if self.initializing:
return
if hasattr(self, prop_name):
# Invalidate UI BLField Caches
if prop_name in self.ui_blfields:
@ -537,7 +552,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to set "ct.FlowKind.Value", but socket does not define it'
raise NotImplementedError(msg)
# ValueArray
# Array
@property
def array(self) -> ct.ArrayFlow:
"""Throws a descriptive error.

View File

@ -1,4 +1,3 @@
from ... import contracts as ct
from .. import base

View File

@ -1,4 +1,3 @@
from ... import contracts as ct
from .. import base
@ -18,7 +17,7 @@ class MaxwellStructureSocketDef(base.SocketDef):
def init(self, bl_socket: MaxwellStructureBLSocket) -> None:
if self.is_list:
bl_socket.active_kind = ct.FlowKind.ValueArray
bl_socket.active_kind = ct.FlowKind.Array
####################