feat: E2E simulation design and analysis.
This is it! This is the milestone. We can now make, run, and analyze simulations in one big chain. Jank remains: - Dynamic enums still need caching, lest the user think to restart Blender while the math nodes are riding high on `FlowPending`. - GN remains untested, and so forth. - Still no plane wave node. Easy to jank together, though. - Still no reindexing in the Transform math, so only frequencies for now. - Active kinds still don't update shape, we still need an explicit (postinit?) directive to do that. - No colors for expr sockets :( But we have a beautifully solid foundation to work on. The new abstractive tools for defining event-driven actions via nodes have had very few showstoppers, and are incredibly nice to work with. There are sharp edges, of course, but generally they only matter where the problem was so very difficult to begin with. We'll start doing physics immediately, and fixing bugs / implementing more nodes as we go.main
parent
695eedca98
commit
b221f9ae2b
|
@ -26,8 +26,8 @@ class OperatorType(enum.StrEnum):
|
||||||
NodeLoadCloudSim = enum.auto()
|
NodeLoadCloudSim = enum.auto()
|
||||||
|
|
||||||
# Node: Tidy3DWebExporter
|
# Node: Tidy3DWebExporter
|
||||||
|
NodeRecomputeSimInfo = enum.auto()
|
||||||
NodeUploadSimulation = enum.auto()
|
NodeUploadSimulation = enum.auto()
|
||||||
|
NodeReleaseUploadedTask = enum.auto()
|
||||||
NodeRunSimulation = enum.auto()
|
NodeRunSimulation = enum.auto()
|
||||||
NodeReloadTrackedTask = enum.auto()
|
NodeReloadTrackedTask = enum.auto()
|
||||||
NodeEstCostTrackedTask = enum.auto()
|
|
||||||
ReleaseTrackedTask = enum.auto()
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ from .flow_signals import FlowSignal
|
||||||
from .icons import Icon
|
from .icons import Icon
|
||||||
from .mobj_types import ManagedObjType
|
from .mobj_types import ManagedObjType
|
||||||
from .node_types import NodeType
|
from .node_types import NodeType
|
||||||
from .sim_types import BoundCondType, SimSpaceAxis, manual_amp_time
|
from .sim_types import BoundCondType, NewSimCloudTask, SimSpaceAxis, manual_amp_time
|
||||||
from .socket_colors import SOCKET_COLORS
|
from .socket_colors import SOCKET_COLORS
|
||||||
from .socket_types import SocketType
|
from .socket_types import SocketType
|
||||||
from .tree_types import TreeType
|
from .tree_types import TreeType
|
||||||
|
@ -79,6 +79,7 @@ __all__ = [
|
||||||
'BLSocketType',
|
'BLSocketType',
|
||||||
'NodeType',
|
'NodeType',
|
||||||
'BoundCondType',
|
'BoundCondType',
|
||||||
|
'NewSimCloudTask',
|
||||||
'SimSpaceAxis',
|
'SimSpaceAxis',
|
||||||
'manual_amp_time',
|
'manual_amp_time',
|
||||||
'NodeCategory',
|
'NodeCategory',
|
||||||
|
|
|
@ -38,6 +38,7 @@ class NodeType(blender_type_enum.BlenderTypeEnum):
|
||||||
Viewer = enum.auto()
|
Viewer = enum.auto()
|
||||||
## Outputs / File Exporters
|
## Outputs / File Exporters
|
||||||
Tidy3DWebExporter = enum.auto()
|
Tidy3DWebExporter = enum.auto()
|
||||||
|
Tidy3DWebRunner = enum.auto()
|
||||||
## Outputs / Web Exporters
|
## Outputs / Web Exporters
|
||||||
JSONFileExporter = enum.auto()
|
JSONFileExporter = enum.auto()
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
"""Declares various simulation types for use by nodes and sockets."""
|
"""Declares various simulation types for use by nodes and sockets."""
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
import tidy3d as td
|
import tidy3d as td
|
||||||
|
|
||||||
|
from blender_maxwell.services import tdcloud
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - JAX-Helpers
|
||||||
|
####################
|
||||||
def manual_amp_time(self, time: float) -> complex:
|
def manual_amp_time(self, time: float) -> complex:
|
||||||
"""Copied implementation of `pulse.amp_time` for `tidy3d` temporal shapes, which replaces use of `numpy` with `jax.numpy` for `jit`-ability.
|
"""Copied implementation of `pulse.amp_time` for `tidy3d` temporal shapes, which replaces use of `numpy` with `jax.numpy` for `jit`-ability.
|
||||||
|
|
||||||
|
@ -42,6 +48,9 @@ def manual_amp_time(self, time: float) -> complex:
|
||||||
## TODO: Sim Domain type, w/pydantic checks!
|
## TODO: Sim Domain type, w/pydantic checks!
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Global Simulation Coordinate System
|
||||||
|
####################
|
||||||
class SimSpaceAxis(enum.StrEnum):
|
class SimSpaceAxis(enum.StrEnum):
|
||||||
"""The axis labels of the global simulation coordinate system."""
|
"""The axis labels of the global simulation coordinate system."""
|
||||||
|
|
||||||
|
@ -89,6 +98,9 @@ class SimSpaceAxis(enum.StrEnum):
|
||||||
return {SSA.X: 0, SSA.Y: 1, SSA.Z: 2}[self]
|
return {SSA.X: 0, SSA.Y: 1, SSA.Z: 2}[self]
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Boundary Condition Type
|
||||||
|
####################
|
||||||
class BoundCondType(enum.StrEnum):
|
class BoundCondType(enum.StrEnum):
|
||||||
r"""A type of boundary condition, applied to a half-axis of a simulation domain.
|
r"""A type of boundary condition, applied to a half-axis of a simulation domain.
|
||||||
|
|
||||||
|
@ -151,3 +163,14 @@ class BoundCondType(enum.StrEnum):
|
||||||
BCT.Pmc: td.PMCBoundary(),
|
BCT.Pmc: td.PMCBoundary(),
|
||||||
BCT.NaiveBloch: td.Periodic(),
|
BCT.NaiveBloch: td.Periodic(),
|
||||||
}[self]
|
}[self]
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Cloud Task
|
||||||
|
####################
|
||||||
|
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||||
|
class NewSimCloudTask:
|
||||||
|
"""Not-yet-existing simulation-oriented cloud task."""
|
||||||
|
|
||||||
|
task_name: tdcloud.CloudTaskName
|
||||||
|
cloud_folder: tdcloud.CloudFolder
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
from . import tidy_3d_web_importer
|
#from . import tidy_3d_web_importer
|
||||||
|
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
*tidy_3d_web_importer.BL_REGISTER,
|
#*tidy_3d_web_importer.BL_REGISTER,
|
||||||
]
|
]
|
||||||
BL_NODES = {
|
BL_NODES = {
|
||||||
**tidy_3d_web_importer.BL_NODES,
|
#**tidy_3d_web_importer.BL_NODES,
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,16 +48,6 @@ class LoadCloudSim(bpy.types.Operator):
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
def _sim_data_cache_path(task_id: str) -> Path:
|
|
||||||
"""Compute an appropriate location for caching simulations downloaded from the internet, unique to each task ID.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
task_id: The ID of the Tidy3D cloud task.
|
|
||||||
"""
|
|
||||||
(ct.addon.ADDON_CACHE / task_id).mkdir(exist_ok=True)
|
|
||||||
return ct.addon.ADDON_CACHE / task_id / 'sim_data.hdf5'
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Node
|
# - Node
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
from . import tidy3d_web_exporter
|
from . import tidy3d_web_exporter, tidy3d_web_runner
|
||||||
|
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
*tidy3d_web_exporter.BL_REGISTER,
|
*tidy3d_web_exporter.BL_REGISTER,
|
||||||
|
*tidy3d_web_runner.BL_REGISTER,
|
||||||
]
|
]
|
||||||
BL_NODES = {
|
BL_NODES = {
|
||||||
**tidy3d_web_exporter.BL_NODES,
|
**tidy3d_web_exporter.BL_NODES,
|
||||||
|
**tidy3d_web_runner.BL_NODES,
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,38 @@ log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Web Uploader / Loader / Runner / Releaser
|
# - Operators
|
||||||
####################
|
####################
|
||||||
|
class RecomputeSimInfo(bpy.types.Operator):
|
||||||
|
bl_idname = ct.OperatorType.NodeRecomputeSimInfo
|
||||||
|
bl_label = 'Recompute Tidy3D Sim Info'
|
||||||
|
bl_description = 'Recompute info for any currently attached sim info'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (
|
||||||
|
# Check Tidy3DWebExporter is Accessible
|
||||||
|
hasattr(context, 'node')
|
||||||
|
and hasattr(context.node, 'node_type')
|
||||||
|
and context.node.node_type == ct.NodeType.Tidy3DWebExporter
|
||||||
|
# Check Sim is Available (aka. uploadeable)
|
||||||
|
and context.node.sim_info_available
|
||||||
|
and context.node.sim_info_invalidated
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
node = context.node
|
||||||
|
|
||||||
|
# Rehydrate the Cache
|
||||||
|
node.total_monitor_data = bl_cache.Signal.InvalidateCache
|
||||||
|
node.is_sim_uploadable = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
|
# Remove the Invalidation Marker
|
||||||
|
## -> This is OK, since we manually guaranteed that it's available.
|
||||||
|
node.sim_info_invalidated = False
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class UploadSimulation(bpy.types.Operator):
|
class UploadSimulation(bpy.types.Operator):
|
||||||
bl_idname = ct.OperatorType.NodeUploadSimulation
|
bl_idname = ct.OperatorType.NodeUploadSimulation
|
||||||
bl_label = 'Upload Tidy3D Simulation'
|
bl_label = 'Upload Tidy3D Simulation'
|
||||||
|
@ -24,119 +54,48 @@ class UploadSimulation(bpy.types.Operator):
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (
|
return (
|
||||||
hasattr(context, 'node')
|
# Check Tidy3D Cloud
|
||||||
|
tdcloud.IS_AUTHENTICATED
|
||||||
|
# Check Tidy3DWebExporter is Accessible
|
||||||
|
and hasattr(context, 'node')
|
||||||
and hasattr(context.node, 'node_type')
|
and hasattr(context.node, 'node_type')
|
||||||
and context.node.node_type == ct.NodeType.Tidy3DWebExporter
|
and context.node.node_type == ct.NodeType.Tidy3DWebExporter
|
||||||
and context.node.lock_tree
|
# Check Sim is Available (aka. uploadeable)
|
||||||
and tdcloud.IS_AUTHENTICATED
|
and context.node.is_sim_uploadable
|
||||||
and not context.node.tracked_task_id
|
and context.node.uploaded_task_id == ''
|
||||||
and context.node.inputs['Sim'].is_linked
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
node = context.node
|
node = context.node
|
||||||
node.upload_sim()
|
cloud_task = tdcloud.TidyCloudTasks.mk_task(
|
||||||
|
task_name=node.new_cloud_task.task_name,
|
||||||
|
cloud_folder=node.new_cloud_task.cloud_folder,
|
||||||
|
sim=node.sim,
|
||||||
|
verbose=True,
|
||||||
|
)
|
||||||
|
node.uploaded_task_id = cloud_task.task_id
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class RunSimulation(bpy.types.Operator):
|
class ReleaseUploadedTask(bpy.types.Operator):
|
||||||
bl_idname = ct.OperatorType.NodeRunSimulation
|
bl_idname = ct.OperatorType.NodeReleaseUploadedTask
|
||||||
bl_label = 'Run Tracked Tidy3D Sim'
|
|
||||||
bl_description = 'Run 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
|
|
||||||
and (
|
|
||||||
task_info := tdcloud.TidyCloudTasks.task_info(
|
|
||||||
context.node.tracked_task_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is not None
|
|
||||||
and task_info.status == 'draft'
|
|
||||||
)
|
|
||||||
|
|
||||||
def execute(self, context):
|
|
||||||
node = context.node
|
|
||||||
node.run_tracked_task()
|
|
||||||
return {'FINISHED'}
|
|
||||||
|
|
||||||
|
|
||||||
class ReloadTrackedTask(bpy.types.Operator):
|
|
||||||
bl_idname = ct.OperatorType.NodeReloadTrackedTask
|
|
||||||
bl_label = 'Reload Tracked Tidy3D Cloud Task'
|
|
||||||
bl_description = 'Reload 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
|
|
||||||
if (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'}
|
|
||||||
|
|
||||||
|
|
||||||
class EstCostTrackedTask(bpy.types.Operator):
|
|
||||||
bl_idname = ct.OperatorType.NodeEstCostTrackedTask
|
|
||||||
bl_label = 'Est Cost of Tracked Tidy3D Cloud Task'
|
|
||||||
bl_description = 'Reload 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
|
|
||||||
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 = ct.OperatorType.ReleaseTrackedTask
|
|
||||||
bl_label = 'Release Tracked Tidy3D Cloud Task'
|
bl_label = 'Release Tracked Tidy3D Cloud Task'
|
||||||
bl_description = 'Release the currently tracked simulation task'
|
bl_description = 'Release the currently tracked simulation task'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (
|
return (
|
||||||
|
# Check Tidy3DWebExporter is Accessible
|
||||||
hasattr(context, 'node')
|
hasattr(context, 'node')
|
||||||
and hasattr(context.node, 'node_type')
|
and hasattr(context.node, 'node_type')
|
||||||
and context.node.node_type == ct.NodeType.Tidy3DWebExporter
|
and context.node.node_type == ct.NodeType.Tidy3DWebExporter
|
||||||
# and tdcloud.IS_AUTHENTICATED
|
# Check Sim is Available (aka. uploadeable)
|
||||||
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.tracked_task_id = ''
|
node.uploaded_task_id = ''
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
@ -154,9 +113,6 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
output_sockets: typ.ClassVar = {
|
output_sockets: typ.ClassVar = {
|
||||||
'Sim Data': sockets.Tidy3DCloudTaskSocketDef(
|
|
||||||
should_exist=True,
|
|
||||||
),
|
|
||||||
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
|
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
|
||||||
should_exist=True,
|
should_exist=True,
|
||||||
),
|
),
|
||||||
|
@ -165,11 +121,12 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
lock_tree: bool = bl_cache.BLField(False, prop_ui=True)
|
sim_info_available: bool = bl_cache.BLField(False)
|
||||||
tracked_task_id: str = bl_cache.BLField('', prop_ui=True)
|
sim_info_invalidated: bool = bl_cache.BLField(False)
|
||||||
|
uploaded_task_id: str = bl_cache.BLField('')
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Computed
|
# - Computed - Sim
|
||||||
####################
|
####################
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property(persist=False)
|
||||||
def sim(self) -> td.Simulation | None:
|
def sim(self) -> td.Simulation | None:
|
||||||
|
@ -177,67 +134,90 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
has_sim = not ct.FlowSignal.check(sim)
|
has_sim = not ct.FlowSignal.check(sim)
|
||||||
|
|
||||||
if has_sim:
|
if has_sim:
|
||||||
sim.validate_pre_upload(source_required=True)
|
|
||||||
return sim
|
return sim
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property(persist=False)
|
||||||
def total_monitor_data(self) -> float:
|
def total_monitor_data(self) -> float | None:
|
||||||
if self.sim is not None:
|
if self.sim is not None:
|
||||||
return sum(self.sim.monitors_data_size.values())
|
return sum(self.sim.monitors_data_size.values())
|
||||||
return 0.0
|
return None
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed - New Cloud Task
|
||||||
|
####################
|
||||||
|
@property
|
||||||
|
def new_cloud_task(self) -> ct.NewSimCloudTask | None:
|
||||||
|
"""Retrieve the current new cloud task from the input socket.
|
||||||
|
|
||||||
|
If one can't be loaded, return None.
|
||||||
|
"""
|
||||||
|
new_cloud_task = self._compute_input(
|
||||||
|
'Cloud Task',
|
||||||
|
kind=ct.FlowKind.Value,
|
||||||
|
)
|
||||||
|
has_new_cloud_task = not ct.FlowSignal.check(new_cloud_task)
|
||||||
|
|
||||||
|
if has_new_cloud_task and new_cloud_task.task_name != '':
|
||||||
|
return new_cloud_task
|
||||||
|
return None
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed - Uploaded Cloud Task
|
||||||
|
####################
|
||||||
|
@property
|
||||||
|
def uploaded_task(self) -> tdcloud.CloudTask | None:
|
||||||
|
"""Retrieve the uploaded cloud task.
|
||||||
|
|
||||||
|
If one can't be loaded, return None.
|
||||||
|
"""
|
||||||
|
has_uploaded_task = self.uploaded_task_id != ''
|
||||||
|
|
||||||
|
if has_uploaded_task:
|
||||||
|
return tdcloud.TidyCloudTasks.task(self.uploaded_task_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uploaded_task_info(self) -> tdcloud.CloudTask | None:
|
||||||
|
"""Retrieve the uploaded cloud task.
|
||||||
|
|
||||||
|
If one can't be loaded, return None.
|
||||||
|
"""
|
||||||
|
has_uploaded_task = self.uploaded_task_id != ''
|
||||||
|
|
||||||
|
if has_uploaded_task:
|
||||||
|
return tdcloud.TidyCloudTasks.task_info(self.uploaded_task_id)
|
||||||
|
return None
|
||||||
|
|
||||||
@bl_cache.cached_bl_property(persist=False)
|
@bl_cache.cached_bl_property(persist=False)
|
||||||
def est_cost(self) -> float | None:
|
def uploaded_est_cost(self) -> float | None:
|
||||||
if self.tracked_task_id != '':
|
task_info = self.uploaded_task_info
|
||||||
task_info = tdcloud.TidyCloudTasks.task_info(self.tracked_task_id)
|
|
||||||
if task_info is not None:
|
if task_info is not None:
|
||||||
return task_info.cost_est()
|
est_cost = task_info.cost_est()
|
||||||
|
if est_cost is not None:
|
||||||
|
return est_cost
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Methods
|
# - Computed - Combined
|
||||||
####################
|
####################
|
||||||
def upload_sim(self):
|
@bl_cache.cached_bl_property(persist=False)
|
||||||
if self.sim is None:
|
def is_sim_uploadable(self) -> bool:
|
||||||
msg = 'Tried to upload simulation, but none is attached'
|
if (
|
||||||
raise ValueError(msg)
|
self.sim is not None
|
||||||
|
and self.uploaded_task_id == ''
|
||||||
if (new_task := self._compute_input('Cloud Task')) is None or isinstance(
|
and self.new_cloud_task is not None
|
||||||
new_task,
|
and self.new_cloud_task.task_name != ''
|
||||||
tdcloud.CloudTask,
|
|
||||||
):
|
):
|
||||||
msg = (
|
try:
|
||||||
'Tried to upload simulation to new task, but existing task was selected'
|
self.sim.validate_pre_upload(source_required=True)
|
||||||
)
|
except:
|
||||||
raise ValueError(msg)
|
log.exception()
|
||||||
|
return False
|
||||||
# Create Cloud Task
|
else:
|
||||||
cloud_task = tdcloud.TidyCloudTasks.mk_task(
|
return True
|
||||||
task_name=new_task[0],
|
return False
|
||||||
cloud_folder=new_task[1],
|
|
||||||
sim=self.sim,
|
|
||||||
verbose=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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'].on_new_task_created(cloud_task)
|
|
||||||
|
|
||||||
# Track the Newly Uploaded Task ID
|
|
||||||
self.tracked_task_id = cloud_task.task_id
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
cloud_task.submit()
|
|
||||||
tdcloud.TidyCloudTasks.update_task(
|
|
||||||
cloud_task
|
|
||||||
) ## TODO: Check that status is actually immediately updated.
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
|
@ -249,21 +229,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
ct.OperatorType.NodeUploadSimulation,
|
ct.OperatorType.NodeUploadSimulation,
|
||||||
text='Upload',
|
text='Upload',
|
||||||
)
|
)
|
||||||
tree_lock_icon = 'LOCKED' if self.lock_tree else 'UNLOCKED'
|
if self.uploaded_task_id:
|
||||||
row.prop(
|
|
||||||
self, self.blfields['lock_tree'], toggle=True, icon=tree_lock_icon, text=''
|
|
||||||
)
|
|
||||||
|
|
||||||
# Row: Run Sim Buttons
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.operator(
|
row.operator(
|
||||||
ct.OperatorType.NodeRunSimulation,
|
ct.OperatorType.NodeReleaseUploadedTask,
|
||||||
text='Run',
|
|
||||||
)
|
|
||||||
if self.tracked_task_id:
|
|
||||||
tree_lock_icon = 'LOOP_BACK'
|
|
||||||
row.operator(
|
|
||||||
ct.OperatorType.ReleaseTrackedTask,
|
|
||||||
icon='LOOP_BACK',
|
icon='LOOP_BACK',
|
||||||
text='',
|
text='',
|
||||||
)
|
)
|
||||||
|
@ -290,54 +258,30 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
col.label(icon=conn_icon)
|
col.label(icon=conn_icon)
|
||||||
|
|
||||||
# Simulation Info
|
# Simulation Info
|
||||||
if self.inputs['Sim'].is_linked:
|
if self.sim is not None:
|
||||||
row = layout.row()
|
row = layout.row()
|
||||||
row.alignment = 'CENTER'
|
row.alignment = 'CENTER'
|
||||||
row.label(text='Sim Info')
|
row.label(text='Sim Info')
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
split = box.split(factor=0.4)
|
|
||||||
|
if self.sim_info_invalidated:
|
||||||
|
box.operator(ct.OperatorType.NodeRecomputeSimInfo, text='Regenerate')
|
||||||
|
else:
|
||||||
|
split = box.split(factor=0.5)
|
||||||
|
|
||||||
## Split: Left Column
|
## Split: Left Column
|
||||||
col = split.column(align=False)
|
col = split.column(align=False)
|
||||||
col.label(text='𝝨 Output')
|
col.label(text='𝝨 Data')
|
||||||
|
|
||||||
## Split: Right Column
|
## Split: Right Column
|
||||||
col = split.column(align=False)
|
col = split.column(align=False)
|
||||||
col.alignment = 'RIGHT'
|
col.alignment = 'RIGHT'
|
||||||
col.label(text=f'{self.total_monitor_data / 1_000_000:.2f}MB')
|
col.label(text=f'{self.total_monitor_data / 1_000_000:.2f}MB')
|
||||||
|
|
||||||
# Cloud Task Info
|
if self.uploaded_task_info is not None:
|
||||||
if self.tracked_task_id and tdcloud.IS_AUTHENTICATED:
|
# Uploaded Task Information
|
||||||
task_info = tdcloud.TidyCloudTasks.task_info(self.tracked_task_id)
|
|
||||||
if task_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
## Header
|
|
||||||
row = layout.row()
|
|
||||||
row.alignment = 'CENTER'
|
|
||||||
row.label(text='Task Info')
|
|
||||||
|
|
||||||
## Progress Bar
|
|
||||||
row = layout.row(align=True)
|
|
||||||
row.progress(
|
|
||||||
factor=0.0,
|
|
||||||
type='BAR',
|
|
||||||
text=f'Status: {task_info.status.capitalize()}',
|
|
||||||
)
|
|
||||||
row.operator(
|
|
||||||
ct.OperatorType.NodeReloadTrackedTask,
|
|
||||||
text='',
|
|
||||||
icon='FILE_REFRESH',
|
|
||||||
)
|
|
||||||
row.operator(
|
|
||||||
ct.OperatorType.NodeEstCostTrackedTask,
|
|
||||||
text='',
|
|
||||||
icon='SORTTIME',
|
|
||||||
)
|
|
||||||
|
|
||||||
## Information
|
|
||||||
box = layout.box()
|
box = layout.box()
|
||||||
split = box.split(factor=0.4)
|
split = box.split(factor=0.6)
|
||||||
|
|
||||||
## Split: Left Column
|
## Split: Left Column
|
||||||
col = split.column(align=False)
|
col = split.column(align=False)
|
||||||
|
@ -346,16 +290,20 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
col.label(text='Real Cost')
|
col.label(text='Real Cost')
|
||||||
|
|
||||||
## Split: Right Column
|
## Split: Right Column
|
||||||
cost_est = f'{self.est_cost:.2f}' if self.est_cost >= 0 else 'TBD'
|
cost_est = (
|
||||||
|
f'{self.uploaded_est_cost:.2f}'
|
||||||
|
if self.uploaded_est_cost is not None
|
||||||
|
else 'TBD'
|
||||||
|
)
|
||||||
cost_real = (
|
cost_real = (
|
||||||
f'{task_info.cost_real:.2f}'
|
f'{self.uploaded_task_info.cost_real:.2f}'
|
||||||
if task_info.cost_real is not None
|
if self.uploaded_task_info.cost_real is not None
|
||||||
else 'TBD'
|
else 'TBD'
|
||||||
)
|
)
|
||||||
|
|
||||||
col = split.column(align=False)
|
col = split.column(align=False)
|
||||||
col.alignment = 'RIGHT'
|
col.alignment = 'RIGHT'
|
||||||
col.label(text=task_info.status.capitalize())
|
col.label(text=self.uploaded_task_info.status.capitalize())
|
||||||
col.label(text=f'{cost_est} creds')
|
col.label(text=f'{cost_est} creds')
|
||||||
col.label(text=f'{cost_real} creds')
|
col.label(text=f'{cost_real} creds')
|
||||||
|
|
||||||
|
@ -364,43 +312,90 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
# - Events
|
# - Events
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(prop_name='lock_tree', props={'lock_tree'})
|
@events.on_value_changed(
|
||||||
def on_lock_tree_changed(self, props):
|
socket_name='Sim',
|
||||||
if props['lock_tree']:
|
run_on_init=True,
|
||||||
|
props={'sim_info_available', 'sim_info_invalidated'},
|
||||||
|
)
|
||||||
|
def on_sim_changed(self, props) -> None:
|
||||||
|
# Sim Linked | First Value Change
|
||||||
|
if self.inputs['Sim'].is_linked and not props['sim_info_available']:
|
||||||
|
log.critical('First Change: Mark Sim Info Available')
|
||||||
|
self.sim = bl_cache.Signal.InvalidateCache
|
||||||
|
self.total_monitor_data = bl_cache.Signal.InvalidateCache
|
||||||
|
self.is_sim_uploadable = bl_cache.Signal.InvalidateCache
|
||||||
|
self.sim_info_available = True
|
||||||
|
|
||||||
|
# Sim Linked | Second Value Change
|
||||||
|
if (
|
||||||
|
self.inputs['Sim'].is_linked
|
||||||
|
and props['sim_info_available']
|
||||||
|
and not props['sim_info_invalidated']
|
||||||
|
):
|
||||||
|
log.critical('Second Change: Mark Sim Info Invalided')
|
||||||
|
self.sim_info_invalidated = True
|
||||||
|
|
||||||
|
# Sim Linked | Nth Time
|
||||||
|
## -> Danger of infinite expensive recompute of the sim every change.
|
||||||
|
## -> Instead, user must manually set "available & not invalidated".
|
||||||
|
## -> The UI should explain that the caches are dry.
|
||||||
|
## -> The UI should also provide such a "hydration" button.
|
||||||
|
|
||||||
|
# Sim Not Linked
|
||||||
|
## -> If the sim is straight-up not available, cache needs changing.
|
||||||
|
## -> Luckily, since we know there's no sim, invalidation is cheap.
|
||||||
|
## -> Ends up being a "circuit breaker" for sim_info_invalidated.
|
||||||
|
elif not self.inputs['Sim'].is_linked:
|
||||||
|
log.critical('Unlinked: Short Circuit Zap Cache')
|
||||||
|
self.sim = bl_cache.Signal.InvalidateCache
|
||||||
|
self.total_monitor_data = bl_cache.Signal.InvalidateCache
|
||||||
|
self.is_sim_uploadable = bl_cache.Signal.InvalidateCache
|
||||||
|
self.sim_info_available = False
|
||||||
|
self.sim_info_invalidated = False
|
||||||
|
|
||||||
|
@events.on_value_changed(
|
||||||
|
socket_name='Cloud Task',
|
||||||
|
run_on_init=True,
|
||||||
|
)
|
||||||
|
def on_new_cloud_task_changed(self):
|
||||||
|
self.is_sim_uploadable = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
|
@events.on_value_changed(
|
||||||
|
# Trigger
|
||||||
|
prop_name='uploaded_task_id',
|
||||||
|
run_on_init=True,
|
||||||
|
# Loaded
|
||||||
|
props={'uploaded_task_id'},
|
||||||
|
)
|
||||||
|
def on_uploaded_task_changed(self, props):
|
||||||
|
log.critical('Uploaded Task Changed')
|
||||||
|
self.is_sim_uploadable = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
|
if props['uploaded_task_id'] != '':
|
||||||
self.trigger_event(ct.FlowEvent.EnableLock)
|
self.trigger_event(ct.FlowEvent.EnableLock)
|
||||||
self.locked = False
|
self.locked = False
|
||||||
for bl_socket in self.inputs:
|
|
||||||
if bl_socket.name == 'Sim':
|
|
||||||
continue
|
|
||||||
bl_socket.locked = False
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.trigger_event(ct.FlowEvent.DisableLock)
|
self.trigger_event(ct.FlowEvent.DisableLock)
|
||||||
|
|
||||||
@events.on_value_changed(prop_name='tracked_task_id', props={'tracked_task_id'})
|
max_tries = 10
|
||||||
def on_tracked_task_id_changed(self, props):
|
for _ in range(max_tries):
|
||||||
if props['tracked_task_id']:
|
self.uploaded_est_cost = bl_cache.Signal.InvalidateCache
|
||||||
self.inputs['Cloud Task'].locked = True
|
if self.uploaded_est_cost is not None:
|
||||||
|
break
|
||||||
else:
|
|
||||||
self.total_monitor_data = bl_cache.Signal.InvalidateCache
|
|
||||||
self.est_cost = bl_cache.Signal.InvalidateCache
|
|
||||||
self.inputs['Cloud Task'].on_prepare_new_task()
|
|
||||||
self.inputs['Cloud Task'].locked = False
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Output Methods
|
# - Outputs
|
||||||
####################
|
####################
|
||||||
## TODO: Retrieve simulation data if/when the simulation is done
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Cloud Task',
|
'Cloud Task',
|
||||||
input_sockets={'Cloud Task'},
|
props={'uploaded_task_id', 'uploaded_task'},
|
||||||
)
|
)
|
||||||
def compute_cloud_task(self, input_sockets: dict) -> tdcloud.CloudTask | None:
|
def compute_cloud_task(self, props) -> tdcloud.CloudTask | None:
|
||||||
if isinstance(cloud_task := input_sockets['Cloud Task'], tdcloud.CloudTask):
|
if props['uploaded_task_id'] != '':
|
||||||
return cloud_task
|
return props['uploaded_task']
|
||||||
|
|
||||||
return None
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -408,11 +403,9 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
UploadSimulation,
|
UploadSimulation,
|
||||||
RunSimulation,
|
ReleaseUploadedTask,
|
||||||
ReloadTrackedTask,
|
|
||||||
EstCostTrackedTask,
|
|
||||||
ReleaseTrackedTask,
|
|
||||||
Tidy3DWebExporterNode,
|
Tidy3DWebExporterNode,
|
||||||
|
RecomputeSimInfo,
|
||||||
]
|
]
|
||||||
BL_NODES = {
|
BL_NODES = {
|
||||||
ct.NodeType.Tidy3DWebExporter: (ct.NodeCategory.MAXWELLSIM_OUTPUTS_WEBEXPORTERS)
|
ct.NodeType.Tidy3DWebExporter: (ct.NodeCategory.MAXWELLSIM_OUTPUTS_WEBEXPORTERS)
|
||||||
|
|
|
@ -0,0 +1,270 @@
|
||||||
|
import typing as typ
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import tidy3d as td
|
||||||
|
|
||||||
|
from blender_maxwell.services import tdcloud
|
||||||
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
|
|
||||||
|
from .... import contracts as ct
|
||||||
|
from .... import sockets
|
||||||
|
from ... import base, events
|
||||||
|
|
||||||
|
log = logger.get(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Operators
|
||||||
|
####################
|
||||||
|
class RunSimulation(bpy.types.Operator):
|
||||||
|
"""Run a Tidy3D simulation accessible from a `Tidy3DWebRunnerNode`."""
|
||||||
|
|
||||||
|
bl_idname = ct.OperatorType.NodeRunSimulation
|
||||||
|
bl_label = 'Run Sim'
|
||||||
|
bl_description = 'Run the currently tracked simulation task'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (
|
||||||
|
# Check Tidy3D Cloud
|
||||||
|
tdcloud.IS_AUTHENTICATED
|
||||||
|
# Check Tidy3DWebRunnerNode is Accessible
|
||||||
|
and hasattr(context, 'node')
|
||||||
|
and hasattr(context.node, 'node_type')
|
||||||
|
and context.node.node_type == ct.NodeType.Tidy3DWebRunner
|
||||||
|
# Check Task is Runnable
|
||||||
|
and context.node.is_task_runnable
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
node = context.node
|
||||||
|
node.cloud_task.submit()
|
||||||
|
|
||||||
|
## TODO: Start modal timer that checks progress in a subprocess.
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class ReloadTrackedTask(bpy.types.Operator):
|
||||||
|
"""Reload information of the selected task in a `Tidy3DWebRunnerNode`."""
|
||||||
|
|
||||||
|
bl_idname = ct.OperatorType.NodeReloadTrackedTask
|
||||||
|
bl_label = 'Reload Tracked Tidy3D Cloud Task'
|
||||||
|
bl_description = 'Reload the currently tracked simulation task'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll(cls, context):
|
||||||
|
return (
|
||||||
|
# Check Tidy3D Cloud
|
||||||
|
tdcloud.IS_AUTHENTICATED
|
||||||
|
# Check Tidy3DWebRunnerNode is Accessible
|
||||||
|
and hasattr(context, 'node')
|
||||||
|
and hasattr(context.node, 'node_type')
|
||||||
|
and context.node.node_type == ct.NodeType.Tidy3DWebRunner
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
node = context.node
|
||||||
|
tdcloud.TidyCloudTasks.update_task(node.cloud_task)
|
||||||
|
node.sim_data = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Node
|
||||||
|
####################
|
||||||
|
class Tidy3DWebRunnerNode(base.MaxwellSimNode):
|
||||||
|
node_type = ct.NodeType.Tidy3DWebRunner
|
||||||
|
bl_label = 'Tidy3D Web Runner'
|
||||||
|
|
||||||
|
input_sockets: typ.ClassVar = {
|
||||||
|
'Cloud Task': sockets.Tidy3DCloudTaskSocketDef(
|
||||||
|
should_exist=True, ## Ensure it is never NewSimCloudTask
|
||||||
|
),
|
||||||
|
}
|
||||||
|
output_sockets: typ.ClassVar = {
|
||||||
|
'Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
|
||||||
|
}
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed (Cached)
|
||||||
|
####################
|
||||||
|
@property
|
||||||
|
def cloud_task(self) -> tdcloud.CloudTask | None:
|
||||||
|
"""Retrieve the current cloud task from the input socket.
|
||||||
|
|
||||||
|
If one can't be loaded, return None.
|
||||||
|
"""
|
||||||
|
cloud_task = self._compute_input(
|
||||||
|
'Cloud Task',
|
||||||
|
kind=ct.FlowKind.Value,
|
||||||
|
)
|
||||||
|
has_cloud_task = not ct.FlowSignal.check(cloud_task)
|
||||||
|
|
||||||
|
if has_cloud_task:
|
||||||
|
return cloud_task
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def task_info(self) -> tdcloud.CloudTaskInfo | None:
|
||||||
|
"""Retrieve the current cloud task information from the input socket.
|
||||||
|
|
||||||
|
If it can't be loaded, return None.
|
||||||
|
"""
|
||||||
|
cloud_task = self.cloud_task
|
||||||
|
if cloud_task is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Retrieve Task Info
|
||||||
|
task_info = tdcloud.TidyCloudTasks.task_info(cloud_task.task_id)
|
||||||
|
if task_info is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return task_info
|
||||||
|
|
||||||
|
@bl_cache.cached_bl_property(persist=False)
|
||||||
|
def sim_data(self) -> td.Simulation | None:
|
||||||
|
"""Retrieve the simulation data of the current cloud task from the input socket.
|
||||||
|
|
||||||
|
If it can't be loaded, return None.
|
||||||
|
"""
|
||||||
|
task_info = self.task_info
|
||||||
|
if task_info is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if task_info.status == 'success':
|
||||||
|
# Download Sim Data
|
||||||
|
## -> self.cloud_task really shouldn't be able to be None here.
|
||||||
|
## -> So, we check it by applying the Ostrich method.
|
||||||
|
sim_data = tdcloud.TidyCloudTasks.download_task_sim_data(
|
||||||
|
self.cloud_task,
|
||||||
|
tdcloud.TidyCloudTasks.task_info(
|
||||||
|
self.cloud_task.task_id
|
||||||
|
).disk_cache_path(ct.addon.ADDON_CACHE),
|
||||||
|
)
|
||||||
|
if sim_data is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return sim_data
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Computed (Uncached)
|
||||||
|
####################
|
||||||
|
@property
|
||||||
|
def is_task_runnable(self) -> bool:
|
||||||
|
"""Checks whether all conditions are satisfied to be able to actually run a simulation."""
|
||||||
|
if self.task_info is not None:
|
||||||
|
return self.task_info.status == 'draft'
|
||||||
|
## TODO: Rely on Visible Cost Estimate
|
||||||
|
return False
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - UI
|
||||||
|
####################
|
||||||
|
def draw_operators(self, context, layout):
|
||||||
|
# Row: Run Sim Buttons
|
||||||
|
row = layout.row(align=True)
|
||||||
|
row.operator(
|
||||||
|
ct.OperatorType.NodeRunSimulation,
|
||||||
|
text='Run Sim',
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw_info(self, context, layout):
|
||||||
|
# Connection Info
|
||||||
|
auth_icon = 'CHECKBOX_HLT' if tdcloud.IS_AUTHENTICATED else 'CHECKBOX_DEHLT'
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Cloud Task Info
|
||||||
|
if self.task_info is not None:
|
||||||
|
# Header
|
||||||
|
row = layout.row()
|
||||||
|
row.alignment = 'CENTER'
|
||||||
|
row.label(text='Task Info')
|
||||||
|
|
||||||
|
# Task Run Progress
|
||||||
|
# row = layout.row(align=True)
|
||||||
|
# row.progress(
|
||||||
|
# factor=0.0,
|
||||||
|
# type='BAR',
|
||||||
|
# text=f'Status: {self.task_info.status.capitalize()}',
|
||||||
|
# )
|
||||||
|
row.operator(
|
||||||
|
ct.OperatorType.NodeReloadTrackedTask,
|
||||||
|
text='',
|
||||||
|
icon='FILE_REFRESH',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Task 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='Real Cost')
|
||||||
|
|
||||||
|
## Split: Right Column
|
||||||
|
cost_real = (
|
||||||
|
f'{self.task_info.cost_real:.2f}'
|
||||||
|
if self.task_info.cost_real is not None
|
||||||
|
else 'TBD'
|
||||||
|
)
|
||||||
|
|
||||||
|
col = split.column(align=False)
|
||||||
|
col.alignment = 'RIGHT'
|
||||||
|
col.label(text=self.task_info.status.capitalize())
|
||||||
|
col.label(text=f'{cost_real} creds')
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Output Methods
|
||||||
|
####################
|
||||||
|
@events.on_value_changed(
|
||||||
|
socket_name='Cloud Task',
|
||||||
|
)
|
||||||
|
def compute_cloud_task(self) -> None:
|
||||||
|
self.sim_data = bl_cache.Signal.InvalidateCache
|
||||||
|
|
||||||
|
@events.computes_output_socket(
|
||||||
|
'Sim Data',
|
||||||
|
props={'sim_data'},
|
||||||
|
input_sockets={'Cloud Task'}, ## Keep to respect dependency chains.
|
||||||
|
)
|
||||||
|
def compute_sim_data(
|
||||||
|
self, props, input_sockets
|
||||||
|
) -> td.SimulationData | ct.FlowSignal:
|
||||||
|
if props['sim_data'] is None:
|
||||||
|
return ct.FlowSignal.FlowPending
|
||||||
|
|
||||||
|
return props['sim_data']
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Blender Registration
|
||||||
|
####################
|
||||||
|
BL_REGISTER = [
|
||||||
|
RunSimulation,
|
||||||
|
ReloadTrackedTask,
|
||||||
|
Tidy3DWebRunnerNode,
|
||||||
|
]
|
||||||
|
BL_NODES = {
|
||||||
|
ct.NodeType.Tidy3DWebRunner: (ct.NodeCategory.MAXWELLSIM_OUTPUTS_WEBEXPORTERS)
|
||||||
|
}
|
|
@ -262,9 +262,12 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
"""
|
"""
|
||||||
## TODO: Evaluate this properly
|
## TODO: Evaluate this properly
|
||||||
if self.initializing:
|
if self.initializing:
|
||||||
return
|
log.debug(
|
||||||
|
'%s: Rejected on_prop_changed("%s") while initializing',
|
||||||
if hasattr(self, prop_name):
|
self.bl_label,
|
||||||
|
prop_name,
|
||||||
|
)
|
||||||
|
elif hasattr(self, prop_name):
|
||||||
# Invalidate UI BLField Caches
|
# Invalidate UI BLField Caches
|
||||||
if prop_name in self.ui_blfields:
|
if prop_name in self.ui_blfields:
|
||||||
setattr(self, prop_name, bl_cache.Signal.InvalidateCache)
|
setattr(self, prop_name, bl_cache.Signal.InvalidateCache)
|
||||||
|
|
|
@ -32,7 +32,9 @@ class ReloadFolderList(bpy.types.Operator):
|
||||||
|
|
||||||
tdcloud.TidyCloudFolders.update_folders()
|
tdcloud.TidyCloudFolders.update_folders()
|
||||||
tdcloud.TidyCloudTasks.update_tasks(bl_socket.existing_folder_id)
|
tdcloud.TidyCloudTasks.update_tasks(bl_socket.existing_folder_id)
|
||||||
bl_socket.on_cloud_updated()
|
|
||||||
|
bl_socket.existing_folder_id = bl_cache.Signal.ResetEnumItems
|
||||||
|
bl_socket.existing_task_id = bl_cache.Signal.ResetEnumItems
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -57,7 +59,9 @@ class Authenticate(bpy.types.Operator):
|
||||||
if not tdcloud.check_authentication():
|
if not tdcloud.check_authentication():
|
||||||
tdcloud.authenticate_with_api_key(bl_socket.api_key)
|
tdcloud.authenticate_with_api_key(bl_socket.api_key)
|
||||||
bl_socket.api_key = ''
|
bl_socket.api_key = ''
|
||||||
bl_socket.on_cloud_updated()
|
|
||||||
|
bl_socket.existing_folder_id = bl_cache.Signal.ResetEnumItems
|
||||||
|
bl_socket.existing_task_id = bl_cache.Signal.ResetEnumItems
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -96,20 +100,6 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
|
|
||||||
new_task_name: str = bl_cache.BLField('', prop_ui=True)
|
new_task_name: str = bl_cache.BLField('', prop_ui=True)
|
||||||
|
|
||||||
####################
|
|
||||||
# - Property Changes
|
|
||||||
####################
|
|
||||||
def on_socket_prop_changed(self, prop_name: str) -> None:
|
|
||||||
if prop_name in [
|
|
||||||
'api_key',
|
|
||||||
'existing_folder_id',
|
|
||||||
'existing_task_id',
|
|
||||||
'new_task_name',
|
|
||||||
'should_exist',
|
|
||||||
]:
|
|
||||||
self.existing_folder_id = bl_cache.Signal.ResetEnumItems
|
|
||||||
self.existing_task_id = bl_cache.Signal.ResetEnumItems
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - FlowKinds
|
# - FlowKinds
|
||||||
####################
|
####################
|
||||||
|
@ -124,31 +114,28 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
@property
|
@property
|
||||||
def value(
|
def value(
|
||||||
self,
|
self,
|
||||||
) -> tuple[tdcloud.CloudTaskName, tdcloud.CloudFolder] | tdcloud.CloudTask | None:
|
) -> ct.NewSimCloudTask | tdcloud.CloudTask | ct.FlowSignal:
|
||||||
if tdcloud.IS_AUTHENTICATED:
|
if tdcloud.IS_AUTHENTICATED:
|
||||||
# Retrieve Folder
|
# Retrieve Folder
|
||||||
cloud_folder = tdcloud.TidyCloudFolders.folders().get(
|
cloud_folder = tdcloud.TidyCloudFolders.folders().get(
|
||||||
self.existing_folder_id
|
self.existing_folder_id
|
||||||
)
|
)
|
||||||
if cloud_folder is None:
|
if cloud_folder is None:
|
||||||
msg = f"Selected folder {cloud_folder} doesn't exist (it was probably deleted elsewhere)"
|
return ct.FlowSignal.NoFlow ## Folder deleted somewhere else
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
# Doesn't Exist: Return Construction Information
|
# Case: New Task
|
||||||
if not self.should_exist:
|
if not self.should_exist:
|
||||||
return (self.new_task_name, cloud_folder)
|
return ct.NewSimCloudTask(
|
||||||
|
task_name=self.new_task_name, cloud_folder=cloud_folder
|
||||||
|
)
|
||||||
|
|
||||||
# No Task Selected: Return None
|
# Case: Existing Task
|
||||||
if self.existing_task_id is None:
|
if self.existing_task_id is not None:
|
||||||
return None
|
|
||||||
|
|
||||||
# Retrieve Cloud Task
|
|
||||||
cloud_task = tdcloud.TidyCloudTasks.tasks(cloud_folder).get(
|
cloud_task = tdcloud.TidyCloudTasks.tasks(cloud_folder).get(
|
||||||
self.existing_task_id
|
self.existing_task_id
|
||||||
)
|
)
|
||||||
if cloud_task is None:
|
if cloud_folder is None:
|
||||||
msg = f"Selected task {cloud_task} doesn't exist (it was probably deleted elsewhere)"
|
return ct.FlowSignal.NoFlow ## Task deleted somewhere else
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
return cloud_task
|
return cloud_task
|
||||||
|
|
||||||
|
@ -227,17 +214,6 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
####################
|
|
||||||
# - Node-Initiated Updates
|
|
||||||
####################
|
|
||||||
def on_new_task_created(self, cloud_task: tdcloud.CloudTask) -> None:
|
|
||||||
self.existing_folder_id = cloud_task.folder_id
|
|
||||||
self.existing_task_id = cloud_task.task_id
|
|
||||||
self.should_exist = True
|
|
||||||
|
|
||||||
def on_prepare_new_task(self):
|
|
||||||
self.should_exist = False
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -8,6 +8,7 @@ import datetime as dt
|
||||||
import functools
|
import functools
|
||||||
import tempfile
|
import tempfile
|
||||||
import typing as typ
|
import typing as typ
|
||||||
|
import urllib
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
@ -46,11 +47,27 @@ def set_offline():
|
||||||
IS_ONLINE = False
|
IS_ONLINE = False
|
||||||
|
|
||||||
|
|
||||||
|
def check_online() -> bool:
|
||||||
|
global IS_ONLINE # noqa: PLW0603
|
||||||
|
try:
|
||||||
|
urllib.request.urlopen(
|
||||||
|
'https://docs.flexcompute.com/projects/tidy3d/en/latest/index.html',
|
||||||
|
timeout=2,
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
IS_ONLINE = False
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
IS_ONLINE = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Cloud Authentication
|
# - Cloud Authentication
|
||||||
####################
|
####################
|
||||||
def check_authentication() -> bool:
|
def check_authentication() -> bool:
|
||||||
global IS_AUTHENTICATED # noqa: PLW0603
|
global IS_AUTHENTICATED # noqa: PLW0603
|
||||||
|
log.critical('Checking Authentication')
|
||||||
|
|
||||||
# Check Previous Authentication
|
# Check Previous Authentication
|
||||||
## If we authenticated once, we presume that it'll work again.
|
## If we authenticated once, we presume that it'll work again.
|
||||||
|
@ -78,6 +95,14 @@ def authenticate_with_api_key(api_key: str) -> bool:
|
||||||
return check_authentication()
|
return check_authentication()
|
||||||
|
|
||||||
|
|
||||||
|
TD_CONFIG = Path(td_web.cli.constants.CONFIG_FILE)
|
||||||
|
|
||||||
|
## TODO: Robustness is key - internet might be down.
|
||||||
|
## -> I'm not a huge fan of the max 2sec startup time burden
|
||||||
|
if TD_CONFIG.is_file() and check_online():
|
||||||
|
check_authentication()
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Cloud Folder
|
# - Cloud Folder
|
||||||
####################
|
####################
|
||||||
|
@ -145,11 +170,12 @@ class TidyCloudFolders:
|
||||||
####################
|
####################
|
||||||
@dataclass
|
@dataclass
|
||||||
class CloudTaskInfo:
|
class CloudTaskInfo:
|
||||||
"""Toned-down, simplified `dataclass` variant of TaskInfo.
|
"""Toned-down `dataclass` variant of `tidy3d`'s TaskInfo.
|
||||||
|
|
||||||
See TaskInfo for more: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_info.py>)
|
See TaskInfo for more: <https://github.com/flexcompute/tidy3d/blob/453055e89dcff6d619597120b47817e996f1c198/tidy3d/web/core/task_info.py>)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
task_id: str
|
||||||
task_name: str
|
task_name: str
|
||||||
status: str
|
status: str
|
||||||
created_at: dt.datetime
|
created_at: dt.datetime
|
||||||
|
@ -168,6 +194,15 @@ class CloudTaskInfo:
|
||||||
version_solver: str | None = None ## solverVersion
|
version_solver: str | None = None ## solverVersion
|
||||||
callback_url: str | None = None ## callbackUrl
|
callback_url: str | None = None ## callbackUrl
|
||||||
|
|
||||||
|
def disk_cache_path(self, addon_cache: Path) -> Path:
|
||||||
|
"""Compute an appropriate location for caching simulations downloaded from the internet, unique to each task ID.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
task_id: The ID of the Tidy3D cloud task.
|
||||||
|
"""
|
||||||
|
(addon_cache / self.task_id).mkdir(exist_ok=True)
|
||||||
|
return addon_cache / self.task_id / 'sim_data.hdf5'
|
||||||
|
|
||||||
|
|
||||||
class TidyCloudTasks:
|
class TidyCloudTasks:
|
||||||
"""Greatly simplifies working with Tidy3D Tasks in the Cloud, specifically, via the lowish-level `tidy3d.web.core.task_core.SimulationTask` object.
|
"""Greatly simplifies working with Tidy3D Tasks in the Cloud, specifically, via the lowish-level `tidy3d.web.core.task_core.SimulationTask` object.
|
||||||
|
@ -232,6 +267,7 @@ class TidyCloudTasks:
|
||||||
## Task Info Cache
|
## Task Info Cache
|
||||||
for task_id, cloud_task in cloud_tasks.items():
|
for task_id, cloud_task in cloud_tasks.items():
|
||||||
cls.cache_task_info[task_id] = CloudTaskInfo(
|
cls.cache_task_info[task_id] = CloudTaskInfo(
|
||||||
|
task_id=task_id,
|
||||||
task_name=cloud_task.taskName,
|
task_name=cloud_task.taskName,
|
||||||
status=cloud_task.status,
|
status=cloud_task.status,
|
||||||
created_at=cloud_task.created_at,
|
created_at=cloud_task.created_at,
|
||||||
|
@ -346,6 +382,7 @@ class TidyCloudTasks:
|
||||||
|
|
||||||
## Task Info Cache
|
## Task Info Cache
|
||||||
cls.cache_task_info[cloud_task.task_id] = CloudTaskInfo(
|
cls.cache_task_info[cloud_task.task_id] = CloudTaskInfo(
|
||||||
|
task_id=cloud_task.task_id,
|
||||||
task_name=cloud_task.taskName,
|
task_name=cloud_task.taskName,
|
||||||
status=cloud_task.status,
|
status=cloud_task.status,
|
||||||
created_at=cloud_task.created_at,
|
created_at=cloud_task.created_at,
|
||||||
|
|
Loading…
Reference in New Issue