feat: Demo-grade simulation feedback loop.

main
Sofus Albert Høgsbro Rose 2024-03-13 19:10:54 +01:00
parent a19403acf7
commit 5be3e20e99
54 changed files with 2452 additions and 915 deletions

View File

@ -7,7 +7,7 @@
## Inputs ## Inputs
[x] Wave Constant [x] Wave Constant
- [ ] Implement export of frequency / wavelength array/range. - [x] Implement export of frequency / wavelength array/range.
[-] Unit System [-] Unit System
- [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row. - [ ] Implement presets, including "Tidy3D" and "Blender", shown in the label row.
@ -41,6 +41,9 @@
[x] Web Export / Tidy3D Web Exporter [x] Web Export / Tidy3D Web Exporter
- [ ] We need better ways of doing checks before uploading, like for monitor data size. Maybe a SimInfo node? - [ ] We need better ways of doing checks before uploading, like for monitor data size. Maybe a SimInfo node?
- [ ] We need to be able to "delete and re-upload" (or maybe just delete from the interface). - [ ] We need to be able to "delete and re-upload" (or maybe just delete from the interface).
- [x] Implement estimation of monitor storage
- [x] Implement cost estimation
- [?] Merge with the Tidy3D File Import (since both are working with HDFs; the web one only really does downloading too).
[x] File Export / JSON File Export [x] File Export / JSON File Export
[ ] File Import / Tidy3D File Export [ ] File Import / Tidy3D File Export
@ -51,11 +54,8 @@
- [ ] Standardize 1D and 2D array loading/saving on numpy's savetxt with gzip enabled. - [ ] Standardize 1D and 2D array loading/saving on numpy's savetxt with gzip enabled.
## Viz ## Viz
[ ] Sim Info [x] Monitor Data Viz
- [ ] Implement estimation of monitor storage - [x] Implement dropdown to choose which monitor in the SimulationData should be visualized (based on which are available in the SimulationData), and implement visualization based on every kind of monitor-adjascent output data type (<https://docs.flexcompute.com/projects/tidy3d/en/latest/api/output_data.html>)
- [ ] Implement cost estimation
[ ] Monitor Data Viz
- [ ] Implement dropdown to choose which monitor in the SimulationData should be visualized (based on which are available in the SimulationData), and implement visualization based on every kind of monitor-adjascent output data type (<https://docs.flexcompute.com/projects/tidy3d/en/latest/api/output_data.html>)
- [ ] Project field values onto a plane object (managed) - [ ] Project field values onto a plane object (managed)
## Sources ## Sources
@ -107,20 +107,20 @@
- [x] Use the modifier itself as memory, via the ManagedObj - [x] Use the modifier itself as memory, via the ManagedObj
- [?] When GeoNodes themselves declare panels, implement a grid-like tab system to select which sockets should be exposed in the node at a given point in time. - [?] When GeoNodes themselves declare panels, implement a grid-like tab system to select which sockets should be exposed in the node at a given point in time.
[ ] Primitive Structures / Plane [ ] Primitive Structures / Plane Structure
[ ] Primitive Structures / Box Structure [x] Primitive Structures / Box Structure
[ ] Primitive Structures / Sphere [x] Primitive Structures / Sphere Structure
[ ] Primitive Structures / Cylinder [ ] Primitive Structures / Cylinder Structure
[ ] Primitive Structures / Ring [ ] Primitive Structures / Ring Structure
[ ] Primitive Structures / Capsule [ ] Primitive Structures / Capsule Structure
[ ] Primitive Structures / Cone [ ] Primitive Structures / Cone Structure
## Monitors ## Monitors
- **ALL**: "Steady-State" / "Time Domain" (only if relevant). - **ALL**: "Steady-State" / "Time Domain" (only if relevant).
[ ] E/H Field Monitor [x] E/H Field Monitor
- [ ] Monitor Domain as dropdown with Frequency or Time - [x] Monitor Domain as dropdown with Frequency or Time
- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel). - [x] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel).
[ ] Field Power Flux Monitor [ ] Field Power Flux Monitor
- [ ] Monitor Domain as dropdown with Frequency or Time - [ ] Monitor Domain as dropdown with Frequency or Time
- [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel). - [ ] Axis-aligned planar 2D (pixel) and coord-aligned box 3D (voxel).
@ -397,3 +397,10 @@
[ ] Test on Windows [ ] Test on Windows
## Node Tree Cache Semantics ## Node Tree Cache Semantics
# TIDY3D BUGS
- Directly running `SimulationTask.get()` is bugged - it doesn't return some fields, including `created_at`. Listing tasks by folder is not broken.

View File

@ -43,4 +43,7 @@ NODE_CAT_LABELS = {
NC.MAXWELLSIM_UTILITIES: "Utilities", NC.MAXWELLSIM_UTILITIES: "Utilities",
NC.MAXWELLSIM_UTILITIES_CONVERTERS: "Converters", NC.MAXWELLSIM_UTILITIES_CONVERTERS: "Converters",
NC.MAXWELLSIM_UTILITIES_OPERATIONS: "Operations", NC.MAXWELLSIM_UTILITIES_OPERATIONS: "Operations",
# Viz/
NC.MAXWELLSIM_VIZ: "Viz",
} }

View File

@ -51,6 +51,9 @@ class NodeCategory(BlenderTypeEnum):
MAXWELLSIM_UTILITIES_CONVERTERS = enum.auto() MAXWELLSIM_UTILITIES_CONVERTERS = enum.auto()
MAXWELLSIM_UTILITIES_OPERATIONS = enum.auto() MAXWELLSIM_UTILITIES_OPERATIONS = enum.auto()
# Viz/
MAXWELLSIM_VIZ = enum.auto()
@classmethod @classmethod
def get_tree(cls): def get_tree(cls):
## TODO: Refactor ## TODO: Refactor

View File

@ -145,3 +145,8 @@ class NodeType(BlenderTypeEnum):
## Utilities / Operations ## Utilities / Operations
ArrayOperation = enum.auto() ArrayOperation = enum.auto()
# Viz
FDTDSimDataViz = enum.auto()

View File

@ -62,6 +62,7 @@ SOCKET_COLORS = {
ST.MaxwellBoundCond: (0.8, 0.7, 0.45, 1.0), # Medium Light Gold ST.MaxwellBoundCond: (0.8, 0.7, 0.45, 1.0), # Medium Light Gold
ST.MaxwellMonitor: (0.7, 0.6, 0.4, 1.0), # Medium Gold ST.MaxwellMonitor: (0.7, 0.6, 0.4, 1.0), # Medium Gold
ST.MaxwellFDTDSim: (0.6, 0.5, 0.35, 1.0), # Medium Dark Gold ST.MaxwellFDTDSim: (0.6, 0.5, 0.35, 1.0), # Medium Dark Gold
ST.MaxwellFDTDSimData: (0.6, 0.5, 0.35, 1.0), # Medium Dark Gold
ST.MaxwellSimGrid: (0.5, 0.4, 0.3, 1.0), # Dark Gold ST.MaxwellSimGrid: (0.5, 0.4, 0.3, 1.0), # Dark Gold
ST.MaxwellSimGridAxis: (0.4, 0.3, 0.25, 1.0), # Darkest Gold ST.MaxwellSimGridAxis: (0.4, 0.3, 0.25, 1.0), # Darkest Gold
ST.MaxwellSimDomain: (0.4, 0.3, 0.25, 1.0), # Darkest Gold ST.MaxwellSimDomain: (0.4, 0.3, 0.25, 1.0), # Darkest Gold

View File

@ -4,64 +4,65 @@ SOCKET_SHAPES = {
# Basic # Basic
ST.Any: "CIRCLE", ST.Any: "CIRCLE",
ST.Bool: "CIRCLE", ST.Bool: "CIRCLE",
ST.String: "SQUARE", ST.String: "CIRCLE",
ST.FilePath: "SQUARE", ST.FilePath: "CIRCLE",
# Number # Number
ST.IntegerNumber: "CIRCLE", ST.IntegerNumber: "CIRCLE",
ST.RationalNumber: "CIRCLE", ST.RationalNumber: "CIRCLE",
ST.RealNumber: "CIRCLE", ST.RealNumber: "CIRCLE",
ST.ComplexNumber: "CIRCLE_DOT", ST.ComplexNumber: "CIRCLE",
# Vector # Vector
ST.Integer2DVector: "SQUARE_DOT", ST.Integer2DVector: "CIRCLE",
ST.Real2DVector: "SQUARE_DOT", ST.Real2DVector: "CIRCLE",
ST.Complex2DVector: "DIAMOND_DOT", ST.Complex2DVector: "CIRCLE",
ST.Integer3DVector: "SQUARE_DOT", ST.Integer3DVector: "CIRCLE",
ST.Real3DVector: "SQUARE_DOT", ST.Real3DVector: "CIRCLE",
ST.Complex3DVector: "DIAMOND_DOT", ST.Complex3DVector: "CIRCLE",
# Physical # Physical
ST.PhysicalUnitSystem: "CIRCLE", ST.PhysicalUnitSystem: "CIRCLE",
ST.PhysicalTime: "CIRCLE", ST.PhysicalTime: "CIRCLE",
ST.PhysicalAngle: "DIAMOND", ST.PhysicalAngle: "CIRCLE",
ST.PhysicalLength: "SQUARE", ST.PhysicalLength: "CIRCLE",
ST.PhysicalArea: "SQUARE", ST.PhysicalArea: "CIRCLE",
ST.PhysicalVolume: "SQUARE", ST.PhysicalVolume: "CIRCLE",
ST.PhysicalPoint2D: "DIAMOND", ST.PhysicalPoint2D: "CIRCLE",
ST.PhysicalPoint3D: "DIAMOND", ST.PhysicalPoint3D: "CIRCLE",
ST.PhysicalSize2D: "SQUARE", ST.PhysicalSize2D: "CIRCLE",
ST.PhysicalSize3D: "SQUARE", ST.PhysicalSize3D: "CIRCLE",
ST.PhysicalMass: "CIRCLE", ST.PhysicalMass: "CIRCLE",
ST.PhysicalSpeed: "CIRCLE", ST.PhysicalSpeed: "CIRCLE",
ST.PhysicalAccelScalar: "CIRCLE", ST.PhysicalAccelScalar: "CIRCLE",
ST.PhysicalForceScalar: "CIRCLE", ST.PhysicalForceScalar: "CIRCLE",
ST.PhysicalAccel3D: "SQUARE_DOT", ST.PhysicalAccel3D: "CIRCLE",
ST.PhysicalForce3D: "SQUARE_DOT", ST.PhysicalForce3D: "CIRCLE",
ST.PhysicalPol: "DIAMOND", ST.PhysicalPol: "CIRCLE",
ST.PhysicalFreq: "CIRCLE", ST.PhysicalFreq: "CIRCLE",
# Blender # Blender
ST.BlenderObject: "SQUARE", ST.BlenderObject: "DIAMOND",
ST.BlenderCollection: "SQUARE", ST.BlenderCollection: "DIAMOND",
ST.BlenderImage: "DIAMOND", ST.BlenderImage: "DIAMOND",
ST.BlenderGeoNodes: "DIAMOND", ST.BlenderGeoNodes: "DIAMOND",
ST.BlenderText: "SQUARE", ST.BlenderText: "DIAMOND",
# Maxwell # Maxwell
ST.MaxwellSource: "CIRCLE", ST.MaxwellSource: "CIRCLE",
ST.MaxwellTemporalShape: "CIRCLE", ST.MaxwellTemporalShape: "CIRCLE",
ST.MaxwellMedium: "CIRCLE", ST.MaxwellMedium: "CIRCLE",
ST.MaxwellMediumNonLinearity: "CIRCLE", ST.MaxwellMediumNonLinearity: "CIRCLE",
ST.MaxwellStructure: "SQUARE", ST.MaxwellStructure: "CIRCLE",
ST.MaxwellBoundConds: "SQUARE", ST.MaxwellBoundConds: "CIRCLE",
ST.MaxwellBoundCond: "DIAMOND", ST.MaxwellBoundCond: "CIRCLE",
ST.MaxwellMonitor: "CIRCLE", ST.MaxwellMonitor: "CIRCLE",
ST.MaxwellFDTDSim: "SQUARE", ST.MaxwellFDTDSim: "CIRCLE",
ST.MaxwellSimGrid: "SQUARE", ST.MaxwellFDTDSimData: "CIRCLE",
ST.MaxwellSimGridAxis: "DIAMOND", ST.MaxwellSimGrid: "CIRCLE",
ST.MaxwellSimDomain: "SQUARE", ST.MaxwellSimGridAxis: "CIRCLE",
ST.MaxwellSimDomain: "CIRCLE",
# Tidy3D # Tidy3D
ST.Tidy3DCloudTask: "CIRCLE", ST.Tidy3DCloudTask: "DIAMOND",
} }

View File

@ -53,6 +53,7 @@ class SocketType(BlenderTypeEnum):
MaxwellMonitor = enum.auto() MaxwellMonitor = enum.auto()
MaxwellFDTDSim = enum.auto() MaxwellFDTDSim = enum.auto()
MaxwellFDTDSimData = enum.auto()
MaxwellSimDomain = enum.auto() MaxwellSimDomain = enum.auto()
MaxwellSimGrid = enum.auto() MaxwellSimGrid = enum.auto()
MaxwellSimGridAxis = enum.auto() MaxwellSimGridAxis = enum.auto()

View File

@ -8,7 +8,8 @@ from . import structures
#from . import bounds #from . import bounds
from . import monitors from . import monitors
from . import simulations from . import simulations
#from . import utilities from . import utilities
from . import viz
BL_REGISTER = [ BL_REGISTER = [
#*kitchen_sink.BL_REGISTER, #*kitchen_sink.BL_REGISTER,
@ -20,7 +21,8 @@ BL_REGISTER = [
# *bounds.BL_REGISTER, # *bounds.BL_REGISTER,
*monitors.BL_REGISTER, *monitors.BL_REGISTER,
*simulations.BL_REGISTER, *simulations.BL_REGISTER,
# *utilities.BL_REGISTER, *utilities.BL_REGISTER,
*viz.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
#**kitchen_sink.BL_NODES, #**kitchen_sink.BL_NODES,
@ -32,5 +34,6 @@ BL_NODES = {
# **bounds.BL_NODES, # **bounds.BL_NODES,
**monitors.BL_NODES, **monitors.BL_NODES,
**simulations.BL_NODES, **simulations.BL_NODES,
# **utilities.BL_NODES, **utilities.BL_NODES,
**viz.BL_NODES,
} }

View File

@ -14,6 +14,12 @@ from .. import sockets
CACHE: dict[str, typ.Any] = {} ## By Instance UUID CACHE: dict[str, typ.Any] = {} ## By Instance UUID
## NOTE: CACHE does not persist between file loads. ## NOTE: CACHE does not persist between file loads.
_DEFAULT_LOOSE_SOCKET_SER = json.dumps({
"socket_names": [],
"socket_def_names": [],
"models": [],
})
class MaxwellSimNode(bpy.types.Node): class MaxwellSimNode(bpy.types.Node):
# Fundamentals # Fundamentals
node_type: ct.NodeType node_type: ct.NodeType
@ -115,6 +121,14 @@ class MaxwellSimNode(bpy.types.Node):
"_callback_type" "_callback_type"
) and method._callback_type == "on_show_plot" ) and method._callback_type == "on_show_plot"
} }
cls._on_init = {
method
for attr_name in dir(cls)
if hasattr(
method := getattr(cls, attr_name),
"_callback_type"
) and method._callback_type == "on_init"
}
# Setup Socket Set Dropdown # Setup Socket Set Dropdown
if not len(cls.input_socket_sets) + len(cls.output_socket_sets) > 0: if not len(cls.input_socket_sets) + len(cls.output_socket_sets) > 0:
@ -151,7 +165,7 @@ class MaxwellSimNode(bpy.types.Node):
) )
], ],
default=socket_set_names[0], default=socket_set_names[0],
update=(lambda self, _: self.sync_sockets()), update=lambda self, context: self.sync_active_socket_set(context),
) )
# Setup Preset Dropdown # Setup Preset Dropdown
@ -179,6 +193,10 @@ class MaxwellSimNode(bpy.types.Node):
#################### ####################
# - Generic Properties # - Generic Properties
#################### ####################
def sync_active_socket_set(self, context):
self.sync_sockets()
self.sync_prop("active_socket_set", context)
def sync_sim_node_name(self, context): def sync_sim_node_name(self, context):
if (mobjs := CACHE[self.instance_id].get("managed_objs")) is None: if (mobjs := CACHE[self.instance_id].get("managed_objs")) is None:
return return
@ -276,11 +294,6 @@ class MaxwellSimNode(bpy.types.Node):
#################### ####################
# - Loose Sockets # - Loose Sockets
#################### ####################
_DEFAULT_LOOSE_SOCKET_SER = json.dumps({
"socket_names": [],
"socket_def_names": [],
"models": [],
})
# Loose Sockets # Loose Sockets
## Only Blender props persist as instance data ## Only Blender props persist as instance data
ser_loose_input_sockets: bpy.props.StringProperty( ser_loose_input_sockets: bpy.props.StringProperty(
@ -336,7 +349,8 @@ class MaxwellSimNode(bpy.types.Node):
def loose_input_sockets( def loose_input_sockets(
self, value: dict[str, ct.schemas.SocketDef], self, value: dict[str, ct.schemas.SocketDef],
) -> None: ) -> None:
self.ser_loose_input_sockets = self._ser_loose_sockets(value) if not value: self.ser_loose_input_sockets = _DEFAULT_LOOSE_SOCKET_SER
else: self.ser_loose_input_sockets = self._ser_loose_sockets(value)
# Synchronize Sockets # Synchronize Sockets
self.sync_sockets() self.sync_sockets()
@ -346,7 +360,8 @@ class MaxwellSimNode(bpy.types.Node):
def loose_output_sockets( def loose_output_sockets(
self, value: dict[str, ct.schemas.SocketDef], self, value: dict[str, ct.schemas.SocketDef],
) -> None: ) -> None:
self.ser_loose_output_sockets = self._ser_loose_sockets(value) if not value: self.ser_loose_output_sockets = _DEFAULT_LOOSE_SOCKET_SER
else: self.ser_loose_output_sockets = self._ser_loose_sockets(value)
# Synchronize Sockets # Synchronize Sockets
self.sync_sockets() self.sync_sockets()
@ -457,7 +472,7 @@ class MaxwellSimNode(bpy.types.Node):
col = layout.column(align=False) col = layout.column(align=False)
if self.use_sim_node_name: if self.use_sim_node_name:
row = col.row(align=True) row = col.row(align=True)
row.label(text="", icon="EVENT_N") row.label(text="", icon="FILE_TEXT")
row.prop(self, "sim_node_name", text="") row.prop(self, "sim_node_name", text="")
# Draw Name # Draw Name
@ -638,8 +653,11 @@ class MaxwellSimNode(bpy.types.Node):
self.sync_sockets() self.sync_sockets()
# Apply Default Preset # Apply Default Preset
if self.active_preset: if self.active_preset: self.sync_active_preset()
self.sync_active_preset()
# Callbacks
for method in self._on_init:
method(self)
def update(self) -> None: def update(self) -> None:
pass pass
@ -652,6 +670,18 @@ class MaxwellSimNode(bpy.types.Node):
CACHE[self.instance_id] = {} CACHE[self.instance_id] = {}
node_tree = self.id_data node_tree = self.id_data
# Unlock
## This is one approach to the "deleted locked nodes" problem.
## Essentially, deleting a locked node will unlock along input chain.
## It also counts if any of the input sockets are linked and locked.
## Thus, we prevent "dangling locks".
## TODO: Don't even allow deleting a locked node.
if self.locked or any(
bl_socket.is_linked and bl_socket.locked
for bl_socket in self.inputs.values()
):
self.trigger_action("disable_lock")
# Free Managed Objects # Free Managed Objects
for managed_obj in self.managed_objs.values(): for managed_obj in self.managed_objs.values():
managed_obj.free() managed_obj.free()
@ -674,6 +704,7 @@ def chain_event_decorator(
"on_value_changed", "on_value_changed",
"on_show_preview", "on_show_preview",
"on_show_plot", "on_show_plot",
"on_init",
], ],
index_by: typ.Any | None = None, index_by: typ.Any | None = None,
extra_data: dict[str, typ.Any] | None = None, extra_data: dict[str, typ.Any] | None = None,
@ -938,3 +969,30 @@ def on_show_plot(
managed_objs=managed_objs, managed_objs=managed_objs,
req_params=req_params, req_params=req_params,
) )
def on_init(
kind: ct.DataFlowKind = ct.DataFlowKind.Value,
input_sockets: set[str] = set(),
output_sockets: set[str] = set(),
props: set[str] = set(),
managed_objs: set[str] = set(),
):
req_params = {"self"} | (
{"input_sockets"} if input_sockets else set()
) | (
{"output_sockets"} if output_sockets else set()
) | (
{"props"} if props else set()
) | (
{"managed_objs"} if managed_objs else set()
)
return chain_event_decorator(
callback_type="on_init",
kind=kind,
input_sockets=input_sockets,
output_sockets=output_sockets,
props=props,
managed_objs=managed_objs,
req_params=req_params,
)

View File

@ -3,6 +3,7 @@ import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import scipy as sc import scipy as sc
from .....utils import extra_sympy_units as spux
from ... import contracts as ct from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base from .. import base
@ -18,16 +19,31 @@ class WaveConstantNode(base.MaxwellSimNode):
bl_label = "Wave Constant" bl_label = "Wave Constant"
input_socket_sets = { input_socket_sets = {
# Single
"Vacuum WL": { "Vacuum WL": {
"WL": sockets.PhysicalLengthSocketDef(), "WL": sockets.PhysicalLengthSocketDef(
default_value=500*spu.nm,
default_unit=spu.nm,
),
}, },
"Frequency": { "Frequency": {
"Freq": sockets.PhysicalFreqSocketDef(), "Freq": sockets.PhysicalFreqSocketDef(
default_value=500*spux.THz,
default_unit=spux.THz,
),
},
# Listy
"Vacuum WLs": {
"WLs": sockets.PhysicalLengthSocketDef(
is_list=True,
),
},
"Frequencies": {
"Freqs": sockets.PhysicalFreqSocketDef(
is_list=True,
),
}, },
}
output_sockets = {
"WL": sockets.PhysicalLengthSocketDef(),
"Freq": sockets.PhysicalFreqSocketDef(),
} }
#################### ####################
@ -35,33 +51,107 @@ class WaveConstantNode(base.MaxwellSimNode):
#################### ####################
@base.computes_output_socket( @base.computes_output_socket(
"WL", "WL",
kind=ct.DataFlowKind.Value,
input_sockets={"WL", "Freq"}, input_sockets={"WL", "Freq"},
) )
def compute_vac_wl(self, input_sockets: dict) -> sp.Expr: def compute_vac_wl(self, input_sockets: dict) -> sp.Expr:
if (vac_wl := input_sockets["WL"]): if (vac_wl := input_sockets["WL"]) is not None:
return vac_wl return vac_wl
elif (freq := input_sockets["Freq"]):
elif (freq := input_sockets["Freq"]) is not None:
return spu.convert_to( return spu.convert_to(
VAC_SPEED_OF_LIGHT / freq, VAC_SPEED_OF_LIGHT / freq,
spu.meter, spu.meter,
) )
raise RuntimeError("Vac WL and Freq are both non-truthy") raise RuntimeError("Vac WL and Freq are both None")
@base.computes_output_socket( @base.computes_output_socket(
"Freq", "Freq",
input_sockets={"WL", "Freq"}, input_sockets={"WL", "Freq"},
) )
def compute_freq(self, input_sockets: dict) -> sp.Expr: def compute_freq(self, input_sockets: dict) -> sp.Expr:
if (vac_wl := input_sockets["WL"]): if (vac_wl := input_sockets["WL"]) is not None:
return spu.convert_to( return spu.convert_to(
VAC_SPEED_OF_LIGHT / vac_wl, VAC_SPEED_OF_LIGHT / vac_wl,
spu.hertz, spu.hertz,
) )
elif (freq := input_sockets["Freq"]): elif (freq := input_sockets["Freq"]) is not None:
return freq return freq
raise RuntimeError("Vac WL and Freq are both None")
####################
# - Listy Callbacks
####################
@base.computes_output_socket(
"WLs",
input_sockets={"WLs", "Freqs"},
)
def compute_vac_wls(self, input_sockets: dict) -> sp.Expr:
if (vac_wls := input_sockets["WLs"]) is not None:
return vac_wls
elif (freqs := input_sockets["Freqs"]) is not None:
return [
spu.convert_to(
VAC_SPEED_OF_LIGHT / freq,
spu.meter,
)
for freq in freqs
][::-1]
raise RuntimeError("Vac WLs and Freqs are both None")
@base.computes_output_socket(
"Freqs",
input_sockets={"WLs", "Freqs"},
)
def compute_freqs(self, input_sockets: dict) -> sp.Expr:
if (vac_wls := input_sockets["WLs"]) is not None:
return [
spu.convert_to(
VAC_SPEED_OF_LIGHT / vac_wl,
spu.hertz,
)
for vac_wl in vac_wls
][::-1]
elif (freqs := input_sockets["Freqs"]) is not None:
return freqs
raise RuntimeError("Vac WLs and Freqs are both None")
####################
# - Callbacks
####################
@base.on_value_changed(
prop_name="active_socket_set",
props={"active_socket_set"}
)
def on_value_changed__active_socket_set(self, props: dict):
# Singular: Normal Output Sockets
if props["active_socket_set"] in {"Vacuum WL", "Frequency"}:
self.loose_output_sockets = {}
self.loose_output_sockets = {
"Freq": sockets.PhysicalFreqSocketDef(),
"WL": sockets.PhysicalLengthSocketDef(),
}
# Plural: Listy Output Sockets
elif props["active_socket_set"] in {"Vacuum WLs", "Frequencies"}:
self.loose_output_sockets = {}
self.loose_output_sockets = {
"Freqs": sockets.PhysicalFreqSocketDef(is_list=True),
"WLs": sockets.PhysicalLengthSocketDef(is_list=True),
}
else:
msg = f"Active socket set invalid for wave constant: {props['active_socket_set']}"
raise RuntimeError(msg)
@base.on_init()
def on_init(self):
self.on_value_changed__active_socket_set()
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################
@ -70,6 +160,6 @@ BL_REGISTER = [
] ]
BL_NODES = { BL_NODES = {
ct.NodeType.WaveConstant: ( ct.NodeType.WaveConstant: (
ct.NodeCategory.MAXWELLSIM_INPUTS_CONSTANTS ct.NodeCategory.MAXWELLSIM_INPUTS
) )
} }

View File

@ -8,17 +8,14 @@ import bpy
import sympy as sp import sympy as sp
import pydantic as pyd import pydantic as pyd
import tidy3d as td import tidy3d as td
import tidy3d.web as _td_web import tidy3d.web as td_web
from ......utils.auth_td_web import g_td_web, is_td_web_authed from ......utils import tdcloud
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base from ... import base
@functools.cache CACHE = {}
def task_status(task_id: str):
task = _td_web.api.webapi.get_info(task_id)
return task.status
#################### ####################
# - Node # - Node
@ -29,42 +26,78 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
input_sockets = { input_sockets = {
"Cloud Task": sockets.Tidy3DCloudTaskSocketDef( "Cloud Task": sockets.Tidy3DCloudTaskSocketDef(
task_exists=True, should_exist=True,
), ),
"Cache Path": sockets.FilePathSocketDef(
default_path=Path("loaded_simulation.hdf5")
)
} }
output_sockets = {}
####################
# - UI
####################
def draw_info(self, context, layout): pass
#################### ####################
# - Output Methods # - Output Methods
#################### ####################
@base.computes_output_socket(
"FDTD Sim Data",
input_sockets={"Cloud Task", "Cache Path"},
)
def compute_fdtd_sim_data(self, input_sockets: dict) -> str:
global CACHE
if not CACHE.get(self.instance_id):
CACHE[self.instance_id] = {"fdtd_sim_data": None}
if CACHE[self.instance_id]["fdtd_sim_data"] is not None:
return CACHE[self.instance_id]["fdtd_sim_data"]
if not (
(cloud_task := input_sockets["Cloud Task"]) is not None
and isinstance(cloud_task, tdcloud.CloudTask)
and cloud_task.status == "success"
):
msg ="Won't attempt getting SimData"
raise RuntimeError(msg)
# Load the Simulation
cache_path = input_sockets["Cache Path"]
if cache_path is None:
print("CACHE PATH IS NONE WHY")
return ## I guess?
if cache_path.is_file():
sim_data = td.SimulationData.from_file(str(cache_path))
else:
sim_data = td_web.api.webapi.load(
cloud_task.task_id,
path=str(cache_path),
)
CACHE[self.instance_id]["fdtd_sim_data"] = sim_data
return sim_data
@base.computes_output_socket( @base.computes_output_socket(
"FDTD Sim", "FDTD Sim",
input_sockets={"Cloud Task"}, input_sockets={"Cloud Task"},
) )
def compute_cloud_task(self, input_sockets: dict) -> str: def compute_fdtd_sim(self, input_sockets: dict) -> str:
if not isinstance(task_id := input_sockets["Cloud Task"], str): if not isinstance(
msg ="Input task does not exist" cloud_task := input_sockets["Cloud Task"],
raise ValueError(msg) tdcloud.CloudTask
):
msg ="Input cloud task does not exist"
raise RuntimeError(msg)
# Load the Simulation # Load the Simulation
td_web = g_td_web(None) ## Presume already auth'ed
with tempfile.NamedTemporaryFile(delete=False) as f: with tempfile.NamedTemporaryFile(delete=False) as f:
_path_tmp = Path(f.name) _path_tmp = Path(f.name)
_path_tmp.rename(f.name + ".json") _path_tmp.rename(f.name + ".json")
path_tmp = Path(f.name + ".json") path_tmp = Path(f.name + ".json")
cloud_sim = _td_web.api.webapi.load_simulation( sim = td_web.api.webapi.load_simulation(
task_id, cloud_task.task_id,
path=str(path_tmp), path=str(path_tmp),
) ) ## TODO: Don't use td_web directly. Only through tdcloud
Path(path_tmp).unlink() Path(path_tmp).unlink()
return cloud_sim return sim
#################### ####################
# - Update # - Update
@ -74,22 +107,22 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
input_sockets={"Cloud Task"} input_sockets={"Cloud Task"}
) )
def on_value_changed__cloud_task(self, input_sockets: dict): def on_value_changed__cloud_task(self, input_sockets: dict):
task_status.cache_clear()
if ( if (
(task_id := input_sockets["Cloud Task"]) is None (cloud_task := input_sockets["Cloud Task"]) is not None
or isinstance(task_id, dict) and isinstance(cloud_task, tdcloud.CloudTask)
or task_status(task_id) != "success" and cloud_task.status == "success"
or not is_td_web_authed
): ):
if self.loose_output_sockets: self.loose_output_sockets = {} self.loose_output_sockets = {
"FDTD Sim Data": sockets.MaxwellFDTDSimDataSocketDef(),
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
}
return return
td_web = g_td_web(None) ## Presume already auth'ed self.loose_output_sockets = {}
self.loose_output_sockets = { @base.on_init()
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), def on_init(self):
"FDTD Sim Data": sockets.AnySocketDef(), self.on_value_changed__cloud_task()
}
#################### ####################

View File

@ -34,7 +34,7 @@ class LibraryMediumNode(base.MaxwellSimNode):
managed_obj_defs = { managed_obj_defs = {
"nk_plot": ct.schemas.ManagedObjDef( "nk_plot": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name), mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix="nkplot_", name_prefix="",
) )
} }

View File

@ -1,17 +1,17 @@
from . import eh_field_monitor from . import eh_field_monitor
#from . import field_power_flux_monitor from . import field_power_flux_monitor
#from . import epsilon_tensor_monitor #from . import epsilon_tensor_monitor
#from . import diffraction_monitor #from . import diffraction_monitor
BL_REGISTER = [ BL_REGISTER = [
*eh_field_monitor.BL_REGISTER, *eh_field_monitor.BL_REGISTER,
# *field_power_flux_monitor.BL_REGISTER, *field_power_flux_monitor.BL_REGISTER,
# *epsilon_tensor_monitor.BL_REGISTER, # *epsilon_tensor_monitor.BL_REGISTER,
# *diffraction_monitor.BL_REGISTER, # *diffraction_monitor.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**eh_field_monitor.BL_NODES, **eh_field_monitor.BL_NODES,
# **field_power_flux_monitor.BL_NODES, **field_power_flux_monitor.BL_NODES,
# **epsilon_tensor_monitor.BL_NODES, # **epsilon_tensor_monitor.BL_NODES,
# **diffraction_monitor.BL_NODES, # **diffraction_monitor.BL_NODES,
} }

View File

@ -26,18 +26,27 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets = {
"Rec Start": sockets.PhysicalTimeSocketDef(),
"Rec Stop": sockets.PhysicalTimeSocketDef(
default_value=200*spux.fs
),
"Center": sockets.PhysicalPoint3DSocketDef(), "Center": sockets.PhysicalPoint3DSocketDef(),
"Size": sockets.PhysicalSize3DSocketDef(), "Size": sockets.PhysicalSize3DSocketDef(),
"Samples/Space": sockets.Integer3DVectorSocketDef( "Samples/Space": sockets.Integer3DVectorSocketDef(
default_value=sp.Matrix([10, 10, 10]) default_value=sp.Matrix([10, 10, 10])
), ),
"Samples/Time": sockets.IntegerNumberSocketDef( }
default_value=100, input_socket_sets = {
), "Freq Domain": {
"Freqs": sockets.PhysicalFreqSocketDef(
is_list=True,
),
},
"Time Domain": {
"Rec Start": sockets.PhysicalTimeSocketDef(),
"Rec Stop": sockets.PhysicalTimeSocketDef(
default_value=200*spux.fs
),
"Samples/Time": sockets.IntegerNumberSocketDef(
default_value=100,
),
},
} }
output_sockets = { output_sockets = {
"Monitor": sockets.MaxwellMonitorSocketDef(), "Monitor": sockets.MaxwellMonitorSocketDef(),
@ -70,33 +79,49 @@ class EHFieldMonitorNode(base.MaxwellSimNode):
"Monitor", "Monitor",
input_sockets={ input_sockets={
"Rec Start", "Rec Stop", "Center", "Size", "Samples/Space", "Rec Start", "Rec Stop", "Center", "Size", "Samples/Space",
"Samples/Time", "Samples/Time", "Freqs",
}, },
props={"sim_node_name"} props={"active_socket_set", "sim_node_name"}
) )
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor: def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
_rec_start = input_sockets["Rec Start"]
_rec_stop = input_sockets["Rec Stop"]
_center = input_sockets["Center"] _center = input_sockets["Center"]
_size = input_sockets["Size"] _size = input_sockets["Size"]
_samples_space = input_sockets["Samples/Space"] _samples_space = input_sockets["Samples/Space"]
samples_time = input_sockets["Samples/Time"]
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
center = tuple(spu.convert_to(_center, spu.um) / spu.um) center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um) size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space) samples_space = tuple(_samples_space)
return td.FieldTimeMonitor( if props["active_socket_set"] == "Freq Domain":
center=center, freqs = input_sockets["Freqs"]
size=size,
name=props["sim_node_name"], return td.FieldMonitor(
start=rec_start, center=center,
stop=rec_stop, size=size,
interval=samples_time, name=props["sim_node_name"],
interval_space=samples_space, interval_space=samples_space,
) freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz)
for freq in freqs
],
)
else: ## Time Domain
_rec_start = input_sockets["Rec Start"]
_rec_stop = input_sockets["Rec Stop"]
samples_time = input_sockets["Samples/Time"]
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
return td.FieldTimeMonitor(
center=center,
size=size,
name=props["sim_node_name"],
start=rec_start,
stop=rec_stop,
interval=samples_time,
interval_space=samples_space,
)
#################### ####################
# - Preview - Changes to Input Sockets # - Preview - Changes to Input Sockets

View File

@ -1,6 +1,201 @@
import typing as typ
import functools
import bpy
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import numpy as np
import scipy as sc
from .....utils import analyze_geonodes
from .....utils import extra_sympy_units as spux
from ... import contracts as ct
from ... import sockets
from ... import managed_objs
from .. import base
GEONODES_MONITOR_BOX = "monitor_flux_box"
class FieldPowerFluxMonitorNode(base.MaxwellSimNode):
node_type = ct.NodeType.FieldPowerFluxMonitor
bl_label = "Field Power Flux Monitor"
use_sim_node_name = True
####################
# - Sockets
####################
input_sockets = {
"Center": sockets.PhysicalPoint3DSocketDef(),
"Size": sockets.PhysicalSize3DSocketDef(),
"Samples/Space": sockets.Integer3DVectorSocketDef(
default_value=sp.Matrix([10, 10, 10])
),
"Direction": sockets.BoolSocketDef(),
}
input_socket_sets = {
"Freq Domain": {
"Freqs": sockets.PhysicalFreqSocketDef(
is_list=True,
),
},
"Time Domain": {
"Rec Start": sockets.PhysicalTimeSocketDef(),
"Rec Stop": sockets.PhysicalTimeSocketDef(
default_value=200*spux.fs
),
"Samples/Time": sockets.IntegerNumberSocketDef(
default_value=100,
),
},
}
output_sockets = {
"Monitor": sockets.MaxwellMonitorSocketDef(),
}
managed_obj_defs = {
"monitor_box": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="",
)
}
####################
# - Properties
####################
####################
# - UI
####################
def draw_props(self, context, layout):
pass
def draw_info(self, context, col):
pass
####################
# - Output Sockets
####################
@base.computes_output_socket(
"Monitor",
input_sockets={
"Rec Start", "Rec Stop", "Center", "Size", "Samples/Space",
"Samples/Time", "Freqs", "Direction",
},
props={"active_socket_set", "sim_node_name"}
)
def compute_monitor(self, input_sockets: dict, props: dict) -> td.FieldTimeMonitor:
_center = input_sockets["Center"]
_size = input_sockets["Size"]
_samples_space = input_sockets["Samples/Space"]
center = tuple(spu.convert_to(_center, spu.um) / spu.um)
size = tuple(spu.convert_to(_size, spu.um) / spu.um)
samples_space = tuple(_samples_space)
direction = "+" if input_sockets["Direction"] else "-"
if props["active_socket_set"] == "Freq Domain":
freqs = input_sockets["Freqs"]
return td.FluxMonitor(
center=center,
size=size,
name=props["sim_node_name"],
interval_space=samples_space,
freqs=[
float(spu.convert_to(freq, spu.hertz) / spu.hertz)
for freq in freqs
],
normal_dir=direction,
)
else: ## Time Domain
_rec_start = input_sockets["Rec Start"]
_rec_stop = input_sockets["Rec Stop"]
samples_time = input_sockets["Samples/Time"]
rec_start = spu.convert_to(_rec_start, spu.second) / spu.second
rec_stop = spu.convert_to(_rec_stop, spu.second) / spu.second
return td.FieldTimeMonitor(
center=center,
size=size,
name=props["sim_node_name"],
start=rec_start,
stop=rec_stop,
interval=samples_time,
interval_space=samples_space,
)
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
socket_name={"Center", "Size"},
input_sockets={"Center", "Size", "Direction"},
managed_objs={"monitor_box"},
)
def on_value_changed__center_size(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple([
float(el)
for el in spu.convert_to(_center, spu.um) / spu.um
])
_size = input_sockets["Size"]
size = tuple([
float(el)
for el in spu.convert_to(_size, spu.um) / spu.um
])
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_MONITOR_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc="INPUT"
)
# Sync Modifier Inputs
managed_objs["monitor_box"].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface["Size"].identifier: size,
geonodes_interface["Direction"].identifier: input_sockets["Direction"],
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}
)
# Sync Object Position
managed_objs["monitor_box"].bl_object("MESH").location = center
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={"monitor_box"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["monitor_box"].show_preview("MESH")
self.on_value_changed__center_size()
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################
BL_REGISTER = [] BL_REGISTER = [
BL_NODES = {} FieldPowerFluxMonitorNode,
]
BL_NODES = {
ct.NodeType.FieldPowerFluxMonitor: (
ct.NodeCategory.MAXWELLSIM_MONITORS
)
}

View File

@ -11,232 +11,185 @@ import pydantic as pyd
import tidy3d as td import tidy3d as td
import tidy3d.web as _td_web import tidy3d.web as _td_web
from ......utils.auth_td_web import g_td_web, is_td_web_authed from ......utils import tdcloud
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from ... import base from ... import base
####################
# - Task Getters
####################
## TODO: We should probably refactor this setup.
@functools.cache
def estimated_task_cost(task_id: str):
return _td_web.api.webapi.estimate_cost(task_id)
@functools.cache
def billed_task_cost(task_id: str):
return _td_web.api.webapi.real_cost(task_id)
@functools.cache
def task_status(task_id: str):
task = _td_web.api.webapi.get_info(task_id)
return task.status
####################
# - Progress Timer
####################
## TODO: We should probably refactor this too.
class Tidy3DTaskStatusModalOperator(bpy.types.Operator):
bl_idname = "blender_maxwell.tidy_3d_task_status_modal_operator"
bl_label = "Tidy3D Task Status Modal Operator"
_timer = None
_task_id = None
_node = None
_status = None
_reported_done = False
def modal(self, context, event):
# Retrieve New Status
task_status.cache_clear()
new_status = task_status(self._task_id)
if new_status != self._status:
task_status.cache_clear()
self._status = new_status
# Check Done Status
if self._status in {"success", "error"}:
# Report Done
if not self._reported_done:
self._node.trigger_action("value_changed")
self._reported_done = True
# Finish when Billing is Known
if not billed_task_cost(self._task_id):
billed_task_cost.cache_clear()
else:
return {'FINISHED'}
return {'PASS_THROUGH'}
def execute(self, context):
node = context.node
wm = context.window_manager
self._timer = wm.event_timer_add(2.0, window=context.window)
self._task_id = node.uploaded_task_id
self._node = node
self._status = task_status(self._task_id)
wm.modal_handler_add(self)
return {'RUNNING_MODAL'}
#################### ####################
# - Web Uploader / Loader / Runner / Releaser # - Web Uploader / Loader / Runner / Releaser
#################### ####################
## TODO: We should probably refactor this too. class UploadSimulation(bpy.types.Operator):
class Tidy3DWebUploadOperator(bpy.types.Operator): bl_idname = "blender_maxwell.nodes__upload_simulation"
bl_idname = "blender_maxwell.tidy_3d_web_upload_operator" bl_label = "Upload Tidy3D Simulation"
bl_label = "Tidy3D Web Upload Operator"
bl_description = "Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud" bl_description = "Upload the attached (locked) simulation, such that it is ready to run on the Tidy3D cloud"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
space = context.space_data
return ( return (
space.type == 'NODE_EDITOR' hasattr(context, "node")
and space.node_tree is not None and hasattr(context.node, "node_type")
and space.node_tree.bl_idname == "MaxwellSimTreeType" and context.node.node_type == ct.NodeType.Tidy3DWebExporter
and is_td_web_authed()
and hasattr(context, "node")
and context.node.lock_tree and context.node.lock_tree
and tdcloud.IS_AUTHENTICATED
and not context.node.tracked_task_id
and context.node.inputs["FDTD Sim"].is_linked
) )
def execute(self, context): def execute(self, context):
node = context.node node = context.node
node.web_upload() node.upload_sim()
return {'FINISHED'} return {'FINISHED'}
class Tidy3DLoadUploadedOperator(bpy.types.Operator): class RunSimulation(bpy.types.Operator):
bl_idname = "blender_maxwell.tidy_3d_load_uploaded_operator" bl_idname = "blender_maxwell.nodes__run_simulation"
bl_label = "Tidy3D Load Uploaded Operator" bl_label = "Run Tracked Tidy3D Sim"
bl_description = "Load an already-uploaded simulation, as selected in the dropdown of the 'Cloud Task' socket" bl_description = "Run the currently tracked simulation task"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
space = context.space_data
return ( return (
space.type == 'NODE_EDITOR' hasattr(context, "node")
and space.node_tree is not None and hasattr(context.node, "node_type")
and space.node_tree.bl_idname == "MaxwellSimTreeType" and context.node.node_type == ct.NodeType.Tidy3DWebExporter
and is_td_web_authed()
and hasattr(context, "node") and tdcloud.IS_AUTHENTICATED
and context.node.lock_tree and context.node.tracked_task_id
and (task_info := tdcloud.TidyCloudTasks.task_info(
context.node.tracked_task_id
)) is not None
and task_info.status == "draft"
) )
def execute(self, context): def execute(self, context):
node = context.node node = context.node
node.load_uploaded_task() node.run_tracked_task()
# Load Simulation to Compare
## Load Local Sim
local_sim = node._compute_input("FDTD Sim")
## Load Cloud Sim
task_id = node.compute_output("Cloud Task")
with tempfile.NamedTemporaryFile(delete=False) as f:
_path_tmp = Path(f.name)
_path_tmp.rename(f.name + ".json")
path_tmp = Path(f.name + ".json")
cloud_sim = _td_web.api.webapi.load_simulation(task_id, path=str(path_tmp))
Path(path_tmp).unlink()
## Compare
if local_sim != cloud_sim:
node.release_uploaded_task()
msg = "Loaded simulation doesn't match input simulation"
raise ValueError(msg)
return {'FINISHED'} return {'FINISHED'}
class RunUploadedTidy3DSim(bpy.types.Operator): class ReloadTrackedTask(bpy.types.Operator):
bl_idname = "blender_maxwell.run_uploaded_tidy_3d_sim" bl_idname = "blender_maxwell.nodes__reload_tracked_task"
bl_label = "Run Uploaded Tidy3D Sim" bl_label = "Reload Tracked Tidy3D Cloud Task"
bl_description = "Run the currently uploaded (and loaded) simulation" bl_description = "Reload the currently tracked simulation task"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
space = context.space_data
return ( return (
space.type == 'NODE_EDITOR' hasattr(context, "node")
and space.node_tree is not None and hasattr(context.node, "node_type")
and space.node_tree.bl_idname == "MaxwellSimTreeType" and context.node.node_type == ct.NodeType.Tidy3DWebExporter
and is_td_web_authed()
and hasattr(context, "node") and tdcloud.IS_AUTHENTICATED
and context.node.lock_tree and context.node.tracked_task_id
and context.node.uploaded_task_id
and task_status(context.node.uploaded_task_id) == "draft"
) )
def execute(self, context): def execute(self, context):
node = context.node node = context.node
node.run_uploaded_task() if (
bpy.ops.blender_maxwell.tidy_3d_task_status_modal_operator() cloud_task := tdcloud.TidyCloudTasks.task(node.tracked_task_id)
) is None:
msg = "Tried to reload tracked task, but it doesn't exist"
raise RuntimeError(msg)
cloud_task = tdcloud.TidyCloudTasks.update_task(cloud_task)
return {'FINISHED'} return {'FINISHED'}
class ReleaseTidy3DExportOperator(bpy.types.Operator): class EstCostTrackedTask(bpy.types.Operator):
bl_idname = "blender_maxwell.release_tidy_3d_export_operator" bl_idname = "blender_maxwell.nodes__est_cost_tracked_task"
bl_label = "Release Tidy3D Export Operator" bl_label = "Est Cost of Tracked Tidy3D Cloud Task"
bl_description = "Reload the currently tracked simulation task"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
space = context.space_data
return ( return (
space.type == 'NODE_EDITOR' hasattr(context, "node")
and space.node_tree is not None and hasattr(context.node, "node_type")
and space.node_tree.bl_idname == "MaxwellSimTreeType" and context.node.node_type == ct.NodeType.Tidy3DWebExporter
and is_td_web_authed()
and hasattr(context, "node") and tdcloud.IS_AUTHENTICATED
and context.node.lock_tree and context.node.tracked_task_id
and context.node.uploaded_task_id
) )
def execute(self, context): def execute(self, context):
node = context.node node = context.node
node.release_uploaded_task() if (
task_info := tdcloud.TidyCloudTasks.task_info(context.node.tracked_task_id)
) is None:
msg = "Tried to estimate cost of tracked task, but it doesn't exist"
raise RuntimeError(msg)
node.cache_est_cost = task_info.cost_est()
return {'FINISHED'}
class ReleaseTrackedTask(bpy.types.Operator):
bl_idname = "blender_maxwell.nodes__release_tracked_task"
bl_label = "Release Tracked Tidy3D Cloud Task"
bl_description = "Release the currently tracked simulation task"
@classmethod
def poll(cls, context):
return (
hasattr(context, "node")
and hasattr(context.node, "node_type")
and context.node.node_type == ct.NodeType.Tidy3DWebExporter
#and tdcloud.IS_AUTHENTICATED
and context.node.tracked_task_id
)
def execute(self, context):
node = context.node
node.tracked_task_id = ""
return {'FINISHED'} return {'FINISHED'}
#################### ####################
# - Web Exporter Node # - Node
#################### ####################
class Tidy3DWebExporterNode(base.MaxwellSimNode): class Tidy3DWebExporterNode(base.MaxwellSimNode):
node_type = ct.NodeType.Tidy3DWebExporter node_type = ct.NodeType.Tidy3DWebExporter
bl_label = "Tidy3DWebExporter" bl_label = "Tidy3D Web Exporter"
input_sockets = { input_sockets = {
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), "FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
"Cloud Task": sockets.Tidy3DCloudTaskSocketDef( "Cloud Task": sockets.Tidy3DCloudTaskSocketDef(
task_exists=False, should_exist=False,
),
}
output_sockets = {
"Cloud Task": sockets.Tidy3DCloudTaskSocketDef(
task_exists=True,
), ),
} }
####################
# - Properties
####################
lock_tree: bpy.props.BoolProperty( lock_tree: bpy.props.BoolProperty(
name="Whether to lock the attached tree", name="Whether to lock the attached tree",
description="Whether or not to lock the attached tree", description="Whether or not to lock the attached tree",
default=False, default=False,
update=(lambda self, context: self.sync_lock_tree(context)), update=lambda self, context: self.sync_lock_tree(context),
) )
uploaded_task_id: bpy.props.StringProperty( tracked_task_id: bpy.props.StringProperty(
name="Uploaded Task ID", name="Tracked Task ID",
description="The uploaded task ID", description="The currently tracked task ID",
default="", 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,
) )
#################### ####################
# - Sync Methods # - Sync Methods
#################### ####################
def sync_lock_tree(self, context): def sync_lock_tree(self, context):
node_tree = self.id_data
if self.lock_tree: if self.lock_tree:
self.trigger_action("enable_lock") self.trigger_action("enable_lock")
self.locked = False self.locked = False
@ -247,106 +200,200 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
else: else:
self.trigger_action("disable_lock") self.trigger_action("disable_lock")
self.sync_prop("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"].sync_prepare_new_task()
self.inputs["Cloud Task"].locked = False
self.sync_prop("tracked_task_id", context)
#################### ####################
# - Output Socket Callbacks # - Output Socket Callbacks
#################### ####################
def web_upload(self): def validate_sim(self):
if not (sim := self._compute_input("FDTD Sim")): if (sim := self._compute_input("FDTD Sim")) is None:
raise ValueError("Must attach simulation") msg = "Tried to validate simulation, but none is attached"
raise ValueError(msg)
if not (new_task_dict := self._compute_input("Cloud Task")): sim.validate_pre_upload(source_required = True)
raise ValueError("No valid cloud task defined")
td_web = g_td_web(None) ## Presume already auth'ed def upload_sim(self):
if (sim := self._compute_input("FDTD Sim")) is None:
msg = "Tried to upload simulation, but none is attached"
raise ValueError(msg)
self.uploaded_task_id = td_web.api.webapi.upload( if (
sim, (new_task := self._compute_input("Cloud Task")) is None
**new_task_dict, or isinstance(
new_task,
tdcloud.CloudTask,
)
):
msg = "Tried to upload simulation to new task, but existing task was selected"
raise ValueError(msg)
# Create Cloud Task
cloud_task = tdcloud.TidyCloudTasks.mk_task(
task_name=new_task[0],
cloud_folder=new_task[1],
sim=sim,
upload_progress_cb=lambda uploaded_bytes: None, ## TODO: Use!
verbose=True, verbose=True,
) )
self.inputs["Cloud Task"].sync_task_loaded(self.uploaded_task_id) # Declare to Cloud Task that it Exists Now
## This will change the UI to not allow free-text input.
## If the socket is linked, this errors.
self.inputs["Cloud Task"].sync_created_new_task(cloud_task)
def load_uploaded_task(self): # Track the Newly Uploaded Task ID
self.inputs["Cloud Task"].sync_task_loaded(None) self.tracked_task_id = cloud_task.task_id
self.uploaded_task_id = self._compute_input("Cloud Task")
self.trigger_action("value_changed") def run_tracked_task(self):
if (
cloud_task := tdcloud.TidyCloudTasks.task(self.tracked_task_id)
) is None:
msg = "Tried to run tracked task, but it doesn't exist"
raise RuntimeError(msg)
def run_uploaded_task(self): cloud_task.submit()
td_web = g_td_web(None) ## Presume already auth'ed tdcloud.TidyCloudTasks.update_task(cloud_task) ## TODO: Check that status is actually immediately updated.
td_web.api.webapi.start(self.uploaded_task_id)
self.trigger_action("value_changed")
def release_uploaded_task(self):
self.uploaded_task_id = ""
self.inputs["Cloud Task"].sync_task_released(specify_new_task=True)
self.trigger_action("value_changed")
#################### ####################
# - UI # - UI
#################### ####################
def draw_operators(self, context, layout): def draw_operators(self, context, layout):
is_authed = is_td_web_authed() # Row: Upload Sim Buttons
has_uploaded_task_id = bool(self.uploaded_task_id)
# Row: Run Simulation
row = layout.row(align=True) row = layout.row(align=True)
if has_uploaded_task_id: row.enabled = False
row.operator( row.operator(
Tidy3DWebUploadOperator.bl_idname, UploadSimulation.bl_idname,
text="Upload Sim", text="Upload",
) )
tree_lock_icon = "LOCKED" if self.lock_tree else "UNLOCKED" 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, "lock_tree", toggle=True, icon=tree_lock_icon, text="")
# Row: Run Simulation # Row: Run Sim Buttons
row = layout.row(align=True) row = layout.row(align=True)
if is_authed and has_uploaded_task_id:
run_sim_text = f"Run Sim (~{estimated_task_cost(self.uploaded_task_id):.3f} credits)"
else:
run_sim_text = f"Run Sim"
row.operator( row.operator(
RunUploadedTidy3DSim.bl_idname, RunSimulation.bl_idname,
text=run_sim_text, text="Run",
) )
if has_uploaded_task_id: if self.tracked_task_id:
tree_lock_icon = "LOOP_BACK" tree_lock_icon = "LOOP_BACK"
row.operator( row.operator(
ReleaseTidy3DExportOperator.bl_idname, ReleaseTrackedTask.bl_idname,
icon="LOOP_BACK", icon="LOOP_BACK",
text="", text="",
) )
else:
row.operator( def draw_info(self, context, layout):
Tidy3DLoadUploadedOperator.bl_idname, # Connection Info
icon="TRIA_UP_BAR", auth_icon = "CHECKBOX_HLT" if tdcloud.IS_AUTHENTICATED else "CHECKBOX_DEHLT"
text="", conn_icon = "CHECKBOX_HLT" if tdcloud.IS_ONLINE else "CHECKBOX_DEHLT"
row = layout.row()
row.alignment = "CENTER"
row.label(text="Cloud Status")
box = layout.box()
split = box.split(factor=0.85)
## Split: Left Column
col = split.column(align=False)
col.label(text="Authed")
col.label(text="Connected")
## Split: Right Column
col = split.column(align=False)
col.label(icon=auth_icon)
col.label(icon=conn_icon)
# Simulation Info
if self.inputs["FDTD Sim"].is_linked:
row = layout.row()
row.alignment = "CENTER"
row.label(text="Sim Info")
box = layout.box()
split = box.split(factor=0.4)
## Split: Left Column
col = split.column(align=False)
col.label(text="𝝨 Output")
## 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")
# Cloud Task Info
if self.tracked_task_id and tdcloud.IS_AUTHENTICATED:
task_info = tdcloud.TidyCloudTasks.task_info(
self.tracked_task_id
) )
if task_info is None: return
# Row: Simulation Progress ## Header
if is_authed and has_uploaded_task_id: row = layout.row()
progress = { row.alignment = "CENTER"
"draft": (0.0, "Waiting to Run..."), row.label(text="Task Info")
"initialized": (0.0, "Initializing..."),
"queued": (0.0, "Queued..."),
"preprocessing": (0.05, "Pre-processing..."),
"running": (0.2, "Running..."),
"postprocess": (0.85, "Post-processing..."),
"success": (1.0, f"Success (={billed_task_cost(self.uploaded_task_id)} credits)"),
"error": (1.0, f"Error (={billed_task_cost(self.uploaded_task_id)} credits)"),
}[task_status(self.uploaded_task_id)]
layout.separator() ## Progress Bar
row = layout.row(align=True) row = layout.row(align=True)
row.progress( row.progress(
factor=progress[0], factor=0.0,
type="BAR", type="BAR",
text=progress[1], text=f"Status: {task_info.status.capitalize()}",
) )
row.operator(
ReloadTrackedTask.bl_idname,
text="",
icon="FILE_REFRESH",
)
row.operator(
EstCostTrackedTask.bl_idname,
text="",
icon="SORTTIME",
)
## Information
box = layout.box()
split = box.split(factor=0.4)
## Split: Left Column
col = split.column(align=False)
col.label(text="Status")
col.label(text="Est. Cost")
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_real = f"{task_info.cost_real:.2f}" if task_info.cost_real is not None else "TBD"
col = split.column(align=False)
col.alignment = "RIGHT"
col.label(text=task_info.status.capitalize())
col.label(text=f"{cost_est} creds")
col.label(text=f"{cost_real} creds")
# Connection Information
#################### ####################
# - Output Methods # - Output Methods
@ -355,35 +402,40 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
"Cloud Task", "Cloud Task",
input_sockets={"Cloud Task"}, input_sockets={"Cloud Task"},
) )
def compute_cloud_task(self, input_sockets: dict) -> str | None: def compute_cloud_task(self, input_sockets: dict) -> tdcloud.CloudTask | None:
if self.uploaded_task_id: return self.uploaded_task_id if isinstance(
cloud_task := input_sockets["Cloud Task"],
tdcloud.CloudTask
):
return cloud_task
return None return None
#################### ####################
# - Update # - Output Methods
#################### ####################
@base.on_value_changed(socket_name="FDTD Sim") @base.on_value_changed(
def on_value_changed__fdtd_sim(self): socket_name="FDTD Sim",
estimated_task_cost.cache_clear() input_sockets={"FDTD Sim"},
task_status.cache_clear() )
billed_task_cost.cache_clear() 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
@base.on_value_changed(socket_name="Cloud Task") sim.validate_pre_upload(source_required = True)
def on_value_changed__cloud_task(self): self.cache_total_monitor_data = sum(sim.monitors_data_size.values())
estimated_task_cost.cache_clear()
task_status.cache_clear()
billed_task_cost.cache_clear()
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################
BL_REGISTER = [ BL_REGISTER = [
Tidy3DWebUploadOperator, UploadSimulation,
Tidy3DTaskStatusModalOperator, RunSimulation,
RunUploadedTidy3DSim, ReloadTrackedTask,
Tidy3DLoadUploadedOperator, EstCostTrackedTask,
ReleaseTidy3DExportOperator, ReleaseTrackedTask,
Tidy3DWebExporterNode, Tidy3DWebExporterNode,
] ]
BL_NODES = { BL_NODES = {

View File

@ -16,9 +16,15 @@ class FDTDSimNode(base.MaxwellSimNode):
input_sockets = { input_sockets = {
"Domain": sockets.MaxwellSimDomainSocketDef(), "Domain": sockets.MaxwellSimDomainSocketDef(),
"BCs": sockets.MaxwellBoundCondsSocketDef(), "BCs": sockets.MaxwellBoundCondsSocketDef(),
"Sources": sockets.MaxwellSourceSocketDef(), "Sources": sockets.MaxwellSourceSocketDef(
"Structures": sockets.MaxwellStructureSocketDef(), is_list=True,
"Monitors": sockets.MaxwellMonitorSocketDef(), ),
"Structures": sockets.MaxwellStructureSocketDef(
is_list=True,
),
"Monitors": sockets.MaxwellMonitorSocketDef(
is_list=True,
),
} }
output_sockets = { output_sockets = {
"FDTD Sim": sockets.MaxwellFDTDSimSocketDef(), "FDTD Sim": sockets.MaxwellFDTDSimSocketDef(),
@ -41,12 +47,12 @@ class FDTDSimNode(base.MaxwellSimNode):
bounds = input_sockets["BCs"] bounds = input_sockets["BCs"]
monitors = input_sockets["Monitors"] monitors = input_sockets["Monitors"]
if not isinstance(sources, list): #if not isinstance(sources, list):
sources = [sources] # sources = [sources]
if not isinstance(structures, list): #if not isinstance(structures, list):
structures = [structures] # structures = [structures]
if not isinstance(monitors, list): #if not isinstance(monitors, list):
monitors = [monitors] # monitors = [monitors]
return td.Simulation( return td.Simulation(
**sim_domain, ## run_time=, size=, grid=, medium= **sim_domain, ## run_time=, size=, grid=, medium=

View File

@ -32,7 +32,7 @@ class SimDomainNode(base.MaxwellSimNode):
managed_obj_defs = { managed_obj_defs = {
"domain_box": ct.schemas.ManagedObjDef( "domain_box": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="domain_box_", name_prefix="",
) )
} }

View File

@ -31,7 +31,7 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
managed_obj_defs = { managed_obj_defs = {
"sphere_empty": ct.schemas.ManagedObjDef( "sphere_empty": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="point_dipole_", name_prefix="",
) )
} }
@ -47,14 +47,20 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
("EZ", "Ez", "Electric field in z-dir"), ("EZ", "Ez", "Electric field in z-dir"),
], ],
default="EX", default="EX",
update=(lambda self, context: self.sync_prop("pol_axis")), update=(lambda self, context: self.sync_prop("pol_axis", context)),
) )
#################### ####################
# - UI # - UI
#################### ####################
def draw_props(self, context, layout): def draw_props(self, context, layout):
layout.prop(self, "pol_axis", text="Pol Axis") split = layout.split(factor=0.6)
col = split.column()
col.label(text="Pol Axis")
col = split.column()
col.prop(self, "pol_axis", text="")
#################### ####################
# - Output Socket Computation # - Output Socket Computation
@ -117,6 +123,7 @@ class PointDipoleSourceNode(base.MaxwellSimNode):
"EMPTY", "EMPTY",
empty_display_type="SPHERE", empty_display_type="SPHERE",
) )
managed_objs["sphere_empty"].bl_object("EMPTY").empty_display_size = 0.2

View File

@ -15,7 +15,6 @@ from ... import base
class GaussianPulseTemporalShapeNode(base.MaxwellSimNode): class GaussianPulseTemporalShapeNode(base.MaxwellSimNode):
node_type = ct.NodeType.GaussianPulseTemporalShape node_type = ct.NodeType.GaussianPulseTemporalShape
bl_label = "Gaussian Pulse Temporal Shape" bl_label = "Gaussian Pulse Temporal Shape"
#bl_icon = ... #bl_icon = ...

View File

@ -35,7 +35,7 @@ class GeoNodesStructureNode(base.MaxwellSimNode):
managed_obj_defs = { managed_obj_defs = {
"geometry": ct.schemas.ManagedObjDef( "geometry": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name), mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="geonodes_", name_prefix="",
) )
} }

View File

@ -1,14 +1,14 @@
from . import box_structure from . import box_structure
#from . import cylinder_structure #from . import cylinder_structure
#from . import sphere_structure from . import sphere_structure
BL_REGISTER = [ BL_REGISTER = [
*box_structure.BL_REGISTER, *box_structure.BL_REGISTER,
# *cylinder_structure.BL_REGISTER, # *cylinder_structure.BL_REGISTER,
# *sphere_structure.BL_REGISTER, *sphere_structure.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
**box_structure.BL_NODES, **box_structure.BL_NODES,
# **cylinder_structure.BL_NODES, # **cylinder_structure.BL_NODES,
# **sphere_structure.BL_NODES, **sphere_structure.BL_NODES,
} }

View File

@ -2,10 +2,16 @@ import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import bpy
from ......utils import analyze_geonodes
from .... import contracts as ct from .... import contracts as ct
from .... import sockets from .... import sockets
from .... import managed_objs
from ... import base from ... import base
GEONODES_STRUCTURE_BOX = "structure_box"
class BoxStructureNode(base.MaxwellSimNode): class BoxStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.BoxStructure node_type = ct.NodeType.BoxStructure
bl_label = "Box Structure" bl_label = "Box Structure"
@ -16,12 +22,21 @@ class BoxStructureNode(base.MaxwellSimNode):
input_sockets = { input_sockets = {
"Medium": sockets.MaxwellMediumSocketDef(), "Medium": sockets.MaxwellMediumSocketDef(),
"Center": sockets.PhysicalPoint3DSocketDef(), "Center": sockets.PhysicalPoint3DSocketDef(),
"Size": sockets.PhysicalSize3DSocketDef(), "Size": sockets.PhysicalSize3DSocketDef(
default_value=sp.Matrix([500, 500, 500]) * spu.nm
),
} }
output_sockets = { output_sockets = {
"Structure": sockets.MaxwellStructureSocketDef(), "Structure": sockets.MaxwellStructureSocketDef(),
} }
managed_obj_defs = {
"structure_box": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="",
)
}
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@ -45,6 +60,66 @@ class BoxStructureNode(base.MaxwellSimNode):
medium=medium, medium=medium,
) )
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
socket_name={"Center", "Size"},
input_sockets={"Center", "Size"},
managed_objs={"structure_box"},
)
def on_value_changed__center_size(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple([
float(el)
for el in spu.convert_to(_center, spu.um) / spu.um
])
_size = input_sockets["Size"]
size = tuple([
float(el)
for el in spu.convert_to(_size, spu.um) / spu.um
])
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_STRUCTURE_BOX]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc="INPUT"
)
# Sync Modifier Inputs
managed_objs["structure_box"].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface["Size"].identifier: size,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}
)
# Sync Object Position
managed_objs["structure_box"].bl_object("MESH").location = center
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={"structure_box"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["structure_box"].show_preview("MESH")
self.on_value_changed__center_size()
#################### ####################

View File

@ -2,43 +2,52 @@ import tidy3d as td
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
from .... import contracts import bpy
from ......utils import analyze_geonodes
from .... import contracts as ct
from .... import sockets from .... import sockets
from .... import managed_objs
from ... import base from ... import base
class SphereStructureNode(base.MaxwellSimTreeNode): GEONODES_STRUCTURE_SPHERE = "structure_sphere"
node_type = contracts.NodeType.SphereStructure
class SphereStructureNode(base.MaxwellSimNode):
node_type = ct.NodeType.SphereStructure
bl_label = "Sphere Structure" bl_label = "Sphere Structure"
#bl_icon = ...
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = { input_sockets = {
"medium": sockets.MaxwellMediumSocketDef( "Center": sockets.PhysicalPoint3DSocketDef(),
label="Medium", "Radius": sockets.PhysicalLengthSocketDef(
), default_value=150*spu.nm,
"center": sockets.PhysicalPoint3DSocketDef(
label="Center",
),
"radius": sockets.PhysicalLengthSocketDef(
label="Radius",
), ),
"Medium": sockets.MaxwellMediumSocketDef(),
} }
output_sockets = { output_sockets = {
"structure": sockets.MaxwellStructureSocketDef( "Structure": sockets.MaxwellStructureSocketDef(),
label="Structure", }
),
managed_obj_defs = {
"structure_sphere": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="",
)
} }
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@base.computes_output_socket("structure") @base.computes_output_socket(
def compute_simulation(self: contracts.NodeTypeProtocol) -> td.Box: "Structure",
medium = self.compute_input("medium") input_sockets={"Center", "Radius", "Medium"},
_center = self.compute_input("center") )
_radius = self.compute_input("radius") def compute_structure(self, input_sockets: dict) -> td.Box:
medium = input_sockets["Medium"]
_center = input_sockets["Center"]
_radius = input_sockets["Radius"]
center = tuple(spu.convert_to(_center, spu.um) / spu.um) center = tuple(spu.convert_to(_center, spu.um) / spu.um)
radius = spu.convert_to(_radius, spu.um) / spu.um radius = spu.convert_to(_radius, spu.um) / spu.um
@ -51,6 +60,63 @@ class SphereStructureNode(base.MaxwellSimTreeNode):
medium=medium, medium=medium,
) )
####################
# - Preview - Changes to Input Sockets
####################
@base.on_value_changed(
socket_name={"Center", "Radius"},
input_sockets={"Center", "Radius"},
managed_objs={"structure_sphere"},
)
def on_value_changed__center_radius(
self,
input_sockets: dict,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
_center = input_sockets["Center"]
center = tuple([
float(el)
for el in spu.convert_to(_center, spu.um) / spu.um
])
_radius = input_sockets["Radius"]
radius = float(spu.convert_to(_radius, spu.um) / spu.um)
## TODO: Preview unit system?? Presume um for now
# Retrieve Hard-Coded GeoNodes and Analyze Input
geo_nodes = bpy.data.node_groups[GEONODES_STRUCTURE_SPHERE]
geonodes_interface = analyze_geonodes.interface(
geo_nodes, direc="INPUT"
)
# Sync Modifier Inputs
managed_objs["structure_sphere"].sync_geonodes_modifier(
geonodes_node_group=geo_nodes,
geonodes_identifier_to_value={
geonodes_interface["Radius"].identifier: radius,
## TODO: Use 'bl_socket_map.value_to_bl`!
## - This accounts for auto-conversion, unit systems, etc. .
## - We could keep it in the node base class...
## - ...But it needs aligning with Blender, too. Hmm.
}
)
# Sync Object Position
managed_objs["structure_sphere"].bl_object("MESH").location = center
####################
# - Preview - Show Preview
####################
@base.on_show_preview(
managed_objs={"structure_sphere"},
)
def on_show_preview(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
):
managed_objs["structure_sphere"].show_preview("MESH")
self.on_value_changed__center_radius()
#################### ####################
@ -60,7 +126,7 @@ BL_REGISTER = [
SphereStructureNode, SphereStructureNode,
] ]
BL_NODES = { BL_NODES = {
contracts.NodeType.SphereStructure: ( ct.NodeType.SphereStructure: (
contracts.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES ct.NodeCategory.MAXWELLSIM_STRUCTURES_PRIMITIVES
) )
} }

View File

@ -1,23 +1,16 @@
#from . import math
from . import combine from . import combine
#from . import separate #from . import separate
from . import math
from . import operations
from . import converter
BL_REGISTER = [ BL_REGISTER = [
# *math.BL_REGISTER,
*combine.BL_REGISTER, *combine.BL_REGISTER,
#*separate.BL_REGISTER, #*separate.BL_REGISTER,
*converter.BL_REGISTER,
*math.BL_REGISTER,
*operations.BL_REGISTER,
] ]
BL_NODES = { BL_NODES = {
# **math.BL_NODES,
**combine.BL_NODES, **combine.BL_NODES,
#**separate.BL_NODES, #**separate.BL_NODES,
**converter.BL_NODES,
**math.BL_NODES,
**operations.BL_NODES,
} }

View File

@ -2,103 +2,167 @@ import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import scipy as sc import scipy as sc
from ... import contracts import bpy
from ... import contracts as ct
from ... import sockets from ... import sockets
from .. import base from .. import base
MAX_AMOUNT = 20
class CombineNode(base.MaxwellSimNode): class CombineNode(base.MaxwellSimNode):
node_type = contracts.NodeType.Combine node_type = ct.NodeType.Combine
bl_label = "Combine" bl_label = "Combine"
#bl_icon = ... #bl_icon = ...
#################### ####################
# - Sockets # - Sockets
#################### ####################
input_sockets = {}
input_socket_sets = { input_socket_sets = {
"real_3d_vector": { "Maxwell Sources": {},
f"x_{i}": sockets.RealNumberSocketDef( "Maxwell Structures": {},
label=f"x_{i}" "Maxwell Monitors": {},
) "Real 3D Vector": {
f"x_{i}": sockets.RealNumberSocketDef()
for i in range(3) for i in range(3)
}, },
"point_3d": { #"Point 3D": {
axis: sockets.PhysicalLengthSocketDef( # axis: sockets.PhysicalLengthSocketDef()
label=axis # for i, axis in zip(
) # range(3),
for i, axis in zip( # ["x", "y", "z"]
range(3), # )
["x", "y", "z"] #},
) #"Size 3D": {
}, # axis_key: sockets.PhysicalLengthSocketDef()
"size_3d": { # for i, axis_key, axis_label in zip(
axis_key: sockets.PhysicalLengthSocketDef( # range(3),
label=axis_label # ["x_size", "y_size", "z_size"],
) # ["X Size", "Y Size", "Z Size"],
for i, axis_key, axis_label in zip( # )
range(3), #},
["x_size", "y_size", "z_size"],
["X Size", "Y Size", "Z Size"],
)
},
} }
output_sockets = {}
output_socket_sets = { output_socket_sets = {
"real_3d_vector": { "Maxwell Sources": {
"real_3d_vector": sockets.Real3DVectorSocketDef( "Sources": sockets.MaxwellSourceSocketDef(
label="Real 3D Vector", is_list=True,
), ),
}, },
"point_3d": { "Maxwell Structures": {
"point_3d": sockets.PhysicalPoint3DSocketDef( "Structures": sockets.MaxwellStructureSocketDef(
label="3D Point", is_list=True,
), ),
}, },
"size_3d": { "Maxwell Monitors": {
"size_3d": sockets.PhysicalSize3DSocketDef( "Monitors": sockets.MaxwellMonitorSocketDef(
label="3D Size", 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.sync_prop("amount", context)
)
####################
# - Draw
####################
def draw_props(self, context, layout):
layout.prop(self, "amount", text="#")
#################### ####################
# - Output Socket Computation # - Output Socket Computation
#################### ####################
@base.computes_output_socket("real_3d_vector") @base.computes_output_socket(
def compute_real_3d_vector(self: contracts.NodeTypeProtocol) -> sp.Expr: "Real 3D Vector",
x1, x2, x3 = [ input_sockets={"x_0", "x_1", "x_2"}
self.compute_input(f"x_{i}") )
for i in range(3) def compute_real_3d_vector(self, input_sockets) -> sp.Expr:
return sp.Matrix([input_sockets[f"x_{i}"] for i in range(3)])
@base.computes_output_socket(
"Sources",
input_sockets={f"Source #{i}" for i in range(MAX_AMOUNT)},
props={"amount"},
)
def compute_sources(self, input_sockets, props) -> sp.Expr:
return [
input_sockets[f"Source #{i}"]
for i in range(props["amount"])
] ]
return (x1, x2, x3) @base.computes_output_socket(
"Structures",
@base.computes_output_socket("point_3d") input_sockets={f"Structure #{i}" for i in range(MAX_AMOUNT)},
def compute_point_3d(self: contracts.NodeTypeProtocol) -> sp.Expr: props={"amount"},
x, y, z = [ )
self.compute_input(axis) def compute_structures(self, input_sockets, props) -> sp.Expr:
#spu.convert_to( return [
# self.compute_input(axis), input_sockets[f"Structure #{i}"]
# spu.meter, for i in range(props["amount"])
#) / spu.meter
for axis in ["x", "y", "z"]
] ]
return sp.Matrix([x, y, z])# * spu.meter @base.computes_output_socket(
"Monitors",
@base.computes_output_socket("size_3d") input_sockets={f"Monitor #{i}" for i in range(MAX_AMOUNT)},
def compute_size_3d(self: contracts.NodeTypeProtocol) -> sp.Expr: props={"amount"},
x_size, y_size, z_size = [ )
self.compute_input(axis) def compute_monitors(self, input_sockets, props) -> sp.Expr:
#spu.convert_to( return [
# self.compute_input(axis), input_sockets[f"Monitor #{i}"]
# spu.meter, for i in range(props["amount"])
#) / spu.meter
for axis in ["x_size", "y_size", "z_size"]
] ]
return sp.Matrix([x_size, y_size, z_size])# * spu.meter
####################
# - Input Socket Compilation
####################
@base.on_value_changed(
prop_name="active_socket_set",
props={"active_socket_set", "amount"},
)
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 = {}
@base.on_value_changed(
prop_name="amount",
)
def on_value_changed__amount(self):
self.on_value_changed__active_socket_set()
@base.on_init()
def on_init(self):
self.on_value_changed__active_socket_set()
#################### ####################
@ -108,7 +172,7 @@ BL_REGISTER = [
CombineNode, CombineNode,
] ]
BL_NODES = { BL_NODES = {
contracts.NodeType.Combine: ( ct.NodeType.Combine: (
contracts.NodeCategory.MAXWELLSIM_UTILITIES ct.NodeCategory.MAXWELLSIM_UTILITIES
) )
} }

View File

@ -1,8 +0,0 @@
from . import wave_converter
BL_REGISTER = [
*wave_converter.BL_REGISTER,
]
BL_NODES = {
**wave_converter.BL_NODES,
}

View File

@ -1,82 +0,0 @@
import tidy3d as td
import sympy as sp
import sympy.physics.units as spu
import scipy as sc
from .... import contracts
from .... import sockets
from ... import base
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
####################
@base.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,
)
@base.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

@ -1,8 +0,0 @@
from . import array_operation
BL_REGISTER = [
*array_operation.BL_REGISTER,
]
BL_NODES = {
**array_operation.BL_NODES,
}

View File

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

View File

@ -0,0 +1,8 @@
from . import sim_data_viz
BL_REGISTER = [
*sim_data_viz.BL_REGISTER,
]
BL_NODES = {
**sim_data_viz.BL_NODES,
}

View File

@ -0,0 +1,332 @@
import typing as typ
import tidy3d as td
import numpy as np
import sympy as sp
import sympy.physics.units as spu
import bpy
from .....utils import analyze_geonodes
from ... import bl_socket_map
from ... import contracts as ct
from ... import sockets
from .. import base
from ... import managed_objs
CACHE = {}
class FDTDSimDataVizNode(base.MaxwellSimNode):
node_type = ct.NodeType.FDTDSimDataViz
bl_label = "FDTD Sim Data Viz"
####################
# - Sockets
####################
input_sockets = {
"FDTD Sim Data": sockets.MaxwellFDTDSimDataSocketDef(),
}
output_sockets= {
"Preview": sockets.AnySocketDef()
}
managed_obj_defs = {
"viz_plot": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLImage(name),
name_prefix="",
),
"viz_object": ct.schemas.ManagedObjDef(
mk=lambda name: managed_objs.ManagedBLObject(name),
name_prefix="",
),
}
####################
# - Properties
####################
viz_monitor_name: bpy.props.EnumProperty(
name="Viz Monitor Name",
description="Monitor to visualize within the attached SimData",
items=lambda self, context: self.retrieve_monitors(context),
update=(lambda self, context: self.sync_viz_monitor_name(context)),
)
cache_viz_monitor_type: bpy.props.StringProperty(
name="Viz Monitor Type",
description="Type of the viz monitor",
default=""
)
# Field Monitor Type
field_viz_component: bpy.props.EnumProperty(
name="Field Component",
description="Field component to visualize",
items=[
("E", "E", "Electric"),
#("H", "H", "Magnetic"),
#("S", "S", "Poynting"),
("Ex", "Ex", "Ex"),
("Ey", "Ey", "Ey"),
("Ez", "Ez", "Ez"),
#("Hx", "Hx", "Hx"),
#("Hy", "Hy", "Hy"),
#("Hz", "Hz", "Hz"),
],
default="E",
update=lambda self, context: self.sync_prop("field_viz_component", context),
)
field_viz_part: bpy.props.EnumProperty(
name="Field Part",
description="Field part to visualize",
items=[
("real", "Real", "Electric"),
("imag", "Imaginary", "Imaginary"),
("abs", "Abs", "Abs"),
("abs^2", "Squared Abs", "Square Abs"),
("phase", "Phase", "Phase"),
],
default="real",
update=lambda self, context: self.sync_prop("field_viz_part", context),
)
field_viz_scale: bpy.props.EnumProperty(
name="Field Scale",
description="Field scale to visualize in, Linear or Log",
items=[
("lin", "Linear", "Linear Scale"),
("dB", "Log (dB)", "Logarithmic (dB) Scale"),
],
default="lin",
update=lambda self, context: self.sync_prop("field_viz_scale", context),
)
field_viz_structure_visibility: bpy.props.FloatProperty(
name="Field Viz Plot: Structure Visibility",
description="Visibility of structes",
default=0.2,
min=0.0,
max=1.0,
update=lambda self, context: self.sync_prop("field_viz_plot_fixed_f", context),
)
field_viz_plot_fix_x: bpy.props.BoolProperty(
name="Field Viz Plot: Fix X",
description="Fix the x-coordinate on the plot",
default=False,
update=lambda self, context: self.sync_prop("field_viz_plot_fix_x", context),
)
field_viz_plot_fix_y: bpy.props.BoolProperty(
name="Field Viz Plot: Fix Y",
description="Fix the y coordinate on the plot",
default=False,
update=lambda self, context: self.sync_prop("field_viz_plot_fix_y", context),
)
field_viz_plot_fix_z: bpy.props.BoolProperty(
name="Field Viz Plot: Fix Z",
description="Fix the z coordinate on the plot",
default=False,
update=lambda self, context: self.sync_prop("field_viz_plot_fix_z", context),
)
field_viz_plot_fix_f: bpy.props.BoolProperty(
name="Field Viz Plot: Fix Freq",
description="Fix the frequency coordinate on the plot",
default=False,
update=lambda self, context: self.sync_prop("field_viz_plot_fix_f", context),
)
field_viz_plot_fixed_x: bpy.props.FloatProperty(
name="Field Viz Plot: Fix X",
description="Fix the x-coordinate on the plot",
default=0.0,
update=lambda self, context: self.sync_prop("field_viz_plot_fixed_x", context),
)
field_viz_plot_fixed_y: bpy.props.FloatProperty(
name="Field Viz Plot: Fixed Y",
description="Fix the y coordinate on the plot",
default=0.0,
update=lambda self, context: self.sync_prop("field_viz_plot_fixed_y", context),
)
field_viz_plot_fixed_z: bpy.props.FloatProperty(
name="Field Viz Plot: Fixed Z",
description="Fix the z coordinate on the plot",
default=0.0,
update=lambda self, context: self.sync_prop("field_viz_plot_fixed_z", context),
)
field_viz_plot_fixed_f: bpy.props.FloatProperty(
name="Field Viz Plot: Fixed Freq (Thz)",
description="Fix the frequency coordinate on the plot",
default=0.0,
update=lambda self, context: self.sync_prop("field_viz_plot_fixed_f", context),
)
####################
# - Derived Properties
####################
def sync_viz_monitor_name(self, context):
if (sim_data := self._compute_input("FDTD Sim Data")) is None:
return
self.cache_viz_monitor_type = sim_data.monitor_data[
self.viz_monitor_name
].type
self.sync_prop("viz_monitor_name", context)
def retrieve_monitors(self, context) -> list[tuple]:
global CACHE
if not CACHE.get(self.instance_id):
sim_data = self._compute_input("FDTD Sim Data")
if sim_data is not None:
CACHE[self.instance_id] = {"monitors": list(
sim_data.monitor_data.keys()
)}
else:
return [("NONE", "None", "No monitors")]
monitor_names = CACHE[self.instance_id]["monitors"]
# Check for No Monitors
if not monitor_names:
return [("NONE", "None", "No monitors")]
return [
(
monitor_name,
monitor_name,
f"Monitor '{monitor_name}' recorded by the FDTD Sim",
)
for monitor_name in monitor_names
]
####################
# - UI
####################
def draw_props(self, context, layout):
row = layout.row()
row.prop(self, "viz_monitor_name", text="")
if self.cache_viz_monitor_type == "FieldData":
# Array Selection
split = layout.split(factor=0.45)
col = split.column(align=False)
col.label(text="Component")
col.label(text="Part")
col.label(text="Scale")
col = split.column(align=False)
col.prop(self, "field_viz_component", text="")
col.prop(self, "field_viz_part", text="")
col.prop(self, "field_viz_scale", text="")
# Coordinate Fixing
split = layout.split(factor=0.45)
col = split.column(align=False)
col.prop(self, "field_viz_plot_fix_x", text="Fix x (um)")
col.prop(self, "field_viz_plot_fix_y", text="Fix y (um)")
col.prop(self, "field_viz_plot_fix_z", text="Fix z (um)")
col.prop(self, "field_viz_plot_fix_f", text="Fix f (THz)")
col = split.column(align=False)
col.prop(self, "field_viz_plot_fixed_x", text="")
col.prop(self, "field_viz_plot_fixed_y", text="")
col.prop(self, "field_viz_plot_fixed_z", text="")
col.prop(self, "field_viz_plot_fixed_f", text="")
####################
# - On Value Changed Methods
####################
@base.on_value_changed(
socket_name="FDTD Sim Data",
managed_objs={"viz_object"},
input_sockets={"FDTD Sim Data"},
)
def on_value_changed__fdtd_sim_data(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict[str, typ.Any],
) -> None:
global CACHE
if (sim_data := input_sockets["FDTD Sim Data"]) is None:
CACHE.pop(self.instance_id, None)
return
CACHE[self.instance_id] = {"monitors": list(
sim_data.monitor_data.keys()
)}
####################
# - Plotting
####################
@base.on_show_plot(
managed_objs={"viz_plot"},
props={
"viz_monitor_name", "field_viz_component",
"field_viz_part", "field_viz_scale",
"field_viz_structure_visibility",
"field_viz_plot_fix_x", "field_viz_plot_fix_y",
"field_viz_plot_fix_z", "field_viz_plot_fix_f",
"field_viz_plot_fixed_x", "field_viz_plot_fixed_y",
"field_viz_plot_fixed_z", "field_viz_plot_fixed_f",
},
input_sockets={"FDTD Sim Data"},
stop_propagation=True,
)
def on_show_plot(
self,
managed_objs: dict[str, ct.schemas.ManagedObj],
input_sockets: dict[str, typ.Any],
props: dict[str, typ.Any],
):
if (
(sim_data := input_sockets["FDTD Sim Data"]) is None
or (monitor_name := props["viz_monitor_name"]) == "NONE"
):
return
coord_fix = {}
for coord in ["x", "y", "z", "f"]:
if props[f"field_viz_plot_fix_{coord}"]:
coord_fix |= {
coord: props[f"field_viz_plot_fixed_{coord}"],
}
if "f" in coord_fix:
coord_fix["f"] *= 1e12
managed_objs["viz_plot"].mpl_plot_to_image(
lambda ax: sim_data.plot_field(
monitor_name,
props["field_viz_component"],
val=props["field_viz_part"],
scale=props["field_viz_scale"],
eps_alpha=props["field_viz_structure_visibility"],
phase=0,
**coord_fix,
ax=ax,
),
bl_select=True,
)
#@base.on_show_preview(
# managed_objs={"viz_object"},
#)
#def on_show_preview(
# self,
# managed_objs: dict[str, ct.schemas.ManagedObj],
#):
# """Called whenever a Loose Input Socket is altered.
#
# Synchronizes the change to the actual GeoNodes modifier, so that the change is immediately visible.
# """
# managed_objs["viz_object"].show_preview("MESH")
####################
# - Blender Registration
####################
BL_REGISTER = [
FDTDSimDataVizNode,
]
BL_NODES = {
ct.NodeType.FDTDSimDataViz: (
ct.NodeCategory.MAXWELLSIM_VIZ
)
}

View File

@ -52,6 +52,7 @@ MaxwellTemporalShapeSocketDef = maxwell.MaxwellTemporalShapeSocketDef
MaxwellStructureSocketDef = maxwell.MaxwellStructureSocketDef MaxwellStructureSocketDef = maxwell.MaxwellStructureSocketDef
MaxwellMonitorSocketDef = maxwell.MaxwellMonitorSocketDef MaxwellMonitorSocketDef = maxwell.MaxwellMonitorSocketDef
MaxwellFDTDSimSocketDef = maxwell.MaxwellFDTDSimSocketDef MaxwellFDTDSimSocketDef = maxwell.MaxwellFDTDSimSocketDef
MaxwellFDTDSimDataSocketDef = maxwell.MaxwellFDTDSimDataSocketDef
MaxwellSimGridSocketDef = maxwell.MaxwellSimGridSocketDef MaxwellSimGridSocketDef = maxwell.MaxwellSimGridSocketDef
MaxwellSimGridAxisSocketDef = maxwell.MaxwellSimGridAxisSocketDef MaxwellSimGridAxisSocketDef = maxwell.MaxwellSimGridAxisSocketDef
MaxwellSimDomainSocketDef = maxwell.MaxwellSimDomainSocketDef MaxwellSimDomainSocketDef = maxwell.MaxwellSimDomainSocketDef

View File

@ -19,12 +19,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
"CIRCLE", "SQUARE", "DIAMOND", "CIRCLE_DOT", "SQUARE_DOT", "CIRCLE", "SQUARE", "DIAMOND", "CIRCLE_DOT", "SQUARE_DOT",
"DIAMOND_DOT", "DIAMOND_DOT",
] ]
## We use the following conventions for shapes:
## - CIRCLE: Single Value.
## - SQUARE: Container of Value.
## - DIAMOND: Pointer Value.
## - +DOT: Uses Units
socket_color: tuple socket_color: tuple
# Options # Options
#link_limit: int = 0 #link_limit: int = 0
use_units: bool = False use_units: bool = False
#list_like: bool = False use_prelock: bool = False
# Computed # Computed
bl_idname: str bl_idname: str
@ -52,8 +57,19 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
cls.socket_color = ct.SOCKET_COLORS[cls.socket_type] cls.socket_color = ct.SOCKET_COLORS[cls.socket_type]
cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type] cls.socket_shape = ct.SOCKET_SHAPES[cls.socket_type]
# Setup List
cls.__annotations__["is_list"] = bpy.props.BoolProperty(
name="Is List",
description="Whether or not a particular socket is a list type socket",
default=False,
update=lambda self, context: self.sync_is_list(context)
)
# Configure Use of Units # Configure Use of Units
if cls.use_units: if cls.use_units:
# Set Shape :)
cls.socket_shape += "_DOT"
if not (socket_units := ct.SOCKET_UNITS.get(cls.socket_type)): if not (socket_units := ct.SOCKET_UNITS.get(cls.socket_type)):
msg = "Tried to `use_units` on {cls.bl_idname} socket, but `SocketType` has no units defined in `contracts.SOCKET_UNITS`" msg = "Tried to `use_units` on {cls.bl_idname} socket, but `SocketType` has no units defined in `contracts.SOCKET_UNITS`"
raise RuntimeError(msg) raise RuntimeError(msg)
@ -123,6 +139,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
#################### ####################
# - Action Chain: Event Handlers # - Action Chain: Event Handlers
#################### ####################
def sync_is_list(self, context: bpy.types.Context):
"""Called when the "is_list_ property has been updated.
"""
if self.is_list:
if self.use_units:
self.display_shape = "SQUARE_DOT"
else:
self.display_shape = "SQUARE"
self.trigger_action("value_changed")
def sync_prop(self, prop_name: str, context: bpy.types.Context): def sync_prop(self, prop_name: str, context: bpy.types.Context):
"""Called when a property has been updated. """Called when a property has been updated.
""" """
@ -166,11 +193,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
@property @property
def value(self) -> typ.Any: def value(self) -> typ.Any:
raise NotImplementedError raise NotImplementedError
@value.setter @value.setter
def value(self, value: typ.Any) -> None: def value(self, value: typ.Any) -> None:
raise NotImplementedError raise NotImplementedError
@property
def value_list(self) -> typ.Any:
return [self.value]
@value_list.setter
def value_list(self, value: typ.Any) -> None:
raise NotImplementedError
def value_as_unit_system( def value_as_unit_system(
self, self,
unit_system: dict, unit_system: dict,
@ -187,11 +220,17 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
@property @property
def lazy_value(self) -> None: def lazy_value(self) -> None:
raise NotImplementedError raise NotImplementedError
@lazy_value.setter @lazy_value.setter
def lazy_value(self, lazy_value: typ.Any) -> None: def lazy_value(self, lazy_value: typ.Any) -> None:
raise NotImplementedError raise NotImplementedError
@property
def lazy_value_list(self) -> typ.Any:
return [self.lazy_value]
@lazy_value_list.setter
def lazy_value_list(self, value: typ.Any) -> None:
raise NotImplementedError
@property @property
def capabilities(self) -> None: def capabilities(self) -> None:
raise NotImplementedError raise NotImplementedError
@ -205,11 +244,15 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
**NOTE**: Low-level method. Use `compute_data` instead. **NOTE**: Low-level method. Use `compute_data` instead.
""" """
if kind == ct.DataFlowKind.Value: if kind == ct.DataFlowKind.Value:
return self.value if self.is_list: return self.value_list
if kind == ct.DataFlowKind.LazyValue: else: return self.value
elif kind == ct.DataFlowKind.LazyValue:
if self.is_list: return self.lazy_value_list
else: return self.lazy_value
return self.lazy_value return self.lazy_value
if kind == ct.DataFlowKind.Capabilities: elif kind == ct.DataFlowKind.Capabilities:
return self.capabilities return self.capabilities
return None return None
def compute_data( def compute_data(
@ -222,8 +265,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
- If output socket, ask node for data. - If output socket, ask node for data.
""" """
# Compute Output Socket # Compute Output Socket
## List-like sockets guarantee that a list of a thing is passed.
if self.is_output: if self.is_output:
return self.node.compute_output(self.name, kind=kind) res = self.node.compute_output(self.name, kind=kind)
if self.is_list and not isinstance(res, list): return [res]
return res
# Compute Input Socket # Compute Input Socket
## Unlinked: Retrieve Socket Value ## Unlinked: Retrieve Socket Value
@ -334,13 +380,21 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
) -> None: ) -> None:
"""Called by Blender to draw the socket UI. """Called by Blender to draw the socket UI.
""" """
if self.locked: layout.enabled = False
if self.is_output: if self.is_output:
self.draw_output(context, layout, node, text) self.draw_output(context, layout, node, text)
else: else:
self.draw_input(context, layout, node, text) self.draw_input(context, layout, node, text)
def draw_prelock(
self,
context: bpy.types.Context,
col: bpy.types.UILayout,
node: bpy.types.Node,
text: str,
) -> None:
pass
def draw_input( def draw_input(
self, self,
context: bpy.types.Context, context: bpy.types.Context,
@ -350,18 +404,20 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
) -> None: ) -> None:
"""Draws the socket UI, when the socket is an input socket. """Draws the socket UI, when the socket is an input socket.
""" """
# Draw Linked Input: Label Row
if self.is_linked:
layout.label(text=text)
return
# Parent Column
col = layout.column(align=False) col = layout.column(align=False)
# Draw Label Row # Label Row
row = col.row(align=True) row = col.row(align=False)
if self.locked: row.enabled = False
## Linked Label
if self.is_linked:
row.label(text=text)
return
## User Label Row (incl. Units)
if self.use_units: if self.use_units:
split = row.split(factor=0.65, align=True) split = row.split(factor=0.6, align=True)
_row = split.row(align=True) _row = split.row(align=True)
self.draw_label_row(_row, text) self.draw_label_row(_row, text)
@ -371,8 +427,25 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
else: else:
self.draw_label_row(row, text) self.draw_label_row(row, text)
# Draw Value Row(s) # Prelock Row
self.draw_value(col) row = col.row(align=False)
if self.use_prelock:
_col = row.column(align=False)
_col.enabled = True
self.draw_prelock(context, _col, node, text)
if self.locked:
row = col.row(align=False)
row.enabled = False
else:
if self.locked: row.enabled = False
# Value Column(s)
col = row.column(align=True)
if self.is_list:
self.draw_value_list(col)
else:
self.draw_value(col)
def draw_output( def draw_output(
self, self,
@ -406,3 +479,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
""" """
pass pass
def draw_value_list(self, col: bpy.types.UILayout) -> None:
"""Called to draw the value list column in unlinked input sockets.
Can be overridden.
"""
pass

View File

@ -49,18 +49,18 @@ class BlenderGeoNodesBLSocket(base.MaxwellSimSocket):
#################### ####################
# - UI # - UI
#################### ####################
def draw_label_row(self, label_col_row, text): #def draw_label_row(self, label_col_row, text):
label_col_row.label(text=text) # label_col_row.label(text=text)
if not self.raw_value: return # if not self.raw_value: return
#
op = label_col_row.operator( # op = label_col_row.operator(
BlenderMaxwellResetGeoNodesSocket.bl_idname, # BlenderMaxwellResetGeoNodesSocket.bl_idname,
text="", # text="",
icon="FILE_REFRESH", # icon="FILE_REFRESH",
) # )
op.socket_name = self.name # op.socket_name = self.name
op.node_name = self.node.name # op.node_name = self.node.name
op.node_tree_name = self.node.id_data.name # op.node_tree_name = self.node.id_data.name
#################### ####################
# - UI # - UI

View File

@ -20,10 +20,12 @@ from . import monitor
MaxwellMonitorSocketDef = monitor.MaxwellMonitorSocketDef MaxwellMonitorSocketDef = monitor.MaxwellMonitorSocketDef
from . import fdtd_sim from . import fdtd_sim
from . import fdtd_sim_data
from . import sim_grid from . import sim_grid
from . import sim_grid_axis from . import sim_grid_axis
from . import sim_domain from . import sim_domain
MaxwellFDTDSimSocketDef = fdtd_sim.MaxwellFDTDSimSocketDef MaxwellFDTDSimSocketDef = fdtd_sim.MaxwellFDTDSimSocketDef
MaxwellFDTDSimDataSocketDef = fdtd_sim_data.MaxwellFDTDSimDataSocketDef
MaxwellSimGridSocketDef = sim_grid.MaxwellSimGridSocketDef MaxwellSimGridSocketDef = sim_grid.MaxwellSimGridSocketDef
MaxwellSimGridAxisSocketDef = sim_grid_axis.MaxwellSimGridAxisSocketDef MaxwellSimGridAxisSocketDef = sim_grid_axis.MaxwellSimGridAxisSocketDef
MaxwellSimDomainSocketDef = sim_domain.MaxwellSimDomainSocketDef MaxwellSimDomainSocketDef = sim_domain.MaxwellSimDomainSocketDef
@ -39,6 +41,7 @@ BL_REGISTER = [
*structure.BL_REGISTER, *structure.BL_REGISTER,
*monitor.BL_REGISTER, *monitor.BL_REGISTER,
*fdtd_sim.BL_REGISTER, *fdtd_sim.BL_REGISTER,
*fdtd_sim_data.BL_REGISTER,
*sim_grid.BL_REGISTER, *sim_grid.BL_REGISTER,
*sim_grid_axis.BL_REGISTER, *sim_grid_axis.BL_REGISTER,
*sim_domain.BL_REGISTER, *sim_domain.BL_REGISTER,

View File

@ -11,6 +11,10 @@ class MaxwellFDTDSimBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.MaxwellFDTDSim socket_type = ct.SocketType.MaxwellFDTDSim
bl_label = "Maxwell FDTD Simulation" bl_label = "Maxwell FDTD Simulation"
@property
def value(self) -> None:
return None
#################### ####################
# - Socket Configuration # - Socket Configuration
#################### ####################

View File

@ -0,0 +1,32 @@
import typing as typ
import bpy
import pydantic as pyd
import tidy3d as td
from .. import base
from ... import contracts as ct
class MaxwellFDTDSimDataBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.MaxwellFDTDSimData
bl_label = "Maxwell FDTD Simulation"
@property
def value(self):
return None
####################
# - Socket Configuration
####################
class MaxwellFDTDSimDataSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.MaxwellFDTDSimData
def init(self, bl_socket: MaxwellFDTDSimDataBLSocket) -> None:
pass
####################
# - Blender Registration
####################
BL_REGISTER = [
MaxwellFDTDSimDataBLSocket,
]

View File

@ -9,11 +9,6 @@ import scipy as sc
from .. import base from .. import base
from ... import contracts as ct from ... import contracts as ct
VAC_SPEED_OF_LIGHT = (
sc.constants.speed_of_light
* spu.meter/spu.second
)
class MaxwellMonitorBLSocket(base.MaxwellSimSocket): class MaxwellMonitorBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.MaxwellMonitor socket_type = ct.SocketType.MaxwellMonitor
bl_label = "Maxwell Monitor" bl_label = "Maxwell Monitor"
@ -24,8 +19,10 @@ class MaxwellMonitorBLSocket(base.MaxwellSimSocket):
class MaxwellMonitorSocketDef(pyd.BaseModel): class MaxwellMonitorSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.MaxwellMonitor socket_type: ct.SocketType = ct.SocketType.MaxwellMonitor
is_list: bool = False
def init(self, bl_socket: MaxwellMonitorBLSocket) -> None: def init(self, bl_socket: MaxwellMonitorBLSocket) -> None:
pass bl_socket.is_list = self.is_list
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -17,8 +17,10 @@ class MaxwellSourceBLSocket(base.MaxwellSimSocket):
class MaxwellSourceSocketDef(pyd.BaseModel): class MaxwellSourceSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.MaxwellSource socket_type: ct.SocketType = ct.SocketType.MaxwellSource
is_list: bool = False
def init(self, bl_socket: MaxwellSourceBLSocket) -> None: def init(self, bl_socket: MaxwellSourceBLSocket) -> None:
pass bl_socket.is_list = self.is_list
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -16,8 +16,10 @@ class MaxwellStructureBLSocket(base.MaxwellSimSocket):
class MaxwellStructureSocketDef(pyd.BaseModel): class MaxwellStructureSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.MaxwellStructure socket_type: ct.SocketType = ct.SocketType.MaxwellStructure
is_list: bool = False
def init(self, bl_socket: MaxwellStructureBLSocket) -> None: def init(self, bl_socket: MaxwellStructureBLSocket) -> None:
pass bl_socket.is_list = self.is_list
#################### ####################
# - Blender Registration # - Blender Registration

View File

@ -1,9 +1,13 @@
import typing as typ import typing as typ
import json
import numpy as np
import bpy import bpy
import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
import pydantic as pyd import pydantic as pyd
from .....utils import extra_sympy_units as spux
from .....utils.pydantic_sympy import SympyExpr from .....utils.pydantic_sympy import SympyExpr
from .. import base from .. import base
from ... import contracts as ct from ... import contracts as ct
@ -27,38 +31,103 @@ class PhysicalFreqBLSocket(base.MaxwellSimSocket):
update=(lambda self, context: self.sync_prop("raw_value", context)), update=(lambda self, context: self.sync_prop("raw_value", context)),
) )
min_freq: bpy.props.FloatProperty(
name="Min Frequency",
description="Lowest frequency",
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop("min_freq", context)),
)
max_freq: bpy.props.FloatProperty(
name="Max Frequency",
description="Highest frequency",
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop("max_freq", context)),
)
steps: bpy.props.IntProperty(
name="Frequency Steps",
description="# of steps between min and max",
default=2,
update=(lambda self, context: self.sync_prop("steps", context)),
)
#################### ####################
# - Socket UI # - Socket UI
#################### ####################
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, "raw_value", text="") col.prop(self, "raw_value", text="")
def draw_value_list(self, col: bpy.types.UILayout) -> None:
col.prop(self, "min_freq", text="Min")
col.prop(self, "max_freq", text="Max")
col.prop(self, "steps", text="Steps")
#################### ####################
# - Default Value # - Default Value
#################### ####################
@property @property
def value(self) -> SympyExpr: def value(self) -> SympyExpr:
return self.raw_value * self.unit return self.raw_value * self.unit
@value.setter @value.setter
def value(self, value: SympyExpr) -> None: def value(self, value: SympyExpr) -> None:
self.raw_value = spu.convert_to(value, self.unit) / self.unit self.raw_value = spu.convert_to(value, self.unit) / self.unit
@property
def value_list(self) -> list[SympyExpr]:
return [
el * self.unit
for el in np.linspace(self.min_freq, self.max_freq, self.steps)
]
@value_list.setter
def value_list(self, value: tuple[SympyExpr, SympyExpr, int]):
self.min_freq, self.max_freq, self.steps = [
spu.convert_to(el, self.unit) / self.unit
for el in value[:2]
] + [value[2]]
def sync_unit_change(self) -> None:
if self.is_list:
self.value_list = (
spu.convert_to(
self.min_freq * self.prev_unit,
self.unit
),
spu.convert_to(
self.max_freq * self.prev_unit,
self.unit
),
self.steps,
)
else:
self.value = self.value / self.unit * self.prev_unit
self.prev_active_unit = self.active_unit
#################### ####################
# - Socket Configuration # - Socket Configuration
#################### ####################
class PhysicalFreqSocketDef(pyd.BaseModel): class PhysicalFreqSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.PhysicalFreq socket_type: ct.SocketType = ct.SocketType.PhysicalFreq
default_value: SympyExpr | None = None default_value: SympyExpr = 500*spux.terahertz
default_unit: SympyExpr | None = None default_unit: SympyExpr | None = None
is_list: bool = False
min_freq: SympyExpr = 400.0*spux.terahertz
max_freq: SympyExpr = 600.0*spux.terahertz
steps: SympyExpr = 50
def init(self, bl_socket: PhysicalFreqBLSocket) -> None: def init(self, bl_socket: PhysicalFreqBLSocket) -> None:
if self.default_value: bl_socket.value = self.default_value
bl_socket.value = self.default_value bl_socket.is_list = self.is_list
if self.default_unit: if self.default_unit:
bl_socket.unit = self.default_unit bl_socket.unit = self.default_unit
if self.is_list:
bl_socket.value_list = (self.min_freq, self.max_freq, self.steps)
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################

View File

@ -2,6 +2,7 @@ import typing as typ
import bpy import bpy
import sympy.physics.units as spu import sympy.physics.units as spu
import numpy as np
import pydantic as pyd import pydantic as pyd
from .....utils.pydantic_sympy import SympyExpr from .....utils.pydantic_sympy import SympyExpr
@ -27,35 +28,103 @@ class PhysicalLengthBLSocket(base.MaxwellSimSocket):
update=(lambda self, context: self.sync_prop("raw_value", context)), update=(lambda self, context: self.sync_prop("raw_value", context)),
) )
min_len: bpy.props.FloatProperty(
name="Min Length",
description="Lowest length",
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop("min_len", context)),
)
max_len: bpy.props.FloatProperty(
name="Max Length",
description="Highest length",
default=0.0,
precision=4,
update=(lambda self, context: self.sync_prop("max_len", context)),
)
steps: bpy.props.IntProperty(
name="Length Steps",
description="# of steps between min and max",
default=2,
update=(lambda self, context: self.sync_prop("steps", context)),
)
#################### ####################
# - Socket UI # - Socket UI
#################### ####################
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_value(self, col: bpy.types.UILayout) -> None:
col.prop(self, "raw_value", text="") col.prop(self, "raw_value", text="")
def draw_value_list(self, col: bpy.types.UILayout) -> None:
col.prop(self, "min_len", text="Min")
col.prop(self, "max_len", text="Max")
col.prop(self, "steps", text="Steps")
#################### ####################
# - Default Value # - Default Value
#################### ####################
@property @property
def value(self) -> SympyExpr: def value(self) -> SympyExpr:
return self.raw_value * self.unit return self.raw_value * self.unit
@value.setter @value.setter
def value(self, value: SympyExpr) -> None: def value(self, value: SympyExpr) -> None:
self.raw_value = spu.convert_to(value, self.unit) / self.unit self.raw_value = spu.convert_to(value, self.unit) / self.unit
@property
def value_list(self) -> list[SympyExpr]:
return [
el * self.unit
for el in np.linspace(self.min_len, self.max_len, self.steps)
]
@value_list.setter
def value_list(self, value: tuple[SympyExpr, SympyExpr, int]):
self.min_len, self.max_len, self.steps = [
spu.convert_to(el, self.unit) / self.unit
for el in value[:2]
] + [value[2]]
def sync_unit_change(self) -> None:
if self.is_list:
self.value_list = (
spu.convert_to(
self.min_len * self.prev_unit,
self.unit
),
spu.convert_to(
self.max_len * self.prev_unit,
self.unit
),
self.steps,
)
else:
self.value = self.value / self.unit * self.prev_unit
self.prev_active_unit = self.active_unit
#################### ####################
# - Socket Configuration # - Socket Configuration
#################### ####################
class PhysicalLengthSocketDef(pyd.BaseModel): class PhysicalLengthSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.PhysicalLength socket_type: ct.SocketType = ct.SocketType.PhysicalLength
default_value: SympyExpr = 1*spu.um
default_unit: SympyExpr | None = None default_unit: SympyExpr | None = None
is_list: bool = False
min_len: SympyExpr = 400.0*spu.nm
max_len: SympyExpr = 600.0*spu.nm
steps: SympyExpr = 50
def init(self, bl_socket: PhysicalLengthBLSocket) -> None: def init(self, bl_socket: PhysicalLengthBLSocket) -> None:
bl_socket.value = self.default_value
bl_socket.is_list = self.is_list
if self.default_unit: if self.default_unit:
bl_socket.unit = self.default_unit bl_socket.unit = self.default_unit
if self.is_list:
bl_socket.value_list = (self.min_len, self.max_len, self.steps)
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################

View File

@ -49,9 +49,11 @@ class PhysicalSize3DBLSocket(base.MaxwellSimSocket):
class PhysicalSize3DSocketDef(pyd.BaseModel): class PhysicalSize3DSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.PhysicalSize3D socket_type: ct.SocketType = ct.SocketType.PhysicalSize3D
default_value: SympyExpr = sp.Matrix([1, 1, 1]) * spu.um
default_unit: SympyExpr | None = None default_unit: SympyExpr | None = None
def init(self, bl_socket: PhysicalSize3DBLSocket) -> None: def init(self, bl_socket: PhysicalSize3DBLSocket) -> None:
bl_socket.value = self.default_value
if self.default_unit: if self.default_unit:
bl_socket.unit = self.default_unit bl_socket.unit = self.default_unit

View File

@ -6,91 +6,74 @@ import pydantic as pyd
import tidy3d as td import tidy3d as td
import tidy3d.web as _td_web import tidy3d.web as _td_web
from .....utils.auth_td_web import g_td_web, is_td_web_authed from .....utils import tdcloud
from .. import base from .. import base
from ... import contracts as ct from ... import contracts as ct
#################### ####################
# - Tidy3D Folder/Task Management # - Operators
#################### ####################
TD_FOLDERS = None class ReloadFolderList(bpy.types.Operator):
## TODO: Keep this data serialized in each node, so it works offline and saves/loads correctly (then we can try/except when the network fails). bl_idname = "blender_maxwell.sockets__reload_folder_list"
## - We should consider adding some kind of serialization-backed instance data to the node base class... bl_label = "Reload Tidy3D Folder List"
## - We could guard it behind a feature, 'use_node_data_store' for example. bl_description = "Reload the the cached Tidy3D folder list"
def g_td_folders():
global TD_FOLDERS
if TD_FOLDERS is not None: return TD_FOLDERS
# Populate Folders Cache & Return
TD_FOLDERS = {
cloud_folder.folder_name: None
for cloud_folder in _td_web.core.task_core.Folder.list()
}
return TD_FOLDERS
def g_td_tasks(cloud_folder_name: str):
global TD_FOLDERS
# Retrieve Cached Tasks
if (_tasks := TD_FOLDERS.get(cloud_folder_name)) is not None:
return _tasks
# Retrieve Cloud Folder (if exists)
try:
cloud_folder = _td_web.core.task_core.Folder.get(cloud_folder_name)
except AttributeError as err:
# Folder Doesn't Exist
TD_FOLDERS = None
return []
# Return Tasks as List (also empty)
if (tasks := cloud_folder.list_tasks()) is None:
tasks = []
# Populate Cloud-Folder Cache & Return
TD_FOLDERS[cloud_folder_name] = [
task
for task in tasks
]
return TD_FOLDERS[cloud_folder_name]
class BlenderMaxwellRefreshTDFolderList(bpy.types.Operator):
bl_idname = "blender_maxwell.refresh_td_folder_list"
bl_label = "Refresh Tidy3D Folder List"
bl_description = "Refresh the cached Tidy3D folder list"
bl_options = {'REGISTER'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
space = context.space_data space = context.space_data
return ( return (
space.type == 'NODE_EDITOR' tdcloud.IS_AUTHENTICATED
and space.node_tree is not None
and space.node_tree.bl_idname == "MaxwellSimTreeType" and hasattr(context, "socket")
and is_td_web_authed() and hasattr(context.socket, "socket_type")
and context.socket.socket_type == ct.SocketType.Tidy3DCloudTask
) )
def execute(self, context): def execute(self, context):
global TD_FOLDERS socket = context.socket
tdcloud.TidyCloudFolders.update_folders()
tdcloud.TidyCloudTasks.update_tasks(socket.existing_folder_id)
TD_FOLDERS = None
return {'FINISHED'} return {'FINISHED'}
class Authenticate(bpy.types.Operator):
bl_idname = "blender_maxwell.sockets__authenticate"
bl_label = "Authenticate Tidy3D"
bl_description = "Authenticate the Tidy3D Web API from a Cloud Task socket"
@classmethod
def poll(cls, context):
return (
not tdcloud.IS_AUTHENTICATED
and hasattr(context, "socket")
and hasattr(context.socket, "socket_type")
and context.socket.socket_type == ct.SocketType.Tidy3DCloudTask
)
def execute(self, context):
bl_socket = context.socket
if not tdcloud.check_authentication():
tdcloud.authenticate_with_api_key(bl_socket.api_key)
bl_socket.api_key = ""
return {'FINISHED'}
####################
# - Socket
####################
class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket): class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
socket_type = ct.SocketType.Tidy3DCloudTask socket_type = ct.SocketType.Tidy3DCloudTask
bl_label = "Tidy3D Cloud Sim" bl_label = "Tidy3D Cloud Task"
use_prelock = True
#################### ####################
# - Properties # - Properties
#################### ####################
task_exists: bpy.props.BoolProperty( # Authentication
name="Cloud Task Should Exist",
description="Whether or not the cloud task referred to should exist",
default=False,
)
api_key: bpy.props.StringProperty( api_key: bpy.props.StringProperty(
name="API Key", name="API Key",
description="API Key for the Tidy3D Cloud", description="API Key for the Tidy3D Cloud",
@ -99,11 +82,19 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
subtype="PASSWORD", subtype="PASSWORD",
) )
existing_folder_name: bpy.props.EnumProperty( # Task Existance Presumption
should_exist: bpy.props.BoolProperty(
name="Cloud Task Should Exist",
description="Whether or not the cloud task should already exist",
default=False,
)
# Identifiers
existing_folder_id: bpy.props.EnumProperty(
name="Folder of Cloud Tasks", name="Folder of Cloud Tasks",
description="An existing folder on the Tidy3D Cloud", description="An existing folder on the Tidy3D Cloud",
items=lambda self, context: self.retrieve_folders(context), items=lambda self, context: self.retrieve_folders(context),
update=(lambda self, context: self.sync_prop("existing_folder_name", context)), update=(lambda self, context: self.sync_prop("existing_folder_id", context)),
) )
existing_task_id: bpy.props.EnumProperty( existing_task_id: bpy.props.EnumProperty(
name="Existing Cloud Task", name="Existing Cloud Task",
@ -111,35 +102,49 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
items=lambda self, context: self.retrieve_tasks(context), items=lambda self, context: self.retrieve_tasks(context),
update=(lambda self, context: self.sync_prop("existing_task_id", context)), update=(lambda self, context: self.sync_prop("existing_task_id", context)),
) )
# (Potential) New Task
new_task_name: bpy.props.StringProperty( new_task_name: bpy.props.StringProperty(
name="New Cloud Task Name", name="New Cloud Task Name",
description="Name of a new task to submit to the Tidy3D Cloud", description="Name of a new task to submit to the Tidy3D Cloud",
default="", default="",
update=(lambda self, context: self.sync_new_task(context)), update=(lambda self, context: self.sync_prop("new_task_name", context)),
) )
lock_nonauth_interface: bpy.props.BoolProperty(
name="Lock the non-Auth Interface", ####################
description="Declares that the non-auth interface should be locked", # - Property Methods
default=False, ####################
) def sync_existing_folder_id(self, context):
folder_task_ids = self.retrieve_tasks(context)
self.existing_task_id = folder_task_ids[0][0]
## There's guaranteed to at least be one element, even if it's "NONE".
self.sync_prop("existing_folder_id", context)
def retrieve_folders(self, context) -> list[tuple]: def retrieve_folders(self, context) -> list[tuple]:
if not is_td_web_authed: return [] folders = tdcloud.TidyCloudFolders.folders()
## What if there are no folders? if not folders:
return [("NONE", "None", "No folders")]
return [ return [
( (
folder_name, cloud_folder.folder_id,
folder_name, cloud_folder.folder_name,
folder_name, f"Folder 'cloud_folder.folder_name' with ID {folder_id}",
) )
for folder_name in g_td_folders() for folder_id, cloud_folder in folders.items()
] ]
def retrieve_tasks(self, context) -> list[tuple]: def retrieve_tasks(self, context) -> list[tuple]:
if not is_td_web_authed: return [] if (cloud_folder := tdcloud.TidyCloudFolders.folders().get(
if not (cloud_tasks := g_td_tasks(self.existing_folder_name)): self.existing_folder_id
)) is None:
return [("NONE", "None", "Folder doesn't exist")]
tasks = tdcloud.TidyCloudTasks.tasks(cloud_folder)
if not tasks:
return [("NONE", "None", "No tasks in folder")] return [("NONE", "None", "No tasks in folder")]
return [ return [
@ -156,81 +161,66 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
]), ]),
## Task Description ## Task Description
{ f"Task Status: {task.status}",
"draft": "Task has been uploaded, but not run",
"initialized": "Task is initializing",
"queued": "Task is queued for simulation",
"preprocessing": "Task is pre-processing",
"running": "Task is currently running",
"postprocess": "Task is post-processing",
"success": "Task ran successfully, costing {task.real_flex_unit} credits",
"error": "Task ran, but an error occurred",
}[task.status],
## Status Icon ## Status Icon
{ _icon if (_icon := {
"draft": "SEQUENCE_COLOR_08", "draft": "SEQUENCE_COLOR_08",
"initialized": "SHADING_SOLID", "initialized": "SHADING_SOLID",
"queued": "SEQUENCE_COLOR_03", "queued": "SEQUENCE_COLOR_03",
"preprocessing": "SEQUENCE_COLOR_02", "preprocessing": "SEQUENCE_COLOR_02",
"running": "SEQUENCE_COLOR_05", "running": "SEQUENCE_COLOR_05",
"postprocess": "SEQUENCE_COLOR_06", "postprocessing": "SEQUENCE_COLOR_06",
"success": "SEQUENCE_COLOR_04", "success": "SEQUENCE_COLOR_04",
"error": "SEQUENCE_COLOR_01", "error": "SEQUENCE_COLOR_01",
}[task.status], }.get(task.status)) else "SEQUENCE_COLOR_09",
## Unique Number ## Unique Number
i, i,
) )
for i, task in enumerate( for i, task in enumerate(
sorted(cloud_tasks, key=lambda el: el.created_at, reverse=True) sorted(
tasks.values(),
key=lambda el: el.created_at,
reverse=True,
)
) )
] ]
#################### ####################
# - Task Sync Methods # - Task Sync Methods
#################### ####################
def sync_new_task(self, context): def sync_created_new_task(self, cloud_task):
if self.new_task_name == "": return """Called whenever the task specified in `new_task_name` has been actually created.
if self.new_task_name in { This changes the socket somewhat: Folder/task IDs are set, and the socket is switched to presume that the task exists.
task.taskName
for task in g_td_tasks(self.existing_folder_name)
}:
self.new_task_name = ""
self.sync_prop("new_task_name", context) If the socket is linked, then an error is raised.
def sync_task_loaded(self, loaded_task_id: str | None):
"""Called whenever a particular task has been loaded.
This resets the 'new_task_name' (if any), sets the dropdown to the new loaded task (which must be in the already-selected folder) (or, if input is None, leaves the selection alone), locks the socket UI (though NEVER the API authentication interface), and declares that the specified task exists.
""" """
global TD_FOLDERS # Propagate along Link
## TODO: This doesn't work with a linked socket. It should. if self.is_linked:
msg = f"Cannot sync newly created task to linked Cloud Task socket."
raise ValueError(msg)
## TODO: A little aggressive. Is there a good use case?
if not (TD_FOLDERS is None): # Synchronize w/New Task Information
TD_FOLDERS[self.existing_folder_name] = None self.existing_folder_id = cloud_task.folder_id
self.existing_task_id = cloud_task.task_id
self.should_exist = True
if loaded_task_id is not None: def sync_prepare_new_task(self):
self.existing_task_id = loaded_task_id """Called to switch the socket to no longer presume that the task it specifies exists (yet).
self.new_task_name = "" If the socket is linked, then an error is raised.
self.lock_nonauth_interface = True """
self.task_exists = True # Propagate along Link
if self.is_linked:
msg = f"Cannot sync newly created task to linked Cloud Task socket."
raise ValueError(msg)
## TODO: A little aggressive. Is there a good use case?
def sync_task_status_change(self, running_task_id: str): # Synchronize w/New Task Information
global TD_FOLDERS self.should_exist = False
## TODO: This doesn't work with a linked socket. It should.
if not (TD_FOLDERS is None):
TD_FOLDERS[self.existing_folder_name] = None
def sync_task_released(self, specify_new_task: bool = False):
## TODO: This doesn't work with a linked socket. It should.
self.new_task_name = ""
self.lock_nonauth_interface = False
self.task_exists = not specify_new_task
#################### ####################
# - Socket UI # - Socket UI
@ -238,78 +228,106 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
def draw_label_row(self, row: bpy.types.UILayout, text: str): def draw_label_row(self, row: bpy.types.UILayout, text: str):
row.label(text=text) row.label(text=text)
auth_icon = "CHECKBOX_HLT" if is_td_web_authed() else "CHECKBOX_DEHLT" auth_icon = "LOCKVIEW_ON" if tdcloud.IS_AUTHENTICATED else "LOCKVIEW_OFF"
row.operator( row.operator(
"blender_maxwell.refresh_td_auth", Authenticate.bl_idname,
text="", text="",
icon=auth_icon, icon=auth_icon,
) )
def draw_value(self, col: bpy.types.UILayout) -> None: def draw_prelock(
if is_td_web_authed(): self,
if self.lock_nonauth_interface: col.enabled = False context: bpy.types.Context,
else: col.enabled = True col: bpy.types.UILayout,
node: bpy.types.Node,
text: str,
) -> None:
if not tdcloud.IS_AUTHENTICATED:
row = col.row() row = col.row()
row.label(icon="FILE_FOLDER") row.alignment = "CENTER"
row.prop(self, "existing_folder_name", text="")
row.operator(
BlenderMaxwellRefreshTDFolderList.bl_idname,
text="",
icon="FILE_REFRESH",
)
if not self.task_exists:
row = col.row()
row.label(icon="SEQUENCE_COLOR_04")
row.prop(self, "new_task_name", text="")
if self.task_exists:
row = col.row()
else:
col.separator(factor=1.0)
box = col.box()
row = box.row()
row.label(icon="NETWORK_DRIVE")
row.prop(self, "existing_task_id", text="")
else:
col.enabled = True
row = col.row()
row.alignment="CENTER"
row.label(text="Tidy3D API Key") row.label(text="Tidy3D API Key")
row = col.row() row = col.row()
row.prop(self, "api_key", text="") row.prop(self, "api_key", text="")
@property row = col.row()
def value(self) -> str | None: row.operator(
if self.task_exists: Authenticate.bl_idname,
if self.existing_task_id == "NONE": return None text="Connect",
return self.existing_task_id )
return dict(
task_name=self.new_task_name,
folder_name=self.existing_folder_name, def draw_value(self, col: bpy.types.UILayout) -> None:
if not tdcloud.IS_AUTHENTICATED: return
# Cloud Folder Selector
row = col.row()
row.label(icon="FILE_FOLDER")
row.prop(self, "existing_folder_id", text="")
row.operator(
ReloadFolderList.bl_idname,
text="",
icon="FILE_REFRESH",
) )
# New Task Name Selector
row = col.row()
if not self.should_exist:
row = col.row()
row.label(icon="NETWORK_DRIVE")
row.prop(self, "new_task_name", text="")
col.separator(factor=1.0)
box = col.box()
row = box.row()
row.prop(self, "existing_task_id", text="")
@property
def value(self) -> tuple[tdcloud.CloudTaskName, tdcloud.CloudFolder] | tdcloud.CloudTask | None:
# Retrieve Folder
## Authentication is presumed OK
if (cloud_folder := tdcloud.TidyCloudFolders.folders().get(
self.existing_folder_id
)) is None:
msg = "Selected folder doesn't exist (it was probably deleted elsewhere)"
raise RuntimeError(msg)
# No Tasks in Folder
## The UI should set to "NONE" when there are no tasks in a folder
if self.existing_task_id == "NONE": return None
# Retrieve Task
if self.should_exist:
if (cloud_task := tdcloud.TidyCloudTasks.tasks(
cloud_folder
).get(self.existing_task_id)) is None:
msg = "Selected task doesn't exist (it was probably deleted elsewhere)"
raise RuntimeError(msg)
return cloud_task
return (self.new_task_name, cloud_folder)
#################### ####################
# - Socket Configuration # - Socket Configuration
#################### ####################
class Tidy3DCloudTaskSocketDef(pyd.BaseModel): class Tidy3DCloudTaskSocketDef(pyd.BaseModel):
socket_type: ct.SocketType = ct.SocketType.Tidy3DCloudTask socket_type: ct.SocketType = ct.SocketType.Tidy3DCloudTask
task_exists: bool should_exist: bool
def init(self, bl_socket: Tidy3DCloudTaskBLSocket) -> None: def init(self, bl_socket: Tidy3DCloudTaskBLSocket) -> None:
bl_socket.task_exists = self.task_exists bl_socket.should_exist = self.should_exist
#################### ####################
# - Blender Registration # - Blender Registration
#################### ####################
BL_REGISTER = [ BL_REGISTER = [
BlenderMaxwellRefreshTDFolderList, ReloadFolderList,
Authenticate,
Tidy3DCloudTaskBLSocket, Tidy3DCloudTaskBLSocket,
] ]

View File

@ -1,13 +1,11 @@
from . import install_deps from . import install_deps
from . import uninstall_deps from . import uninstall_deps
from . import connect_viewer from . import connect_viewer
from . import refresh_td_auth
BL_REGISTER = [ BL_REGISTER = [
*install_deps.BL_REGISTER, *install_deps.BL_REGISTER,
*uninstall_deps.BL_REGISTER, *uninstall_deps.BL_REGISTER,
*connect_viewer.BL_REGISTER, *connect_viewer.BL_REGISTER,
*refresh_td_auth.BL_REGISTER,
] ]
BL_KMI_REGISTER = [ BL_KMI_REGISTER = [
*connect_viewer.BL_KMI_REGISTER, *connect_viewer.BL_KMI_REGISTER,

View File

@ -1,30 +0,0 @@
import bpy
from ..utils.auth_td_web import is_td_web_authed
class BlenderMaxwellRefreshTDAuth(bpy.types.Operator):
bl_idname = "blender_maxwell.refresh_td_auth"
bl_label = "Refresh Tidy3D Auth"
bl_description = "Refresh the authentication of Tidy3D Web API"
bl_options = {'REGISTER'}
@classmethod
def poll(cls, context):
space = context.space_data
return (
space.type == 'NODE_EDITOR'
and space.node_tree is not None
and space.node_tree.bl_idname == "MaxwellSimTreeType"
)
def invoke(self, context, event):
is_td_web_authed(force_check=True)
return {'FINISHED'}
####################
# - Blender Registration
####################
BL_REGISTER = [
BlenderMaxwellRefreshTDAuth,
]
BL_KMI_REGISTER = []

View File

@ -3,7 +3,7 @@ import bpy
from .operators import types as operators_types from .operators import types as operators_types
class BlenderMaxwellAddonPreferences(bpy.types.AddonPreferences): class BlenderMaxwellAddonPreferences(bpy.types.AddonPreferences):
bl_idname = "blender_maxwell_preferences" bl_idname = "blender_maxwell"
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout

View File

@ -3,3 +3,5 @@ pydantic==2.6.0
sympy==1.12 sympy==1.12
scipy==1.12.0 scipy==1.12.0
trimesh==4.1.4 trimesh==4.1.4
networkx==3.2.1
Rtree==1.2.0

View File

@ -1,57 +0,0 @@
import types
import tidy3d.web as td_web
AUTHENTICATED = False
def td_auth(api_key: str):
# Check for API Key
if api_key:
msg = "API Key must be defined to authenticate"
raise ValueError(msg)
# Perform Authentication
td_web.configure(api_key)
try:
td_web.test()
except:
msg = "Tidy3D Cloud Authentication Failed"
raise ValueError(msg)
AUTHENTICATED = True
def is_td_web_authed(force_check: bool = False) -> bool:
"""Checks whether `td_web` is authenticated, using the cache.
The result is heuristically accurate.
If accuracy must be guaranteed, an aliveness-check can be performed by setting `force_check=True`.
This comes at a performance penalty, as a web request must be made; thus, `force_check` is not appropriate for hot-paths like `draw` functions.
If a check is performed
"""
global AUTHENTICATED
# Return Cached Authentication
if not force_check:
return AUTHENTICATED
# Re-Check Authentication
try:
td_web.test()
AUTHENTICATED = True ## Guarantee cache value to True.
return True
except:
AUTHENTICATED = False ## Guarantee cache value to False.
return False
def g_td_web(api_key: str, force_check: bool = False) -> types.ModuleType:
"""Returns a `tidy3d.web` module object that is already authenticated using the given API key.
The authentication status is cached using a global module-level variable, `AUTHENTICATED`.
"""
global AUTHENTICATED
# Check Cached Authentication
if not is_td_web_authed(force_check=force_check):
td_auth(api_key)
return td_web

View File

@ -1,3 +1,5 @@
import functools
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
@ -67,11 +69,21 @@ exahertz.set_global_relative_scale_factor(spu.exa, spu.hertz)
#################### ####################
# - Sympy Expression Typing # - Sympy Expression Typing
#################### ####################
#ALL_UNIT_SYMBOLS = { ALL_UNIT_SYMBOLS = {
# unit unit.abbrev: unit
# for unit in spu.__dict__.values() for unit in spu.__dict__.values()
# if isinstance(unit, spu.Quantity) if isinstance(unit, spu.Quantity)
#} } | {
unit.abbrev: unit
for unit in globals().values()
if isinstance(unit, spu.Quantity)
}
@functools.lru_cache(maxsize=1024)
def parse_abbrev_symbols_to_units(expr: sp.Basic) -> sp.Basic:
print("IN ABBREV", expr)
return expr.subs(ALL_UNIT_SYMBOLS)
#def has_units(expr: sp.Expr): #def has_units(expr: sp.Expr):
# return any( # return any(
# symbol in ALL_UNIT_SYMBOLS # symbol in ALL_UNIT_SYMBOLS

View File

@ -6,12 +6,12 @@ from pydantic_core import core_schema as pyd_core_schema
import sympy as sp import sympy as sp
import sympy.physics.units as spu import sympy.physics.units as spu
from . import extra_sympy_units as spuex from . import extra_sympy_units as spux
#################### ####################
# - Missing Basics # - Missing Basics
#################### ####################
AllowedSympyExprs = sp.Expr | sp.MatrixBase AllowedSympyExprs = sp.Expr | sp.MatrixBase | sp.MutableDenseMatrix
Complex = typx.Annotated[ Complex = typx.Annotated[
complex, complex,
pyd.GetPydanticSchema( pyd.GetPydanticSchema(
@ -36,11 +36,13 @@ class _SympyExpr:
return value return value
try: try:
return sp.sympify(value) expr = sp.sympify(value)
except ValueError as ex: except ValueError as ex:
msg = f"Value {value} is not a `sympify`able string" msg = f"Value {value} is not a `sympify`able string"
raise ValueError(msg) from ex raise ValueError(msg) from ex
return expr.subs(spux.ALL_UNIT_SYMBOLS)
def validate_from_expr(value: AllowedSympyExprs) -> AllowedSympyExprs: def validate_from_expr(value: AllowedSympyExprs) -> AllowedSympyExprs:
if not ( if not (
isinstance(value, sp.Expr) isinstance(value, sp.Expr)
@ -108,7 +110,7 @@ def ConstrSympyExpr(
# Validate Feature Class # Validate Feature Class
if (not allow_variables) and (len(expr.free_symbols) > 0): if (not allow_variables) and (len(expr.free_symbols) > 0):
msgs.add(f"allow_variables={allow_variables} does not match expression {expr}.") msgs.add(f"allow_variables={allow_variables} does not match expression {expr}.")
if (not allow_units) and spuex.uses_units(expr): if (not allow_units) and spux.uses_units(expr):
msgs.add(f"allow_units={allow_units} does not match expression {expr}.") msgs.add(f"allow_units={allow_units} does not match expression {expr}.")
# Validate Structure Class # Validate Structure Class
@ -134,7 +136,7 @@ def ConstrSympyExpr(
# Validate Element Class # Validate Element Class
if allowed_symbols and expr.free_symbols.issubset(allowed_symbols): if allowed_symbols and expr.free_symbols.issubset(allowed_symbols):
msgs.add(f"allowed_symbols={allowed_symbols} does not match expression {expr}") msgs.add(f"allowed_symbols={allowed_symbols} does not match expression {expr}")
if allowed_units and spuex.get_units(expr).issubset(allowed_units): if allowed_units and spux.get_units(expr).issubset(allowed_units):
msgs.add(f"allowed_units={allowed_units} does not match expression {expr}") msgs.add(f"allowed_units={allowed_units} does not match expression {expr}")
# Validate Shape Class # Validate Shape Class

View File

@ -0,0 +1,407 @@
"""Defines a sane interface to the Tidy3D cloud, as constructed by reverse-engineering the official open-source `tidy3d` client library.
- SimulationTask: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_core.py>
- Tidy3D Stub: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/api/tidy3d_stub.py>
"""
from dataclasses import dataclass
import typing as typ
import functools
import datetime as dt
import tidy3d as td
import tidy3d.web as td_web
CloudFolderID = str
CloudFolderName = str
CloudFolder = td_web.core.task_core.Folder
CloudTaskID = str
CloudTaskName = str
CloudTask = td_web.core.task_core.SimulationTask
FileUploadCallback = typ.Callable[[float], None]
## Takes "uploaded bytes" as argument.
####################
# - Module-Level Globals
####################
IS_ONLINE = False
IS_AUTHENTICATED = False
def is_online():
global IS_ONLINE
return IS_ONLINE
def set_online():
global IS_ONLINE
IS_ONLINE = True
def set_offline():
global IS_ONLINE
IS_ONLINE = False
####################
# - Cloud Authentication
####################
def check_authentication() -> bool:
global IS_AUTHENTICATED
global IS_ONLINE
# Check Previous Authentication
## If we authenticated once, we presume that it'll work again.
## TODO: API keys can change... It would just look like "offline" for now.
if IS_AUTHENTICATED:
return True
api_key = td_web.core.http_util.api_key()
if api_key is not None:
try:
td_web.test()
set_online()
except td.exceptions.WebError:
set_offline()
return False
IS_AUTHENTICATED = True
return True
return False
def authenticate_with_api_key(api_key: str) -> bool:
td_web.configure(api_key)
return check_authentication()
####################
# - Cloud Folder
####################
class TidyCloudFolders:
cache_folders: dict[CloudFolderID, CloudFolder] | None = None
####################
# - Folders
####################
@classmethod
def folders(cls) -> dict[CloudFolderID, CloudFolder]:
"""Get all cloud folders as a dict, indexed by ID.
"""
if cls.cache_folders is not None: return cls.cache_folders
try:
cloud_folders = td_web.core.task_core.Folder.list()
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to get cloud folders, but cannot connect to cloud"
raise RuntimeError(msg)
folders = {
cloud_folder.folder_id: cloud_folder
for cloud_folder in cloud_folders
}
cls.cache_folders = folders
return folders
@classmethod
def mk_folder(cls, folder_name: CloudFolderName) -> CloudFolder:
"""Create a cloud folder, raising an exception if it exists.
"""
folders = cls.update_folders()
if folder_name not in {
cloud_folder.folder_name
for cloud_folder in folders.values()
}:
try:
cloud_folder = td_web.core.task_core.Folder.create(folder_name)
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to create cloud folder, but cannot connect to cloud"
raise RuntimeError(msg)
if cls.cache_folders is None: cls.cache_folders = {}
cls.cache_folders[cloud_folder.folder_id] = cloud_folder
return cloud_folder
msg = f"Cannot create cloud folder: Folder '{folder_name}' already exists"
raise ValueError(msg)
@classmethod
def update_folders(cls) -> dict[CloudFolderID, CloudFolder]:
"""Get all cloud folders as a dict, forcing a re-check with the web service.
"""
cls.cache_folders = None
return cls.folders()
## TODO: Support removing folders. Unsure of the semantics (does it recursively delete tasks too?)
####################
# - Cloud Task
####################
@dataclass
class CloudTaskInfo:
"""Toned-down, simplified `dataclass` variant of TaskInfo.
See TaskInfo for more: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_info.py>)
"""
task_name: str
status: str
created_at: dt.datetime
cost_est: typ.Callable[[], float | None]
run_info: typ.Callable[[], tuple[float | None, float | None] | None]
# Timing
completed_at: dt.datetime | None = None ## completedAt
# Cost
cost_real: float | None = None ## realCost
# Sim Properties
task_type: str | None = None ## solverVersion
version_solver: str | None = None ## solverVersion
callback_url: str | None = None ## callbackUrl
class TidyCloudTasks:
"""Greatly simplifies working with Tidy3D Tasks in the Cloud, specifically, via the lowish-level `tidy3d.web.core.task_core.SimulationTask` object.
In particular, cache mechanics ensure that web-requests are only made when absolutely needed.
This greatly improves performance in ex. UI functions.
In particular, `update_task` updates only one task with a single request.
Of particular note are the `SimulationTask` methods that are not abstracted:
- `cloud_task.taskName`: Undocumented, but it works (?)
- `cloud_task.submit()`: Starts the running of a drafted task.
- `cloud_task.real_flex_unit`: `None` until available. Just repeat `update_task` until not None.
- `cloud_task.get_running_info()`: GETs % and field-decay of a running task.
- `cloud_task.get_log(path)`: GET the run log. Remember to use `NamedTemporaryFile` if a stringified log is desired.
"""
cache_tasks: dict[CloudTaskID, CloudTask] = {}
cache_folder_tasks: dict[CloudFolderID, set[CloudTaskID]] = {}
cache_task_info: dict[CloudTaskID, CloudTaskInfo] = {}
@classmethod
def clear_cache(cls):
cls.cache_tasks = {}
####################
# - Task Getters
####################
@classmethod
def task(cls, task_id: CloudTaskID) -> CloudTask | None:
return cls.cache_tasks.get(task_id)
@classmethod
def task_info(cls, task_id: CloudTaskID) -> CloudTaskInfo | None:
return cls.cache_task_info.get(task_id)
@classmethod
def tasks(cls, cloud_folder: CloudFolder) -> dict[CloudTaskID, CloudTask]:
"""Get all cloud tasks within a particular cloud folder as a set.
"""
# Retrieve Cached Tasks
if (task_ids := cls.cache_folder_tasks.get(cloud_folder.folder_id)) is not None:
return {
task_id: cls.cache_tasks[task_id]
for task_id in task_ids
}
# Retrieve Tasks by-Folder
try:
folder_tasks = cloud_folder.list_tasks()
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to get tasks of a cloud folder, but cannot access cloud"
raise RuntimeError(msg)
# No Tasks: Empty Set
if folder_tasks is None:
cls.cache_folder_tasks[cloud_folder.folder_id] = set()
return {}
# Populate Caches
## Direct Task Cache
cloud_tasks = {
cloud_task.task_id: cloud_task
for cloud_task in folder_tasks
}
cls.cache_tasks |= cloud_tasks
## Task Info Cache
for task_id, cloud_task in cloud_tasks.items():
cls.cache_task_info[task_id] = CloudTaskInfo(
task_name=cloud_task.taskName,
status=cloud_task.status,
created_at=cloud_task.created_at,
cost_est=functools.partial(td_web.estimate_cost, cloud_task.task_id),
run_info=cloud_task.get_running_info,
callback_url=cloud_task.callback_url,
)
## Task by-Folder Cache
cls.cache_folder_tasks[cloud_folder.folder_id] = {
task_id
for task_id in cloud_tasks
}
return cloud_tasks
####################
# - Task Create/Delete
####################
@classmethod
def mk_task(
cls,
task_name: CloudTaskName,
cloud_folder: CloudFolder,
sim: td.Simulation,
upload_progress_cb: FileUploadCallback | None = None,
verbose: bool = True,
) -> CloudTask:
"""Creates a `CloudTask` of the given `td.Simulation`.
Presume that `sim.validate_pre_upload()` has already been run, so that the simulation is good to go.
"""
# Create "Stub"
## Minimal Tidy3D object that can be turned into a file for upload
## Has "type" in {"Simulation", "ModeSolver", "HeatSimulation"}
stub = td_web.api.tidy3d_stub.Tidy3dStub(simulation=sim)
# Create Cloud Task
## So far, this is a boring, empty task with no data
## May overlay by name with other tasks - then makes a new "version"
try:
cloud_task = td_web.core.task_core.SimulationTask.create(
task_type=stub.get_type(),
task_name=task_name,
folder_name=cloud_folder.folder_name,
)
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to create cloud task, but cannot access cloud"
raise RuntimeError(msg)
# Upload Simulation to Cloud Task
if not upload_progress_cb is None:
upload_progress_cb = lambda uploaded_bytes: None
try:
cloud_task.upload_simulation(
stub,
verbose=verbose,
#progress_callback=upload_progress_cb,
)
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to upload simulation to cloud task, but cannot access cloud"
raise RuntimeError(msg)
# Populate Caches
## Direct Task Cache
cls.cache_tasks[cloud_task.task_id] = cloud_task
## Task Info Cache
cls.cache_task_info[cloud_task.task_id] = CloudTaskInfo(
task_name=cloud_task.taskName,
status=cloud_task.status,
created_at=cloud_task.created_at,
cost_est=functools.partial(td_web.estimate_cost, cloud_task.task_id),
run_info=cloud_task.get_running_info,
callback_url=cloud_task.callback_url,
)
## Task by-Folder Cache
if cls.cache_folder_tasks.get(cloud_task.folder_id):
cls.cache_folder_tasks[cloud_task.folder_id].add(cloud_task.task_id)
else:
cls.cache_folder_tasks[cloud_task.folder_id] = {cloud_task.task_id}
return cloud_task
####################
# - Task Update/Delete
####################
@classmethod
def rm_task(
cls,
cloud_task: CloudTask,
) -> CloudTask:
"""Deletes a cloud task.
"""
## TODO: Abort first?
task_id = cloud_task.task_id
folder_id = cloud_task.folder_id
try:
cloud_task.delete()
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to delete cloud task, but cannot access cloud"
raise RuntimeError(msg)
# Populate Caches
## Direct Task Cache
cls.cache_tasks.pop(task_id, None)
## Task Info Cache
cls.cache_task_info.pop(task_id, None)
## Task by-Folder Cache
cls.cache_folder_tasks[folder_id].remove(task_id)
@classmethod
def update_task(cls, cloud_task: CloudTask) -> CloudTask:
"""Updates the CloudTask to the latest ex. status attributes.
"""
# BUG: td_web.core.task_core.SimulationTask.get(task_id) doesn't return the `created_at` field.
## Therefore, we unfortunately need to get all tasks for the folder ID just to update one.
# Retrieve Folder
task_id = cloud_task.task_id
folder_id = cloud_task.folder_id
cloud_folder = TidyCloudFolders.folders()[folder_id]
# Repopulate All Caches
## By deleting the folder ID, all tasks within will be reloaded
del cls.cache_folder_tasks[folder_id]
folder_tasks = cls.tasks(cloud_folder)
return cls.tasks(cloud_folder)[task_id]
@classmethod
def update_tasks(cls, folder_id: CloudFolderID) -> dict[CloudTaskID, CloudTask]:
"""Updates the CloudTask to the latest ex. status attributes.
"""
# BUG: td_web.core.task_core.SimulationTask.get(task_id) doesn't return the `created_at` field.
## Therefore, we unfortunately need to get all tasks for the folder ID just to update one.
# Retrieve Folder
cloud_folder = TidyCloudFolders.folders()[folder_id]
# Repopulate All Caches
## By deleting the folder ID, all tasks within will be reloaded
del cls.cache_folder_tasks[folder_id]
folder_tasks = cls.tasks(cloud_folder)
return {
task_id: cls.cache_tasks[task_id]
for task_id in cls.cache_folder_tasks[folder_id]
}
@classmethod
def abort_task(cls, cloud_task: CloudTask) -> CloudTask:
"""Aborts a running CloudTask to the latest ex. status attributes.
"""
## TODO: Check status?
new_cloud_task = cls.update_task(cloud_task)
try:
new_cloud_task.abort()
set_online()
except td.exceptions.WebError:
set_offline()
msg = "Tried to abort cloud task, but cannot access cloud"
raise RuntimeError(msg)
return cls.update_task(cloud_task)