fix: Implement explicit no-flow w/FlowSignal
parent
a3defd3c1c
commit
c82862dde9
34
TODO.md
34
TODO.md
|
@ -541,19 +541,19 @@ We need support for arbitrary objects, but still backed by the persistance seman
|
|||
- [ ] Implement Enum property, (also see <https://developer.blender.org/docs/release_notes/4.1/python_api/#enum-id-properties>)
|
||||
- Use this to bridge the enum UI to actual StrEnum objects.
|
||||
- This also maybe enables some very interesting use cases when it comes to ex. static verifiability of data provided to event callbacks.
|
||||
- [ ] Ensure certain options, namely `name` (as `ui_name`), `default`, `subtype`, (numeric) `min`, `max`, `step`, `precision`, (string) `maxlen`, `search`, and `search_options`, can be passed down via the `BLField()` constructor.
|
||||
- [x] Ensure certain options, namely `name` (as `ui_name`), `default`, `subtype`, (numeric) `min`, `max`, `step`, `precision`, (string) `maxlen`, `search`, and `search_options`, can be passed down via the `BLField()` constructor.
|
||||
- [ ] Make a class method that parses the docstring.
|
||||
- [ ] `description`: Use the docstring parser to extract the first description sentence of the attribute name from the subclass docstring, so we are both encouraged to document our nodes/sockets, and so we're not documenting twice.
|
||||
|
||||
### Niceness
|
||||
- [x] Rename the internal property to 'blfield__'.
|
||||
- [ ] Add a method that extracts the internal property name, for places where we need the Blender property name.
|
||||
- [x] Add a method that extracts the internal property name, for places where we need the Blender property name.
|
||||
- **Key use case**: `draw.prop(self, self.field_name._bl_prop_name)`, which is also nice b/c no implicit string-based reference.
|
||||
- The work done above with types makes this as fast and useful as internal props. Just make sure we validate that the type can be usefully accessed like this.
|
||||
- [ ] Add a field method (called w/instance) that updates min/max/etc. on the 'blfield__' prop, in a native property type compatible manner: https://developer.blender.org/docs/release_notes/3.0/python_api/#idproperty-ui-data-api
|
||||
- [x] Add a field method (called w/instance) that updates min/max/etc. on the 'blfield__' prop, in a native property type compatible manner: https://developer.blender.org/docs/release_notes/3.0/python_api/#idproperty-ui-data-api
|
||||
- Should also throw appropriate errors for invalid access from Python, while Blender handles access from the inside.
|
||||
- This allows us
|
||||
- [ ] Similarly, a field method that gets the 'blfield__' prop data as a dictionary.
|
||||
- [x] Similarly, a field method that gets the 'blfield__' prop data as a dictionary.
|
||||
|
||||
### Parallel Features
|
||||
- [x] Move serialization work to a `utils`.
|
||||
|
@ -569,3 +569,29 @@ We need support for arbitrary objects, but still backed by the persistance seman
|
|||
- Benefit: Any serializable object can be "simply used", at almost native speed (due to the aggressive read-cache).
|
||||
- Benefit: Better error properties for updating, access, setting, etc. .
|
||||
- Benefit: Validate usage in a vastly greater amount of contexts.
|
||||
|
||||
|
||||
|
||||
# Overnight Ideas
|
||||
- [ ] Fix file-load hiccups by persisting `_enum_cb_cache` and `_str_cb_cache`.
|
||||
|
||||
- [x] Implement `FlowSignal`s as special return values for `@computes_output_socket`, instead of juggling `None`.
|
||||
- `FlowSignal.FlowPending`: Data was asked for, and it's not yet available, but it's expected to become available.
|
||||
- Semantically: "Just hold on for a hot second".
|
||||
- Return: If in any socket data provided to cb, return the same signal insted of running the callback.
|
||||
- Caches: Don't invalidate caches, since the user will expect their data to still persist.
|
||||
- Net Effect: Absolutely nothing happens. Perhaps we can recolor the nodes, though.
|
||||
|
||||
- [ ] `FlowSignal.FlowLost`: Output socket requires data that simply isn't available.
|
||||
- Generally, nodes don't return it
|
||||
- Return: If in any socket data provided to cb, return the same signal insted of running the callback.
|
||||
- Caches: Do invalidate caches, since the user will expect their data to still persist.
|
||||
- Net Effect: Sometimes, stuff happens in the output method [BB
|
||||
- Net Effect: `DataChanged` is an event that signifies Node data will reset along the flow.
|
||||
|
||||
- [ ] Packing Imported Data in `Tidy3D Web Importer`, `Tidy3D File Importer`.
|
||||
- Just `.to_hdf5_gz()` it into a `BytesIO`, Base85
|
||||
|
||||
- [ ] Remove Matplotlib Bottlenecks (~70ms -> ~5ms)
|
||||
- Reuse `fig` per-`ManagedBLImage` (~25ms)
|
||||
- Use `Agg` backend, plot with `fig.canvas.draw()`, and load image buffer directly as np.frombuffer(ax.figure.canvas.tostring_rgb(), dtype=np.uint8) (~40ms).
|
||||
|
|
|
@ -15,3 +15,10 @@ class OperatorType(enum.StrEnum):
|
|||
ManagePyDeps = enum.auto()
|
||||
|
||||
ConnectViewerNode = enum.auto()
|
||||
|
||||
# Socket: Tidy3DCloudTask
|
||||
SocketCloudAuthenticate = enum.auto()
|
||||
SocketReloadCloudFolderList = enum.auto()
|
||||
|
||||
# Node: Tidy3DWebImporter
|
||||
NodeLoadCloudSim = enum.auto()
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
from blender_maxwell.contracts import (
|
||||
BLClass,
|
||||
BLColorRGBA,
|
||||
BLEnumElement,
|
||||
BLEnumID,
|
||||
BLIcon,
|
||||
BLIconSet,
|
||||
BLIDStruct,
|
||||
BLKeymapItem,
|
||||
BLModifierType,
|
||||
BLNodeTreeInterfaceID,
|
||||
BLOperatorStatus,
|
||||
BLPropFlag,
|
||||
BLRegionType,
|
||||
BLSpaceType,
|
||||
KeymapItemDef,
|
||||
ManagedObjName,
|
||||
OperatorType,
|
||||
PanelType,
|
||||
PresetName,
|
||||
SocketName,
|
||||
addon,
|
||||
BLClass,
|
||||
BLColorRGBA,
|
||||
BLEnumElement,
|
||||
BLEnumID,
|
||||
BLIcon,
|
||||
BLIconSet,
|
||||
BLIDStruct,
|
||||
BLKeymapItem,
|
||||
BLModifierType,
|
||||
BLNodeTreeInterfaceID,
|
||||
BLOperatorStatus,
|
||||
BLPropFlag,
|
||||
BLRegionType,
|
||||
BLSpaceType,
|
||||
KeymapItemDef,
|
||||
ManagedObjName,
|
||||
OperatorType,
|
||||
PanelType,
|
||||
PresetName,
|
||||
SocketName,
|
||||
addon,
|
||||
)
|
||||
|
||||
from .bl_socket_desc_map import BL_SOCKET_DESCR_ANNOT_STRING, BL_SOCKET_DESCR_TYPE_MAP
|
||||
|
@ -28,15 +28,16 @@ from .category_labels import NODE_CAT_LABELS
|
|||
from .category_types import NodeCategory
|
||||
from .flow_events import FlowEvent
|
||||
from .flow_kinds import (
|
||||
ArrayFlow,
|
||||
CapabilitiesFlow,
|
||||
FlowKind,
|
||||
InfoFlow,
|
||||
LazyArrayRangeFlow,
|
||||
LazyValueFuncFlow,
|
||||
ParamsFlow,
|
||||
ValueFlow,
|
||||
ArrayFlow,
|
||||
CapabilitiesFlow,
|
||||
FlowKind,
|
||||
InfoFlow,
|
||||
LazyArrayRangeFlow,
|
||||
LazyValueFuncFlow,
|
||||
ParamsFlow,
|
||||
ValueFlow,
|
||||
)
|
||||
from .flow_signals import FlowSignal
|
||||
from .icons import Icon
|
||||
from .mobj_types import ManagedObjType
|
||||
from .node_types import NodeType
|
||||
|
@ -93,4 +94,5 @@ __all__ = [
|
|||
'LazyValueFuncFlow',
|
||||
'ParamsFlow',
|
||||
'ValueFlow',
|
||||
'FlowSignal',
|
||||
]
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import enum
|
||||
import typing as typ
|
||||
|
||||
|
||||
_FLOW_SIGNAL_SET: set | None = None
|
||||
|
||||
|
||||
class FlowSignal(enum.StrEnum):
|
||||
"""Special output socket return value, which indicates a piece of information about the state of the flow, instead of data.
|
||||
|
||||
Attributes:
|
||||
FlowPending: The data that was requested is not available, but it is expected to become available soon.
|
||||
- **Behavior**: When encountered downstream in `events` decorators for all `FlowKind`s, either `FlowSignal.FlowPending` should return instead of the method, or the decorated method should simply not run.
|
||||
- **Caching**: Don't invalidate caches, since the user will expect their data to persist.
|
||||
- **Net Effect**: All nodes that encounter FlowPending are forward-locked, possibly with an explanatory indicator. In terms of data, nothing happens - including no changes to the user's data.
|
||||
|
||||
"""
|
||||
|
||||
FlowPending = enum.auto()
|
||||
NoFlow = enum.auto()
|
||||
|
||||
@classmethod
|
||||
def all(cls) -> set[typ.Self]:
|
||||
"""Query all flow signals, using a simple cache to ensure minimal overhead when used in ex. `draw()` functions.
|
||||
|
||||
Returns:
|
||||
Set of FlowSignal enum items, for easy `O(1)` lookup
|
||||
"""
|
||||
global _FLOW_SIGNAL_SET # noqa: PLW0603
|
||||
|
||||
if _FLOW_SIGNAL_SET is None:
|
||||
_FLOW_SIGNAL_SET = set(FlowSignal)
|
||||
|
||||
return _FLOW_SIGNAL_SET
|
||||
|
||||
@classmethod
|
||||
def check(cls, obj: typ.Any) -> set[typ.Self]:
|
||||
"""Checks whether an arbitrary object is a `FlowSignal` with tiny overhead.
|
||||
|
||||
Notes:
|
||||
Optimized by first performing an `isinstance` check against both `FlowSignal` and `str`.
|
||||
Then, we can check membership in `cls.all()` with `O(1)`, since (by type narrowing like this) we've ensured that the object is hashable.
|
||||
|
||||
Returns:
|
||||
Whether `obj` is a `FlowSignal`.
|
||||
|
||||
Examples:
|
||||
A common pattern to ensure an object is **not** a `FlowSignal` is `not FlowSignal.check(obj)`.
|
||||
"""
|
||||
return isinstance(obj, FlowSignal | str) and obj in FlowSignal.all()
|
||||
|
||||
@classmethod
|
||||
def check_single(cls, obj: typ.Any, single: typ.Self) -> set[typ.Self]:
|
||||
"""Checks whether an arbitrary object is a particular `FlowSignal`, with tiny overhead.
|
||||
|
||||
Use this whenever it is important to make different decisions based on different `FlowSignal`s.
|
||||
|
||||
Notes:
|
||||
Generally, you should use `cls.check()`.
|
||||
It tends to only be important to know whether you're getting a proper object from the flow, or whether it's dumping a `FlowSignal` on you instead.
|
||||
|
||||
However, certain nodes might have a good reason to react differently .
|
||||
One example is deciding whether to keep node-internal caches around in the absence of data: `FlowSignal.FlowPending` hints to keep it around (allowing the user to ex. change selections, etc.), while `FlowSignal.NoFlow` hints to get rid of it immediately (resetting the node entirely).
|
||||
|
||||
Returns:
|
||||
Whether `obj` is a `FlowSignal`.
|
||||
|
||||
Examples:
|
||||
A common pattern to ensure an object is **not** a `FlowSignal` is `not FlowSignal.check(obj)`.
|
||||
"""
|
||||
return isinstance(obj, FlowSignal | str) and obj == single
|
|
@ -5,6 +5,7 @@ import bpy
|
|||
import jax
|
||||
import jax.numpy as jnp
|
||||
import sympy.physics.units as spu
|
||||
import tidy3d as td
|
||||
|
||||
from blender_maxwell.utils import bl_cache, logger
|
||||
from blender_maxwell.utils import extra_sympy_units as spux
|
||||
|
@ -15,9 +16,18 @@ from .. import base, events
|
|||
|
||||
log = logger.get(__name__)
|
||||
|
||||
TDMonitorData: typ.TypeAlias = td.components.data.monitor_data.MonitorData
|
||||
|
||||
|
||||
class ExtractDataNode(base.MaxwellSimNode):
|
||||
"""Node for extracting data from particular objects.
|
||||
"""Extract data from sockets for further analysis.
|
||||
|
||||
# Socket Sets
|
||||
## Sim Data
|
||||
Extracts monitors from a `MaxwelFDTDSimDataSocket`.
|
||||
|
||||
## Monitor Data
|
||||
Extracts array attributes from a `MaxwelFDTDSimDataSocket`.
|
||||
|
||||
Attributes:
|
||||
extract_filter: Identifier for data to extract from the input.
|
||||
|
@ -45,31 +55,145 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
enum_cb=lambda self, _: self.search_extract_filters(),
|
||||
)
|
||||
|
||||
# Sim Data
|
||||
sim_data_monitor_nametype: dict[str, str] = bl_cache.BLField(
|
||||
{}, use_prop_update=False
|
||||
)
|
||||
|
||||
# Monitor Data
|
||||
monitor_data_type: str = bl_cache.BLField('', use_prop_update=False)
|
||||
monitor_data_components: list[str] = bl_cache.BLField([], use_prop_update=False)
|
||||
|
||||
####################
|
||||
# - Computed Properties
|
||||
# - Computed: Sim Data
|
||||
####################
|
||||
@property
|
||||
def has_sim_data(self) -> bool:
|
||||
return self.active_socket_set == 'Sim Data' and self.sim_data_monitor_nametype
|
||||
def sim_data(self) -> td.SimulationData | None:
|
||||
"""Computes the (cached) simulation data from the input socket.
|
||||
|
||||
Return:
|
||||
Either the simulation data, if available, or None.
|
||||
"""
|
||||
sim_data = self._compute_input(
|
||||
'Sim Data', kind=ct.FlowKind.Value, optional=True
|
||||
)
|
||||
if not ct.FlowSignal.check(sim_data):
|
||||
log.critical('Sim Data was Not FlowSignal')
|
||||
return sim_data
|
||||
|
||||
log.critical('Sim Data was FlowSignal: %s', str(sim_data))
|
||||
return None
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
def sim_data_monitor_nametype(self) -> dict[str, str] | None:
|
||||
"""For simulation data, computes and and caches a map from name to "type".
|
||||
|
||||
Return:
|
||||
The name to type of monitors in the simulation data.
|
||||
"""
|
||||
if self.sim_data is not None:
|
||||
return {
|
||||
monitor_name: monitor_data.type
|
||||
for monitor_name, monitor_data in self.sim_data.monitor_data.items()
|
||||
}
|
||||
|
||||
return None
|
||||
|
||||
####################
|
||||
# - Computed Properties: Monitor Data
|
||||
####################
|
||||
@property
|
||||
def has_monitor_data(self) -> bool:
|
||||
return self.active_socket_set == 'Monitor Data' and self.monitor_data_type
|
||||
def monitor_data(self) -> TDMonitorData | None:
|
||||
"""Computes the (cached) monitor data from the input socket.
|
||||
|
||||
Return:
|
||||
Either the monitor data, if available, or None.
|
||||
"""
|
||||
monitor_data = self._compute_input(
|
||||
'Monitor Data', kind=ct.FlowKind.Value, optional=True
|
||||
)
|
||||
if not ct.FlowSignal.check(monitor_data):
|
||||
return monitor_data
|
||||
|
||||
return None
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
def monitor_data_type(self) -> str | None:
|
||||
"""For monitor data, computes and caches the monitor "type".
|
||||
|
||||
Notes:
|
||||
Should be invalidated with (before) `self.monitor_data_components`.
|
||||
|
||||
Return:
|
||||
The "type" of the monitor, if available, else None.
|
||||
"""
|
||||
if self.monitor_data is not None:
|
||||
return self.monitor_data.type.removesuffix('Data')
|
||||
|
||||
return None
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
def monitor_data_components(self) -> list[str] | None:
|
||||
r"""For monitor data, computes and caches the component sof the monitor.
|
||||
|
||||
The output depends entirely on the output of `self.monitor_data`.
|
||||
|
||||
- **Field(Time)**: Whichever `[E|H][x|y|z]` are not `None` on the monitor.
|
||||
- **Permittivity**: Specifically `['xx', 'yy', 'zz']`.
|
||||
- **Flux(Time)**: Only `['flux']`.
|
||||
- **FieldProjection(...)**: All of $r$, $\theta$, $\phi$ for both `E` and `H`.
|
||||
- **Diffraction**: Same as `FieldProjection`.
|
||||
|
||||
Notes:
|
||||
Should be invalidated after with `self.monitor_data_type`.
|
||||
|
||||
Return:
|
||||
The "type" of the monitor, if available, else None.
|
||||
"""
|
||||
if self.monitor_data is not None:
|
||||
# Field/FieldTime
|
||||
if self.monitor_data_type in ['Field', 'FieldTime']:
|
||||
return [
|
||||
field_component
|
||||
for field_component in ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']
|
||||
if hasattr(self.monitor_data, field_component)
|
||||
]
|
||||
|
||||
# Permittivity
|
||||
if self.monitor_data_type == 'Permittivity':
|
||||
return ['xx', 'yy', 'zz']
|
||||
|
||||
# Flux/FluxTime
|
||||
if self.monitor_data_type in ['Flux', 'FluxTime']:
|
||||
return ['flux']
|
||||
|
||||
# FieldProjection(Angle/Cartesian/KSpace)/Diffraction
|
||||
if self.monitor_data_type in [
|
||||
'FieldProjectionAngle',
|
||||
'FieldProjectionCartesian',
|
||||
'FieldProjectionKSpace',
|
||||
'Diffraction',
|
||||
]:
|
||||
return [
|
||||
'Er',
|
||||
'Etheta',
|
||||
'Ephi',
|
||||
'Hr',
|
||||
'Htheta',
|
||||
'Hphi',
|
||||
]
|
||||
|
||||
return None
|
||||
|
||||
####################
|
||||
# - Extraction Filter Search
|
||||
####################
|
||||
def search_extract_filters(self) -> list[ct.BLEnumElement]:
|
||||
if self.has_sim_data:
|
||||
"""Compute valid values for `self.extract_filter`, for a dynamic `EnumProperty`.
|
||||
|
||||
Notes:
|
||||
Should be reset (via `self.extract_filter`) with (after) `self.sim_data_monitor_nametype`, `self.monitor_data_components`, and (implicitly) `self.monitor_type`.
|
||||
|
||||
See `bl_cache.BLField` for more on dynamic `EnumProperty`.
|
||||
|
||||
Returns:
|
||||
Valid `self.extract_filter` in a format compatible with dynamic `EnumProperty`.
|
||||
"""
|
||||
log.critical('Searching Extract Filters')
|
||||
log.critical(self.sim_data_monitor_nametype)
|
||||
log.critical(self.monitor_data_components)
|
||||
if self.sim_data_monitor_nametype is not None:
|
||||
return [
|
||||
(monitor_name, monitor_name, monitor_type.removesuffix('Data'), '', i)
|
||||
for i, (monitor_name, monitor_type) in enumerate(
|
||||
|
@ -77,9 +201,15 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
)
|
||||
]
|
||||
|
||||
if self.has_monitor_data:
|
||||
if self.monitor_data_components is not None:
|
||||
return [
|
||||
(component_name, component_name, f'ℂ {component_name[1]}-Pol', '', i)
|
||||
(
|
||||
component_name,
|
||||
component_name,
|
||||
f'ℂ {component_name[1]}-polarization of the {"electric" if component_name[0] == "E" else "magnetic"} field',
|
||||
'',
|
||||
i,
|
||||
)
|
||||
for i, component_name in enumerate(self.monitor_data_components)
|
||||
]
|
||||
|
||||
|
@ -89,156 +219,172 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
# - UI
|
||||
####################
|
||||
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
||||
"""Draw node properties in the node.
|
||||
|
||||
Parameters:
|
||||
col: UI target for drawing.
|
||||
"""
|
||||
col.prop(self, self.blfields['extract_filter'], text='')
|
||||
|
||||
def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
||||
if self.has_sim_data or self.has_monitor_data:
|
||||
"""Draw dynamic information in the node, for user consideration.
|
||||
|
||||
Parameters:
|
||||
col: UI target for drawing.
|
||||
"""
|
||||
has_sim_data = self.sim_data_monitor_nametype is not None
|
||||
has_monitor_data = self.monitor_data_components is not None
|
||||
|
||||
if has_sim_data or has_monitor_data:
|
||||
# Header
|
||||
row = col.row()
|
||||
row.alignment = 'CENTER'
|
||||
if self.has_sim_data:
|
||||
if has_sim_data:
|
||||
row.label(text=f'{len(self.sim_data_monitor_nametype)} Monitors')
|
||||
elif self.has_monitor_data:
|
||||
elif has_monitor_data:
|
||||
row.label(text=f'{self.monitor_data_type} Monitor Data')
|
||||
|
||||
# Monitor Data Contents
|
||||
## TODO: More compact double-split
|
||||
## TODO: Output shape data.
|
||||
## TODO: Local ENUM_MANY tabs for visible column selection?
|
||||
row = col.row()
|
||||
box = row.box()
|
||||
grid = box.grid_flow(row_major=True, columns=2, even_columns=True)
|
||||
for name, desc in [
|
||||
(name, desc) for idname, name, desc, *_ in self.search_extract_filters()
|
||||
]:
|
||||
grid.label(text=name)
|
||||
grid.label(text=desc if desc else '')
|
||||
for monitor_name, monitor_type in self.sim_data_monitor_nametype.items():
|
||||
grid.label(text=monitor_name)
|
||||
grid.label(text=monitor_type)
|
||||
|
||||
####################
|
||||
# - Events
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
# Trigger
|
||||
socket_name={'Sim Data', 'Monitor Data'},
|
||||
prop_name='active_socket_set',
|
||||
input_sockets={'Sim Data', 'Monitor Data'},
|
||||
input_sockets_optional={'Sim Data': True, 'Monitor Data': True},
|
||||
run_on_init=True,
|
||||
)
|
||||
def on_sim_data_changed(self, input_sockets: dict):
|
||||
if input_sockets['Sim Data'] is not None:
|
||||
# Sim Data Monitors: Set Name -> Type
|
||||
self.sim_data_monitor_nametype = {
|
||||
monitor_name: monitor_data.type
|
||||
for monitor_name, monitor_data in input_sockets[
|
||||
'Sim Data'
|
||||
].monitor_data.items()
|
||||
}
|
||||
elif self.sim_data_monitor_nametype:
|
||||
self.sim_data_monitor_nametype = {}
|
||||
|
||||
if input_sockets['Monitor Data'] is not None:
|
||||
# Monitor Data Type
|
||||
self.monitor_data_type = input_sockets['Monitor Data'].type.removesuffix(
|
||||
'Data'
|
||||
)
|
||||
|
||||
# Field/FieldTime
|
||||
if self.monitor_data_type in ['Field', 'FieldTime']:
|
||||
self.monitor_data_components = [
|
||||
field_component
|
||||
for field_component in ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']
|
||||
if hasattr(input_sockets['Monitor Data'], field_component)
|
||||
]
|
||||
|
||||
# Permittivity
|
||||
if self.monitor_data_type == 'Permittivity':
|
||||
self.monitor_data_components = ['xx', 'yy', 'zz']
|
||||
|
||||
# Flux/FluxTime
|
||||
if self.monitor_data_type in ['Flux', 'FluxTime']:
|
||||
self.monitor_data_components = ['flux']
|
||||
|
||||
# FieldProjection(Angle/Cartesian/KSpace)/Diffraction
|
||||
if self.monitor_data_type in [
|
||||
'FieldProjectionAngle',
|
||||
'FieldProjectionCartesian',
|
||||
'FieldProjectionKSpace',
|
||||
'Diffraction',
|
||||
]:
|
||||
self.monitor_data_components = [
|
||||
'Er',
|
||||
'Etheta',
|
||||
'Ephi',
|
||||
'Hr',
|
||||
'Htheta',
|
||||
'Hphi',
|
||||
]
|
||||
else:
|
||||
if self.monitor_data_type:
|
||||
self.monitor_data_type = ''
|
||||
if self.monitor_data_components:
|
||||
self.monitor_data_components = []
|
||||
|
||||
# Invalidate Computed Property Caches
|
||||
def on_input_sockets_changed(self) -> None:
|
||||
"""Invalidate the cached properties for sim data / monitor data, and reset the extraction filter."""
|
||||
self.sim_data_monitor_nametype = bl_cache.Signal.InvalidateCache
|
||||
self.monitor_data_type = bl_cache.Signal.InvalidateCache
|
||||
self.monitor_data_components = bl_cache.Signal.InvalidateCache
|
||||
self.extract_filter = bl_cache.Signal.ResetEnumItems
|
||||
|
||||
####################
|
||||
# - Output: Sim Data -> Monitor Data
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Monitor Data',
|
||||
kind=ct.FlowKind.Value,
|
||||
# Loaded
|
||||
props={'extract_filter'},
|
||||
input_sockets={'Sim Data'},
|
||||
)
|
||||
def compute_monitor_data(self, props: dict, input_sockets: dict):
|
||||
if input_sockets['Sim Data'] is not None and props['extract_filter'] != 'NONE':
|
||||
def compute_monitor_data(
|
||||
self, props: dict, input_sockets: dict
|
||||
) -> TDMonitorData | ct.FlowSignal:
|
||||
"""Compute `Monitor Data` by querying an attribute of `Sim Data`.
|
||||
|
||||
Notes:
|
||||
The attribute to query is read directly from `self.extract_filter`.
|
||||
This is also the mechanism that protects from trying to reference an invalid attribute.
|
||||
|
||||
Returns:
|
||||
Monitor data, if available, else `ct.FlowSignal.FlowPending`.
|
||||
"""
|
||||
sim_data = input_sockets['Sim Data']
|
||||
has_sim_data = not ct.FlowSignal.check(sim_data)
|
||||
|
||||
if has_sim_data and props['extract_filter'] != 'NONE':
|
||||
return input_sockets['Sim Data'].monitor_data[props['extract_filter']]
|
||||
|
||||
return None
|
||||
# Propagate NoFlow
|
||||
if ct.FlowSignal.check_single(sim_data, ct.FlowSignal.NoFlow):
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
####################
|
||||
# - Output: Monitor Data -> Data
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Data',
|
||||
kind=ct.FlowKind.Array,
|
||||
# Loaded
|
||||
props={'extract_filter'},
|
||||
input_sockets={'Monitor Data'},
|
||||
input_socket_kinds={'Monitor Data': ct.FlowKind.Value},
|
||||
)
|
||||
def compute_data(self, props: dict, input_sockets: dict) -> jax.Array | None:
|
||||
if (
|
||||
input_sockets['Monitor Data'] is not None
|
||||
and props['extract_filter'] != 'NONE'
|
||||
):
|
||||
def compute_data(
|
||||
self, props: dict, input_sockets: dict
|
||||
) -> jax.Array | ct.FlowSignal:
|
||||
"""Compute `Data:Array` by querying an array-like attribute of `Monitor Data`, then constructing an `ct.ArrayFlow`.
|
||||
|
||||
Uses the internal `xarray` data returned by Tidy3D.
|
||||
|
||||
Notes:
|
||||
The attribute to query is read directly from `self.extract_filter`.
|
||||
This is also the mechanism that protects from trying to reference an invalid attribute.
|
||||
|
||||
Used as the first part of the `LazyFuncValue` chain used for further array manipulations with Math nodes.
|
||||
|
||||
Returns:
|
||||
The data array, if available, else `ct.FlowSignal.FlowPending`.
|
||||
"""
|
||||
has_monitor_data = not ct.FlowSignal.check(input_sockets['Monitor Data'])
|
||||
|
||||
if has_monitor_data and props['extract_filter'] != 'NONE':
|
||||
xarray_data = getattr(
|
||||
input_sockets['Monitor Data'], props['extract_filter']
|
||||
)
|
||||
return jnp.array(xarray_data.data)
|
||||
## TODO: Let the array itself have its output unit too!
|
||||
return ct.ArrayFlow(values=jnp.array(xarray_data.data), unit=None)
|
||||
## TODO: Try np.array instead, as it removes a copy, while still (I believe) being JIT-compatible.
|
||||
|
||||
return None
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Data',
|
||||
kind=ct.FlowKind.LazyValueFunc,
|
||||
# Loaded
|
||||
output_sockets={'Data'},
|
||||
output_socket_kinds={'Data': ct.FlowKind.Array},
|
||||
)
|
||||
def compute_extracted_data_lazy(
|
||||
self, output_sockets: dict
|
||||
) -> ct.LazyValueFuncFlow | None:
|
||||
if output_sockets['Data'] is not None:
|
||||
"""Declare `Data:LazyValueFunc` by creating a simple function that directly wraps `Data:Array`.
|
||||
|
||||
Returns:
|
||||
The composable function array, if available, else `ct.FlowSignal.FlowPending`.
|
||||
"""
|
||||
has_output_data = not ct.FlowSignal.check(output_sockets['Data'])
|
||||
|
||||
if has_output_data:
|
||||
return ct.LazyValueFuncFlow(
|
||||
func=lambda: output_sockets['Data'], supports_jax=True
|
||||
func=lambda: output_sockets['Data'].values, supports_jax=True
|
||||
)
|
||||
|
||||
return None
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
####################
|
||||
# - Auxiliary: Monitor Data -> Data
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
'Data',
|
||||
kind=ct.FlowKind.Params,
|
||||
)
|
||||
def compute_data_params(self) -> ct.ParamsFlow:
|
||||
return ct.ParamsFlow()
|
||||
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Data',
|
||||
kind=ct.FlowKind.Info,
|
||||
# Loaded
|
||||
props={'monitor_data_type', 'extract_filter'},
|
||||
input_sockets={'Monitor Data'},
|
||||
input_socket_kinds={'Monitor Data': ct.FlowKind.Value},
|
||||
|
@ -247,14 +393,18 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
def compute_extracted_data_info(
|
||||
self, props: dict, input_sockets: dict
|
||||
) -> ct.InfoFlow:
|
||||
"""Declare `Data:Info` by manually selecting appropriate axes, units, etc. for each monitor type.
|
||||
|
||||
Returns:
|
||||
Information describing the `Data:LazyValueFunc`, if available, else `ct.FlowSignal.FlowPending`.
|
||||
"""
|
||||
has_monitor_data = not ct.FlowSignal.check(input_sockets['Monitor Data'])
|
||||
|
||||
# Retrieve XArray
|
||||
if (
|
||||
input_sockets['Monitor Data'] is not None
|
||||
and props['extract_filter'] != 'NONE'
|
||||
):
|
||||
if has_monitor_data and props['extract_filter'] != 'NONE':
|
||||
xarr = getattr(input_sockets['Monitor Data'], props['extract_filter'])
|
||||
else:
|
||||
return ct.InfoFlow()
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
info_output_names = {
|
||||
'output_names': [props['extract_filter']],
|
||||
|
|
|
@ -48,8 +48,12 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
)
|
||||
|
||||
@property
|
||||
def _info(self) -> ct.InfoFlow:
|
||||
return self._compute_input('Data', kind=ct.FlowKind.Info)
|
||||
def data_info(self) -> ct.InfoFlow | None:
|
||||
info = self._compute_input('Data', kind=ct.FlowKind.Info)
|
||||
if not ct.FlowSignal.check(info):
|
||||
return info
|
||||
|
||||
return None
|
||||
|
||||
####################
|
||||
# - Operation Search
|
||||
|
@ -71,16 +75,18 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
# - Dim Search
|
||||
####################
|
||||
def search_dims(self) -> list[ct.BLEnumElement]:
|
||||
if (info := self._info).dim_names:
|
||||
if self.data_info is not None:
|
||||
dims = [
|
||||
(dim_name, dim_name, dim_name, '', i)
|
||||
for i, dim_name in enumerate(info.dim_names)
|
||||
for i, dim_name in enumerate(self.data_info.dim_names)
|
||||
]
|
||||
|
||||
# Squeeze: Dimension Must Have Length=1
|
||||
## We must also correct the "NUMBER" of the enum.
|
||||
if self.operation == 'SQUEEZE':
|
||||
filtered_dims = [dim for dim in dims if info.dim_lens[dim[0]] == 1]
|
||||
filtered_dims = [
|
||||
dim for dim in dims if self.data_info.dim_lens[dim[0]] == 1
|
||||
]
|
||||
return [(*dim[:-1], i) for i, dim in enumerate(filtered_dims)]
|
||||
|
||||
return dims
|
||||
|
@ -91,42 +97,43 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
####################
|
||||
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||
layout.prop(self, self.blfields['operation'], text='')
|
||||
if self._info.dim_names:
|
||||
if self.data_info is not None and self.data_info.dim_names:
|
||||
layout.prop(self, self.blfields['dim'], text='')
|
||||
|
||||
####################
|
||||
# - Events
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
prop_name='active_socket_set',
|
||||
run_on_init=True,
|
||||
)
|
||||
def on_socket_set_changed(self):
|
||||
self.operation = bl_cache.Signal.ResetEnumItems
|
||||
|
||||
@events.on_value_changed(
|
||||
socket_name='Data',
|
||||
prop_name='active_socket_set',
|
||||
props={'active_socket_set'},
|
||||
run_on_init=True,
|
||||
input_sockets={'Data'},
|
||||
input_socket_kinds={'Data': ct.FlowKind.Info},
|
||||
# run_on_init=True,
|
||||
)
|
||||
def on_any_change(self, props: dict, input_sockets: dict):
|
||||
self.dim = bl_cache.Signal.ResetEnumItems
|
||||
def on_any_change(self, input_sockets: dict):
|
||||
if all(
|
||||
not ct.FlowSignal.check_single(
|
||||
input_socket_value, ct.FlowSignal.FlowPending
|
||||
)
|
||||
for input_socket_value in input_sockets.values()
|
||||
):
|
||||
self.operation = bl_cache.Signal.ResetEnumItems
|
||||
self.dim = bl_cache.Signal.ResetEnumItems
|
||||
|
||||
@events.on_value_changed(
|
||||
socket_name='Data',
|
||||
prop_name='dim',
|
||||
## run_on_init: Implicitly triggered.
|
||||
props={'active_socket_set', 'dim'},
|
||||
input_sockets={'Data'},
|
||||
input_socket_kinds={'Data': ct.FlowKind.Info},
|
||||
# run_on_init=True,
|
||||
)
|
||||
def on_dim_change(self, props: dict, input_sockets: dict):
|
||||
if input_sockets['Data'] == ct.FlowSignal.FlowPending:
|
||||
return
|
||||
|
||||
# Add/Remove Input Socket "Value"
|
||||
if (
|
||||
input_sockets['Data'] != ct.InfoFlow()
|
||||
not ct.Flowsignal.check(input_sockets['Data'])
|
||||
and props['active_socket_set'] == 'By Dim Value'
|
||||
and props['dim'] != 'NONE'
|
||||
):
|
||||
|
@ -144,7 +151,7 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
):
|
||||
self.loose_input_sockets = {
|
||||
'Value': wanted_socket_def(),
|
||||
}
|
||||
} ## TODO: Can we do the boilerplate in base.py?
|
||||
elif self.loose_input_sockets:
|
||||
self.loose_input_sockets = {}
|
||||
|
||||
|
@ -163,13 +170,16 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
lazy_value_func = input_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
||||
info = input_sockets['Data'][ct.FlowKind.Info]
|
||||
|
||||
# Check Flow
|
||||
if (
|
||||
any(ct.FlowSignal.check(inp) for inp in [info, lazy_value_func])
|
||||
or props['operation'] == 'NONE'
|
||||
):
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
# Compute Bound/Free Parameters
|
||||
func_args = [int] if props['active_socket_set'] == 'By Dim Value' else []
|
||||
if props['dim'] != 'NONE':
|
||||
axis = info.dim_names.index(props['dim'])
|
||||
else:
|
||||
msg = 'Dimension cannot be empty'
|
||||
raise ValueError(msg)
|
||||
axis = info.dim_names.index(props['dim'])
|
||||
|
||||
# Select Function
|
||||
filter_func: typ.Callable[[jax.Array], jax.Array] = {
|
||||
|
@ -201,6 +211,10 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
||||
params = output_sockets['Data'][ct.FlowKind.Params]
|
||||
|
||||
# Check Flow
|
||||
if any(ct.FlowSignal.check(inp) for inp in [lazy_value_func, params]):
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
# Compute Array
|
||||
return ct.ArrayFlow(
|
||||
values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs),
|
||||
|
@ -221,15 +235,14 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
# Retrieve Inputs
|
||||
info = input_sockets['Data']
|
||||
|
||||
# Compute Bound/Free Parameters
|
||||
## Empty Dimension -> Empty InfoFlow
|
||||
if input_sockets['Data'] != ct.InfoFlow() and props['dim'] != 'NONE':
|
||||
axis = info.dim_names.index(props['dim'])
|
||||
else:
|
||||
return ct.InfoFlow()
|
||||
# Check Flow
|
||||
if ct.FlowSignal.check(info) or props['dim'] == 'NONE':
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
# Compute Information
|
||||
## Compute Info w/By-Operation Change to Dimensions
|
||||
axis = info.dim_names.index(props['dim'])
|
||||
|
||||
if (props['active_socket_set'], props['operation']) in [
|
||||
('By Dim', 'SQUEEZE'),
|
||||
('By Dim Value', 'FIX'),
|
||||
|
@ -246,8 +259,8 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
output_units=info.output_units,
|
||||
)
|
||||
|
||||
# Fallback to Empty InfoFlow
|
||||
return ct.InfoFlow()
|
||||
msg = f'Active socket set {props["active_socket_set"]} and operation {props["operation"]} don\'t have an InfoFlow defined'
|
||||
raise RuntimeError(msg)
|
||||
|
||||
@events.computes_output_socket(
|
||||
'Data',
|
||||
|
@ -264,6 +277,9 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
info = input_sockets['Data'][ct.FlowKind.Info]
|
||||
params = input_sockets['Data'][ct.FlowKind.Params]
|
||||
|
||||
if any(ct.FlowSignal.check(inp) for inp in [info, params]):
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
# Compute Composed Parameters
|
||||
## -> Only operations that add parameters.
|
||||
## -> A dimension must be selected.
|
||||
|
@ -274,7 +290,7 @@ class FilterMathNode(base.MaxwellSimNode):
|
|||
('By Dim Value', 'FIX'),
|
||||
]
|
||||
and props['dim'] != 'NONE'
|
||||
and input_sockets['Value'] is not None
|
||||
and not ct.FlowSignal.check(input_sockets['Value'])
|
||||
):
|
||||
# Compute IDX Corresponding to Coordinate Value
|
||||
## -> Each dimension declares a unit-aware real number at each index.
|
||||
|
|
|
@ -54,9 +54,8 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
)
|
||||
|
||||
def search_operations(self) -> list[ct.BLEnumElement]:
|
||||
items = []
|
||||
if self.active_socket_set == 'By Element':
|
||||
items += [
|
||||
items = [
|
||||
# General
|
||||
('REAL', 'ℝ(v)', 'real(v) (by el)'),
|
||||
('IMAG', 'Im(v)', 'imag(v) (by el)'),
|
||||
|
@ -73,11 +72,11 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
('ATAN', 'atan v', 'atan(v) (by el)'),
|
||||
]
|
||||
elif self.active_socket_set in 'By Vector':
|
||||
items += [
|
||||
items = [
|
||||
('NORM_2', '||v||₂', 'norm(v, 2) (by Vec)'),
|
||||
]
|
||||
elif self.active_socket_set == 'By Matrix':
|
||||
items += [
|
||||
items = [
|
||||
# Matrix -> Number
|
||||
('DET', 'det V', 'det(V) (by Mat)'),
|
||||
('COND', 'κ(V)', 'cond(V) (by Mat)'),
|
||||
|
@ -96,7 +95,10 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
('SVD', 'svd V', 'svd(V) -> U·Σ·V† (by Mat)'),
|
||||
]
|
||||
elif self.active_socket_set == 'Expr':
|
||||
items += [('EXPR_EL', 'By Element', 'Expression-defined (by el)')]
|
||||
items = [('EXPR_EL', 'By Element', 'Expression-defined (by el)')]
|
||||
else:
|
||||
msg = f'Active socket set {self.active_socket_set} is unknown'
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return [(*item, '', i) for i, item in enumerate(items)]
|
||||
|
||||
|
@ -127,6 +129,14 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
input_sockets_optional={'Mapper': True},
|
||||
)
|
||||
def compute_data(self, props: dict, input_sockets: dict):
|
||||
if (
|
||||
ct.FlowSignal.check(input_sockets['Data']) or props['operation'] == 'NONE'
|
||||
) or (
|
||||
props['active_socket_set'] == 'Expr'
|
||||
and ct.FlowSignal.check(input_sockets['Mapper'])
|
||||
):
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
mapping_func: typ.Callable[[jax.Array], jax.Array] = {
|
||||
'By Element': {
|
||||
'REAL': lambda data: jnp.real(data),
|
||||
|
@ -186,10 +196,16 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
def compute_array(self, output_sockets: dict) -> ct.ArrayFlow:
|
||||
lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
||||
params = output_sockets['Data'][ct.FlowKind.Params]
|
||||
return ct.ArrayFlow(
|
||||
values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs),
|
||||
unit=None, ## TODO: Unit Propagation
|
||||
)
|
||||
|
||||
if all(not ct.FlowSignal.check(inp) for inp in [lazy_value_func, params]):
|
||||
return ct.ArrayFlow(
|
||||
values=lazy_value_func.func_jax(
|
||||
*params.func_args, **params.func_kwargs
|
||||
),
|
||||
unit=None,
|
||||
)
|
||||
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
####################
|
||||
# - Compute Auxiliary: Info / Params
|
||||
|
@ -205,11 +221,16 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
info = input_sockets['Data']
|
||||
|
||||
# Complex -> Real
|
||||
if props['active_socket_set'] == 'By Element' and props['operation'] in [
|
||||
'REAL',
|
||||
'IMAG',
|
||||
'ABS',
|
||||
]:
|
||||
if (
|
||||
props['active_socket_set'] == 'By Element'
|
||||
and props['operation']
|
||||
in [
|
||||
'REAL',
|
||||
'IMAG',
|
||||
'ABS',
|
||||
]
|
||||
and not ct.FlowSignal.check(info)
|
||||
):
|
||||
return ct.InfoFlow(
|
||||
dim_names=info.dim_names,
|
||||
dim_idx=info.dim_idx,
|
||||
|
@ -232,7 +253,7 @@ class MapMathNode(base.MaxwellSimNode):
|
|||
input_sockets={'Data'},
|
||||
input_socket_kinds={'Data': ct.FlowKind.Params},
|
||||
)
|
||||
def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow:
|
||||
def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow | ct.FlowSignal:
|
||||
return input_sockets['Data']
|
||||
|
||||
|
||||
|
|
|
@ -218,36 +218,42 @@ class VizNode(base.MaxwellSimNode):
|
|||
## - Mode Searcher
|
||||
#####################
|
||||
@property
|
||||
def _info(self) -> ct.InfoFlow:
|
||||
def data_info(self) -> ct.InfoFlow:
|
||||
return self._compute_input('Data', kind=ct.FlowKind.Info)
|
||||
|
||||
def search_modes(self) -> list[ct.BLEnumElement]:
|
||||
info = self._info
|
||||
return [
|
||||
(
|
||||
viz_mode,
|
||||
VizMode.to_name(viz_mode),
|
||||
VizMode.to_name(viz_mode),
|
||||
VizMode.to_icon(viz_mode),
|
||||
i,
|
||||
)
|
||||
for i, viz_mode in enumerate(VizMode.valid_modes_for(info))
|
||||
]
|
||||
if not ct.FlowSignal.check(self.data_info):
|
||||
return [
|
||||
(
|
||||
viz_mode,
|
||||
VizMode.to_name(viz_mode),
|
||||
VizMode.to_name(viz_mode),
|
||||
VizMode.to_icon(viz_mode),
|
||||
i,
|
||||
)
|
||||
for i, viz_mode in enumerate(VizMode.valid_modes_for(self.data_info))
|
||||
]
|
||||
|
||||
return []
|
||||
|
||||
#####################
|
||||
## - Target Searcher
|
||||
#####################
|
||||
def search_targets(self) -> list[ct.BLEnumElement]:
|
||||
return [
|
||||
(
|
||||
viz_target,
|
||||
VizTarget.to_name(viz_target),
|
||||
VizTarget.to_name(viz_target),
|
||||
VizTarget.to_icon(viz_target),
|
||||
i,
|
||||
)
|
||||
for i, viz_target in enumerate(VizTarget.valid_targets_for(self.viz_mode))
|
||||
]
|
||||
if self.viz_mode != 'NONE':
|
||||
return [
|
||||
(
|
||||
viz_target,
|
||||
VizTarget.to_name(viz_target),
|
||||
VizTarget.to_name(viz_target),
|
||||
VizTarget.to_icon(viz_target),
|
||||
i,
|
||||
)
|
||||
for i, viz_target in enumerate(
|
||||
VizTarget.valid_targets_for(self.viz_mode)
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
#####################
|
||||
## - UI
|
||||
|
@ -264,17 +270,20 @@ class VizNode(base.MaxwellSimNode):
|
|||
@events.on_value_changed(
|
||||
socket_name='Data',
|
||||
input_sockets={'Data'},
|
||||
run_on_init=True,
|
||||
input_socket_kinds={'Data': ct.FlowKind.Info},
|
||||
input_sockets_optional={'Data': True},
|
||||
run_on_init=True,
|
||||
)
|
||||
def on_socket_set_changed(self, input_sockets: dict):
|
||||
self.viz_mode = bl_cache.Signal.ResetEnumItems
|
||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
||||
def on_any_changed(self, input_sockets: dict):
|
||||
if not ct.FlowSignal.check_single(
|
||||
input_sockets['Data'], ct.FlowSignal.FlowPending
|
||||
):
|
||||
self.viz_mode = bl_cache.Signal.ResetEnumItems
|
||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
||||
|
||||
@events.on_value_changed(
|
||||
prop_name='viz_mode',
|
||||
# run_on_init=True,
|
||||
## run_on_init: Implicitly triggered.
|
||||
)
|
||||
def on_viz_mode_changed(self):
|
||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
||||
|
@ -287,7 +296,6 @@ class VizNode(base.MaxwellSimNode):
|
|||
props={'viz_mode', 'viz_target', 'colormap'},
|
||||
input_sockets={'Data'},
|
||||
input_socket_kinds={'Data': {ct.FlowKind.Array, ct.FlowKind.Info}},
|
||||
input_sockets_optional={'Data': True},
|
||||
stop_propagation=True,
|
||||
)
|
||||
def on_show_plot(
|
||||
|
@ -296,12 +304,19 @@ class VizNode(base.MaxwellSimNode):
|
|||
input_sockets: dict,
|
||||
props: dict,
|
||||
):
|
||||
# Retrieve Inputs
|
||||
array_flow = input_sockets['Data'][ct.FlowKind.Array]
|
||||
info = input_sockets['Data'][ct.FlowKind.Info]
|
||||
|
||||
if input_sockets['Data'] is None:
|
||||
# Check Flow
|
||||
if (
|
||||
any(ct.FlowSignal.check(inp) for inp in [array_flow, info])
|
||||
or props['viz_mode'] == 'NONE'
|
||||
or props['viz_target'] == 'NONE'
|
||||
):
|
||||
return
|
||||
|
||||
# Viz Target
|
||||
if props['viz_target'] == VizTarget.Plot2D:
|
||||
managed_objs['plot'].mpl_plot_to_image(
|
||||
lambda ax: VizMode.to_plotter(props['viz_mode'])(
|
||||
|
|
|
@ -597,7 +597,7 @@ class MaxwellSimNode(bpy.types.Node):
|
|||
)
|
||||
|
||||
if optional:
|
||||
return None
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket'
|
||||
raise ValueError(msg)
|
||||
|
@ -645,14 +645,8 @@ class MaxwellSimNode(bpy.types.Node):
|
|||
return output_socket_methods[0](self)
|
||||
|
||||
# Auxiliary Fallbacks
|
||||
if kind == ct.FlowKind.Info:
|
||||
return ct.InfoFlow()
|
||||
|
||||
if kind == ct.FlowKind.Params:
|
||||
return ct.ParamsFlow()
|
||||
|
||||
if optional:
|
||||
return None
|
||||
if optional or kind in [ct.FlowKind.Info, ct.FlowKind.Params]:
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
msg = f'No output method for ({output_socket_name}, {kind})'
|
||||
raise ValueError(msg)
|
||||
|
@ -777,6 +771,12 @@ class MaxwellSimNode(bpy.types.Node):
|
|||
# Invalidate Input Socket Cache
|
||||
if input_socket_name is not None:
|
||||
if socket_kinds is None:
|
||||
log.critical(
|
||||
'[%s] Invalidating: (%s, %s)',
|
||||
self.sim_node_name,
|
||||
input_socket_name,
|
||||
str(socket_kinds),
|
||||
)
|
||||
self._compute_input.invalidate(
|
||||
input_socket_name=input_socket_name,
|
||||
kind=...,
|
||||
|
@ -784,6 +784,12 @@ class MaxwellSimNode(bpy.types.Node):
|
|||
)
|
||||
else:
|
||||
for socket_kind in socket_kinds:
|
||||
log.critical(
|
||||
'[%s] Invalidating: (%s, %s)',
|
||||
self.sim_node_name,
|
||||
input_socket_name,
|
||||
str(socket_kind),
|
||||
)
|
||||
self._compute_input.invalidate(
|
||||
input_socket_name=input_socket_name,
|
||||
kind=socket_kind,
|
||||
|
@ -838,6 +844,15 @@ class MaxwellSimNode(bpy.types.Node):
|
|||
)
|
||||
for event_method in triggered_event_methods:
|
||||
stop_propagation |= event_method.stop_propagation
|
||||
log.critical(
|
||||
'$[%s] [%s %s %s %s] Running: (%s)',
|
||||
self.sim_node_name,
|
||||
event,
|
||||
socket_name,
|
||||
socket_kinds,
|
||||
prop_name,
|
||||
event_method.callback_info,
|
||||
)
|
||||
event_method(self)
|
||||
|
||||
# Propagate Event to All Sockets in "Trigger Direction"
|
||||
|
|
|
@ -270,6 +270,8 @@ def event_decorator(
|
|||
)
|
||||
|
||||
# Call Method
|
||||
## If there is a FlowPending, then the method would fail.
|
||||
## Therefore, propagate FlowPending if found.
|
||||
return method(
|
||||
node,
|
||||
**method_kw_args,
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import typing as typ
|
||||
from pathlib import Path
|
||||
|
||||
from blender_maxwell.utils import logger
|
||||
import bpy
|
||||
import tidy3d as td
|
||||
|
||||
from blender_maxwell.services import tdcloud
|
||||
from blender_maxwell.utils import bl_cache, logger
|
||||
|
||||
from ......services import tdcloud
|
||||
from .... import contracts as ct
|
||||
from .... import sockets
|
||||
from ... import base, events
|
||||
|
@ -11,6 +14,40 @@ from ... import base, events
|
|||
log = logger.get(__name__)
|
||||
|
||||
|
||||
class LoadCloudSim(bpy.types.Operator):
|
||||
bl_idname = ct.OperatorType.NodeLoadCloudSim
|
||||
bl_label = '(Re)Load Sim'
|
||||
bl_description = '(Re)Load simulation data associated with the attached cloud task'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
# Node Type
|
||||
hasattr(context, 'node')
|
||||
and hasattr(context.node, 'node_type')
|
||||
and context.node.node_type == ct.NodeType.Tidy3DWebImporter
|
||||
# Cloud Status
|
||||
and tdcloud.IS_ONLINE
|
||||
and tdcloud.IS_AUTHENTICATED
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
node = context.node
|
||||
|
||||
# Try Loading Simulation Data
|
||||
node.sim_data = bl_cache.Signal.InvalidateCache
|
||||
sim_data = node.sim_data
|
||||
if sim_data is None:
|
||||
self.report(
|
||||
{'ERROR'},
|
||||
'Sim Data could not be loaded. Check your network connection.',
|
||||
)
|
||||
else:
|
||||
self.report({'INFO'}, 'Sim Data loaded.')
|
||||
|
||||
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.
|
||||
|
||||
|
@ -34,68 +71,89 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
|
|||
),
|
||||
}
|
||||
|
||||
####################
|
||||
# - Event Methods
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
'FDTD Sim Data',
|
||||
input_sockets={'Cloud Task'},
|
||||
)
|
||||
def compute_sim_data(self, input_sockets: dict) -> str:
|
||||
## TODO: REMOVE TEST
|
||||
log.info('Loading SimulationData File')
|
||||
import sys
|
||||
sim_data_loaded: bool = bl_cache.BLField(False)
|
||||
|
||||
for module_name, module in sys.modules.copy().items():
|
||||
if module_name == '__mp_main__':
|
||||
print('Problematic Module Entry', module_name)
|
||||
print(module)
|
||||
# print('MODULE REPR', module)
|
||||
continue
|
||||
# return td.SimulationData.from_file(
|
||||
# fname='/home/sofus/src/blender_maxwell/dev/sim_demo.hdf5'
|
||||
# )
|
||||
|
||||
# Validate Task Availability
|
||||
if (cloud_task := input_sockets['Cloud Task']) is None:
|
||||
msg = f'"{self.bl_label}" CloudTask doesn\'t exist'
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# Validate Task Existence
|
||||
if not isinstance(cloud_task, tdcloud.CloudTask):
|
||||
msg = f'"{self.bl_label}" CloudTask input "{cloud_task}" has wrong "should_exists", as it isn\'t an instance of tdcloud.CloudTask'
|
||||
raise TypeError(msg)
|
||||
|
||||
# Validate Task Status
|
||||
if cloud_task.status != 'success':
|
||||
msg = f'"{self.bl_label}" CloudTask is "{cloud_task.status}", not "success"'
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# Download and Return SimData
|
||||
return tdcloud.TidyCloudTasks.download_task_sim_data(
|
||||
cloud_task, _sim_data_cache_path(cloud_task.task_id)
|
||||
@bl_cache.cached_bl_property()
|
||||
def sim_data(self) -> td.SimulationData | None:
|
||||
cloud_task = self._compute_input(
|
||||
'Cloud Task', kind=ct.FlowKind.Value, optional=True
|
||||
)
|
||||
|
||||
@events.on_value_changed(
|
||||
socket_name='Cloud Task', run_on_init=True, input_sockets={'Cloud Task'}
|
||||
)
|
||||
def on_cloud_task_changed(self, input_sockets: dict):
|
||||
if (
|
||||
(cloud_task := input_sockets['Cloud Task']) is not None
|
||||
# Check Flow
|
||||
not ct.FlowSignal.check(cloud_task)
|
||||
# Check Task
|
||||
and cloud_task is not None
|
||||
and isinstance(cloud_task, tdcloud.CloudTask)
|
||||
and cloud_task.status == 'success'
|
||||
):
|
||||
self.loose_output_sockets = {
|
||||
'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
|
||||
}
|
||||
sim_data = tdcloud.TidyCloudTasks.download_task_sim_data(
|
||||
cloud_task, _sim_data_cache_path(cloud_task.task_id)
|
||||
)
|
||||
self.sim_data_loaded = True
|
||||
return sim_data
|
||||
|
||||
return None
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
def draw_operators(self, context, layout):
|
||||
if self.sim_data_loaded:
|
||||
layout.operator(ct.OperatorType.NodeLoadCloudSim, text='Reload Sim')
|
||||
else:
|
||||
layout.operator(ct.OperatorType.NodeLoadCloudSim, text='Load Sim')
|
||||
|
||||
####################
|
||||
# - Events
|
||||
####################
|
||||
@events.on_value_changed(socket_name='Cloud Task')
|
||||
def on_cloud_task_changed(self):
|
||||
self.inputs['Cloud Task'].on_cloud_updated()
|
||||
## TODO: Must we babysit sockets like this?
|
||||
|
||||
@events.on_value_changed(
|
||||
prop_name='sim_data_loaded', run_on_init=True, props={'sim_data_loaded'}
|
||||
)
|
||||
def on_cloud_task_changed(self, props: dict):
|
||||
if props['sim_data_loaded']:
|
||||
if not self.loose_output_sockets:
|
||||
self.loose_output_sockets = {
|
||||
'Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
|
||||
}
|
||||
elif self.loose_output_sockets:
|
||||
self.loose_output_sockets = {}
|
||||
|
||||
####################
|
||||
# - Output
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
'Sim Data',
|
||||
props={'sim_data_loaded'},
|
||||
input_sockets={'Cloud Task'},
|
||||
)
|
||||
def compute_sim_data(self, props: dict, input_sockets: dict) -> str:
|
||||
if props['sim_data_loaded']:
|
||||
cloud_task = input_sockets['Cloud Task']
|
||||
if (
|
||||
# Check Flow
|
||||
not ct.FlowSignal.check(cloud_task)
|
||||
# Check Task
|
||||
and cloud_task is not None
|
||||
and isinstance(cloud_task, tdcloud.CloudTask)
|
||||
and cloud_task.status == 'success'
|
||||
):
|
||||
return self.sim_data
|
||||
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
return ct.FlowSignal.FlowPending
|
||||
|
||||
|
||||
####################
|
||||
# - Blender Registration
|
||||
####################
|
||||
BL_REGISTER = [
|
||||
LoadCloudSim,
|
||||
Tidy3DWebImporterNode,
|
||||
]
|
||||
BL_NODES = {
|
||||
|
|
|
@ -209,7 +209,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
|||
else:
|
||||
self.cache_est_cost = -1.0
|
||||
self.loose_output_sockets = {}
|
||||
self.inputs['Cloud Task'].sync_prepare_new_task()
|
||||
self.inputs['Cloud Task'].on_prepare_new_task()
|
||||
self.inputs['Cloud Task'].locked = False
|
||||
|
||||
self.on_prop_changed('tracked_task_id', context)
|
||||
|
@ -249,7 +249,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
|||
# 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)
|
||||
self.inputs['Cloud Task'].on_new_task_created(cloud_task)
|
||||
|
||||
# Track the Newly Uploaded Task ID
|
||||
self.tracked_task_id = cloud_task.task_id
|
||||
|
|
|
@ -548,7 +548,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
Returns:
|
||||
An empty `ct.InfoFlow`.
|
||||
"""
|
||||
return ct.InfoFlow()
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
# Param
|
||||
@property
|
||||
|
@ -561,7 +561,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
Returns:
|
||||
An empty `ct.ParamsFlow`.
|
||||
"""
|
||||
return ct.ParamsFlow()
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
####################
|
||||
# - FlowKind: Auxiliary
|
||||
|
@ -577,8 +577,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
Raises:
|
||||
NotImplementedError: When used without being overridden.
|
||||
"""
|
||||
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.Value", but socket does not define it'
|
||||
raise NotImplementedError(msg)
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
@value.setter
|
||||
def value(self, value: ct.ValueFlow) -> None:
|
||||
|
@ -604,8 +603,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
Raises:
|
||||
NotImplementedError: When used without being overridden.
|
||||
"""
|
||||
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.Array", but socket does not define it'
|
||||
raise NotImplementedError(msg)
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
@array.setter
|
||||
def array(self, value: ct.ArrayFlow) -> None:
|
||||
|
@ -631,8 +629,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
Raises:
|
||||
NotImplementedError: When used without being overridden.
|
||||
"""
|
||||
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.LazyValueFunc", but socket does not define it'
|
||||
raise NotImplementedError(msg)
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
@lazy_value_func.setter
|
||||
def lazy_value_func(self, lazy_value_func: ct.LazyValueFuncFlow) -> None:
|
||||
|
@ -658,8 +655,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
Raises:
|
||||
NotImplementedError: When used without being overridden.
|
||||
"""
|
||||
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.LazyArrayRange", but socket does not define it'
|
||||
raise NotImplementedError(msg)
|
||||
return ct.FlowSignal.NoFlow
|
||||
|
||||
@lazy_array_range.setter
|
||||
def lazy_array_range(self, value: ct.LazyArrayRangeFlow) -> None:
|
||||
|
@ -892,7 +888,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
# Info Drawing
|
||||
if self.use_info_draw:
|
||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||
self.draw_info(info, col)
|
||||
if not ct.FlowSignal.check(info):
|
||||
self.draw_info(info, col)
|
||||
|
||||
def draw_output(
|
||||
self,
|
||||
|
@ -920,7 +917,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
|||
# Draw FlowKind.Info related Information
|
||||
if self.use_info_draw:
|
||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||
self.draw_info(info, col)
|
||||
if not ct.FlowSignal.check(info):
|
||||
self.draw_info(info, col)
|
||||
|
||||
####################
|
||||
# - UI Methods: Active FlowKind
|
||||
|
|
|
@ -34,10 +34,6 @@ class DataBLSocket(base.MaxwellSimSocket):
|
|||
must_match={'format': self.format},
|
||||
)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return None
|
||||
|
||||
####################
|
||||
# - UI
|
||||
####################
|
||||
|
|
|
@ -7,10 +7,6 @@ class MaxwellFDTDSimBLSocket(base.MaxwellSimSocket):
|
|||
socket_type = ct.SocketType.MaxwellFDTDSim
|
||||
bl_label = 'Maxwell FDTD Simulation'
|
||||
|
||||
@property
|
||||
def value(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
####################
|
||||
# - Socket Configuration
|
||||
|
|
|
@ -6,10 +6,6 @@ class MaxwellFDTDSimDataBLSocket(base.MaxwellSimSocket):
|
|||
socket_type = ct.SocketType.MaxwellFDTDSimData
|
||||
bl_label = 'Maxwell FDTD Simulation'
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return None
|
||||
|
||||
|
||||
####################
|
||||
# - Socket Configuration
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import enum
|
||||
|
||||
import bpy
|
||||
|
||||
from .....services import tdcloud
|
||||
from blender_maxwell.services import tdcloud
|
||||
from blender_maxwell.utils import bl_cache
|
||||
|
||||
from ... import contracts as ct
|
||||
from .. import base
|
||||
|
||||
|
@ -9,30 +13,32 @@ from .. import base
|
|||
# - Operators
|
||||
####################
|
||||
class ReloadFolderList(bpy.types.Operator):
|
||||
bl_idname = 'blender_maxwell.sockets__reload_folder_list'
|
||||
bl_idname = ct.OperatorType.SocketReloadCloudFolderList
|
||||
bl_label = 'Reload Tidy3D Folder List'
|
||||
bl_description = 'Reload the the cached Tidy3D folder list'
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return (
|
||||
tdcloud.IS_AUTHENTICATED
|
||||
tdcloud.IS_ONLINE
|
||||
and 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):
|
||||
socket = context.socket
|
||||
bl_socket = context.socket
|
||||
|
||||
tdcloud.TidyCloudFolders.update_folders()
|
||||
tdcloud.TidyCloudTasks.update_tasks(socket.existing_folder_id)
|
||||
tdcloud.TidyCloudTasks.update_tasks(bl_socket.existing_folder_id)
|
||||
bl_socket.on_cloud_updated()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class Authenticate(bpy.types.Operator):
|
||||
bl_idname = 'blender_maxwell.sockets__authenticate'
|
||||
bl_idname = ct.OperatorType.SocketCloudAuthenticate
|
||||
bl_label = 'Authenticate Tidy3D'
|
||||
bl_description = 'Authenticate the Tidy3D Web API from a Cloud Task socket'
|
||||
|
||||
|
@ -51,6 +57,7 @@ class Authenticate(bpy.types.Operator):
|
|||
if not tdcloud.check_authentication():
|
||||
tdcloud.authenticate_with_api_key(bl_socket.api_key)
|
||||
bl_socket.api_key = ''
|
||||
bl_socket.on_cloud_updated()
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
@ -59,6 +66,16 @@ class Authenticate(bpy.types.Operator):
|
|||
# - Socket
|
||||
####################
|
||||
class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||
"""Interact with Tidy3D Cloud Tasks.
|
||||
|
||||
Attributes:
|
||||
api_key: API key for the Tidy3D cloud.
|
||||
should_exist: Whether or not the cloud task should already exist.
|
||||
existing_folder_id: ID of an existing folder on the Tidy3D cloud.
|
||||
existing_task_id: ID of an existing task on the Tidy3D cloud.
|
||||
new_task_name: Name of a new task to submit to the Tidy3D cloud.
|
||||
"""
|
||||
|
||||
socket_type = ct.SocketType.Tidy3DCloudTask
|
||||
bl_label = 'Tidy3D Cloud Task'
|
||||
|
||||
|
@ -67,81 +84,90 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
|||
####################
|
||||
# - Properties
|
||||
####################
|
||||
# Authentication
|
||||
api_key: bpy.props.StringProperty(
|
||||
name='API Key',
|
||||
description='API Key for the Tidy3D Cloud',
|
||||
default='',
|
||||
options={'SKIP_SAVE'},
|
||||
subtype='PASSWORD',
|
||||
api_key: str = bl_cache.BLField('', prop_ui=True, str_secret=True)
|
||||
should_exist: bool = bl_cache.BLField(False)
|
||||
|
||||
existing_folder_id: enum.Enum = bl_cache.BLField(
|
||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
|
||||
)
|
||||
existing_task_id: enum.Enum = bl_cache.BLField(
|
||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_tasks()
|
||||
)
|
||||
|
||||
# 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,
|
||||
)
|
||||
new_task_name: str = bl_cache.BLField('', prop_ui=True)
|
||||
|
||||
# Identifiers
|
||||
existing_folder_id: bpy.props.EnumProperty(
|
||||
name='Folder of Cloud Tasks',
|
||||
description='An existing folder on the Tidy3D Cloud',
|
||||
items=lambda self, _: self.retrieve_folders(),
|
||||
update=(lambda self, context: self.on_prop_changed('existing_folder_id', context)),
|
||||
)
|
||||
existing_task_id: bpy.props.EnumProperty(
|
||||
name='Existing Cloud Task',
|
||||
description='An existing task on the Tidy3D Cloud, within the given folder',
|
||||
items=lambda self, _: self.retrieve_tasks(),
|
||||
update=(lambda self, context: self.on_prop_changed('existing_task_id', context)),
|
||||
)
|
||||
@property
|
||||
def capabilities(self) -> ct.CapabilitiesFlow:
|
||||
return ct.CapabilitiesFlow(
|
||||
socket_type=self.socket_type,
|
||||
active_kind=self.active_kind,
|
||||
must_match={'should_exist': self.should_exist},
|
||||
)
|
||||
|
||||
# (Potential) New Task
|
||||
new_task_name: bpy.props.StringProperty(
|
||||
name='New Cloud Task Name',
|
||||
description='Name of a new task to submit to the Tidy3D Cloud',
|
||||
default='',
|
||||
update=(lambda self, context: self.on_prop_changed('new_task_name', context)),
|
||||
)
|
||||
|
||||
####################
|
||||
# - Property Methods
|
||||
####################
|
||||
def sync_existing_folder_id(self, context):
|
||||
folder_task_ids = self.retrieve_tasks()
|
||||
|
||||
self.existing_task_id = folder_task_ids[0][0]
|
||||
## There's guaranteed to at least be one element, even if it's "NONE".
|
||||
|
||||
self.on_prop_changed('existing_folder_id', context)
|
||||
|
||||
def retrieve_folders(self) -> list[tuple]:
|
||||
folders = tdcloud.TidyCloudFolders.folders()
|
||||
if not folders:
|
||||
return [('NONE', 'None', 'No folders')]
|
||||
|
||||
return [
|
||||
(
|
||||
cloud_folder.folder_id,
|
||||
cloud_folder.folder_name,
|
||||
f"Folder 'cloud_folder.folder_name' with ID {folder_id}",
|
||||
)
|
||||
for folder_id, cloud_folder in folders.items()
|
||||
]
|
||||
|
||||
def retrieve_tasks(self) -> list[tuple]:
|
||||
if (
|
||||
cloud_folder := tdcloud.TidyCloudFolders.folders().get(
|
||||
@property
|
||||
def value(
|
||||
self,
|
||||
) -> tuple[tdcloud.CloudTaskName, tdcloud.CloudFolder] | tdcloud.CloudTask | None:
|
||||
if tdcloud.IS_AUTHENTICATED:
|
||||
# Retrieve Folder
|
||||
cloud_folder = tdcloud.TidyCloudFolders.folders().get(
|
||||
self.existing_folder_id
|
||||
)
|
||||
) is None:
|
||||
return [('NONE', 'None', "Folder doesn't exist")]
|
||||
if cloud_folder is None:
|
||||
msg = f"Selected folder {cloud_folder} doesn't exist (it was probably deleted elsewhere)"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# Doesn't Exist: Return Construction Information
|
||||
if not self.should_exist:
|
||||
return (self.new_task_name, cloud_folder)
|
||||
|
||||
# No Task Selected: Return None
|
||||
if self.existing_task_id == 'NONE':
|
||||
return None
|
||||
|
||||
# Retrieve Cloud Task
|
||||
cloud_task = tdcloud.TidyCloudTasks.tasks(cloud_folder).get(
|
||||
self.existing_task_id
|
||||
)
|
||||
if cloud_task is None:
|
||||
msg = f"Selected task {cloud_task} doesn't exist (it was probably deleted elsewhere)"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
return cloud_task
|
||||
|
||||
return None
|
||||
|
||||
####################
|
||||
# - Searchers
|
||||
####################
|
||||
def search_cloud_folders(self) -> list[ct.BLEnumElement]:
|
||||
if tdcloud.IS_AUTHENTICATED:
|
||||
return [
|
||||
(
|
||||
cloud_folder.folder_id,
|
||||
cloud_folder.folder_name,
|
||||
f'Folder {cloud_folder.folder_name} (ID={folder_id})',
|
||||
'',
|
||||
i,
|
||||
)
|
||||
for i, (folder_id, cloud_folder) in enumerate(
|
||||
tdcloud.TidyCloudFolders.folders().items()
|
||||
)
|
||||
]
|
||||
|
||||
return []
|
||||
|
||||
def search_cloud_tasks(self) -> list[ct.BLEnumElement]:
|
||||
if self.existing_folder_id == 'NONE' or not tdcloud.IS_AUTHENTICATED:
|
||||
return []
|
||||
|
||||
# Get Cloud Folder
|
||||
cloud_folder = tdcloud.TidyCloudFolders.folders().get(self.existing_folder_id)
|
||||
if cloud_folder is None:
|
||||
return []
|
||||
|
||||
# Get Cloud Tasks
|
||||
tasks = tdcloud.TidyCloudTasks.tasks(cloud_folder)
|
||||
if not tasks:
|
||||
return [('NONE', 'None', 'No tasks in folder')]
|
||||
|
||||
return [
|
||||
(
|
||||
## Task ID
|
||||
|
@ -158,9 +184,9 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
|||
## Task Description
|
||||
f'Task Status: {task.status}',
|
||||
## Status Icon
|
||||
_icon
|
||||
icon
|
||||
if (
|
||||
_icon := {
|
||||
icon := {
|
||||
'draft': 'SEQUENCE_COLOR_08',
|
||||
'initialized': 'SHADING_SOLID',
|
||||
'queued': 'SEQUENCE_COLOR_03',
|
||||
|
@ -185,52 +211,28 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
|||
]
|
||||
|
||||
####################
|
||||
# - Task Sync Methods
|
||||
# - Node-Initiated Updates
|
||||
####################
|
||||
def sync_created_new_task(self, cloud_task):
|
||||
"""Called whenever the task specified in `new_task_name` has been actually created.
|
||||
|
||||
This changes the socket somewhat: Folder/task IDs are set, and the socket is switched to presume that the task exists.
|
||||
|
||||
If the socket is linked, then an error is raised.
|
||||
"""
|
||||
# Propagate along Link
|
||||
if self.is_linked:
|
||||
msg = 'Cannot sync newly created task to linked Cloud Task socket.'
|
||||
raise ValueError(msg)
|
||||
## TODO: A little aggressive. Is there a good use case?
|
||||
|
||||
# Synchronize w/New Task Information
|
||||
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 sync_prepare_new_task(self):
|
||||
"""Called to switch the socket to no longer presume that the task it specifies exists (yet).
|
||||
|
||||
If the socket is linked, then an error is raised.
|
||||
"""
|
||||
# Propagate along Link
|
||||
if self.is_linked:
|
||||
msg = 'Cannot sync newly created task to linked Cloud Task socket.'
|
||||
raise ValueError(msg)
|
||||
## TODO: A little aggressive. Is there a good use case?
|
||||
|
||||
# Synchronize w/New Task Information
|
||||
def on_prepare_new_task(self):
|
||||
self.should_exist = False
|
||||
|
||||
def on_cloud_updated(self):
|
||||
self.existing_folder_id = bl_cache.Signal.ResetEnumItems
|
||||
self.existing_task_id = bl_cache.Signal.ResetEnumItems
|
||||
|
||||
####################
|
||||
# - Socket UI
|
||||
# - UI
|
||||
####################
|
||||
def draw_label_row(self, row: bpy.types.UILayout, text: str):
|
||||
row.label(text=text)
|
||||
|
||||
auth_icon = 'LOCKVIEW_ON' if tdcloud.IS_AUTHENTICATED else 'LOCKVIEW_OFF'
|
||||
row.operator(
|
||||
Authenticate.bl_idname,
|
||||
text='',
|
||||
icon=auth_icon,
|
||||
)
|
||||
row.label(text='', icon=auth_icon)
|
||||
|
||||
def draw_prelock(
|
||||
self,
|
||||
|
@ -245,11 +247,11 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
|||
row.label(text='Tidy3D API Key')
|
||||
|
||||
row = col.row()
|
||||
row.prop(self, 'api_key', text='')
|
||||
row.prop(self, self.blfields['api_key'], text='')
|
||||
|
||||
row = col.row()
|
||||
row.operator(
|
||||
Authenticate.bl_idname,
|
||||
ct.OperatorType.SocketCloudAuthenticate,
|
||||
text='Connect',
|
||||
)
|
||||
|
||||
|
@ -260,9 +262,9 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
|||
# Cloud Folder Selector
|
||||
row = col.row()
|
||||
row.label(icon='FILE_FOLDER')
|
||||
row.prop(self, 'existing_folder_id', text='')
|
||||
row.prop(self, self.blfields['existing_folder_id'], text='')
|
||||
row.operator(
|
||||
ReloadFolderList.bl_idname,
|
||||
ct.OperatorType.SocketReloadCloudFolderList,
|
||||
text='',
|
||||
icon='FILE_REFRESH',
|
||||
)
|
||||
|
@ -272,47 +274,14 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
|||
if not self.should_exist:
|
||||
row = col.row()
|
||||
row.label(icon='NETWORK_DRIVE')
|
||||
row.prop(self, 'new_task_name', text='')
|
||||
row.prop(self, self.blfields['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)
|
||||
row.prop(self, self.blfields['existing_task_id'], text='')
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -268,7 +268,7 @@ class TidyCloudTasks:
|
|||
|
||||
# Get Sim Data (from file and/or download)
|
||||
if path_sim.is_file():
|
||||
log.info('Loading Cloud Task "%s" from "%s"', cloud_task.cloud_id, path_sim)
|
||||
log.info('Loading Cloud Task "%s" from "%s"', cloud_task.task_id, path_sim)
|
||||
sim_data = td.SimulationData.from_file(str(path_sim))
|
||||
else:
|
||||
log.info(
|
||||
|
@ -420,7 +420,7 @@ class TidyCloudTasks:
|
|||
|
||||
# Repopulate All Caches
|
||||
## By deleting the folder ID, all tasks within will be reloaded
|
||||
del cls.cache_folder_tasks[folder_id]
|
||||
cls.cache_folder_tasks.pop(folder_id, None)
|
||||
|
||||
return dict(cls.tasks(cloud_folder).items())
|
||||
|
||||
|
|
|
@ -375,6 +375,7 @@ class CachedBLProperty:
|
|||
return
|
||||
|
||||
if value == Signal.InvalidateCache:
|
||||
log.critical('![%s] Invalidating %s', str(bl_instance), str(self))
|
||||
self._invalidate_cache(bl_instance)
|
||||
return
|
||||
|
||||
|
@ -447,7 +448,7 @@ class CachedBLProperty:
|
|||
####################
|
||||
# - Property Decorators
|
||||
####################
|
||||
def cached_bl_property(persist: bool = ...):
|
||||
def cached_bl_property(persist: bool = False):
|
||||
"""Decorator creating a descriptor that caches a computed attribute of a Blender node/socket.
|
||||
|
||||
Many such `bl_instance`s rely on fast access to computed, cached properties, for example to ensure that `draw()` remains effectively non-blocking.
|
||||
|
@ -545,6 +546,7 @@ class BLField:
|
|||
self._str_cb = str_cb
|
||||
self._enum_cb = enum_cb
|
||||
|
||||
## HUGE TODO: Persist these
|
||||
self._str_cb_cache = {}
|
||||
self._enum_cb_cache = {}
|
||||
|
||||
|
@ -575,6 +577,7 @@ class BLField:
|
|||
Thus, whenever the user wants the items in the enum to update, they must manually set the descriptor attribute to the value `Signal.ResetEnumItems`.
|
||||
"""
|
||||
if self._enum_cb_cache.get(_self.instance_id) is None:
|
||||
log.critical('REGEN ENUM')
|
||||
# Retrieve Dynamic Enum Items
|
||||
enum_items = self._enum_cb(_self, context)
|
||||
|
||||
|
|
Loading…
Reference in New Issue