Compare commits
No commits in common. "785995117e24f94f6b052ae6dce6c812df5b4db5" and "a3defd3c1ca045a2991e1235feee5daa674da710" have entirely different histories.
785995117e
...
a3defd3c1c
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>)
|
- [ ] 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.
|
- 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.
|
- This also maybe enables some very interesting use cases when it comes to ex. static verifiability of data provided to event callbacks.
|
||||||
- [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.
|
- [ ] 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.
|
- [ ] 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.
|
- [ ] `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
|
### Niceness
|
||||||
- [x] Rename the internal property to 'blfield__'.
|
- [x] Rename the internal property to 'blfield__'.
|
||||||
- [x] Add a method that extracts the internal property name, for places where we need the Blender property name.
|
- [ ] 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.
|
- **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.
|
- 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.
|
||||||
- [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
|
- [ ] 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.
|
- Should also throw appropriate errors for invalid access from Python, while Blender handles access from the inside.
|
||||||
- This allows us
|
- This allows us
|
||||||
- [x] Similarly, a field method that gets the 'blfield__' prop data as a dictionary.
|
- [ ] Similarly, a field method that gets the 'blfield__' prop data as a dictionary.
|
||||||
|
|
||||||
### Parallel Features
|
### Parallel Features
|
||||||
- [x] Move serialization work to a `utils`.
|
- [x] Move serialization work to a `utils`.
|
||||||
|
@ -569,29 +569,3 @@ 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: 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: Better error properties for updating, access, setting, etc. .
|
||||||
- Benefit: Validate usage in a vastly greater amount of contexts.
|
- 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,10 +15,3 @@ class OperatorType(enum.StrEnum):
|
||||||
ManagePyDeps = enum.auto()
|
ManagePyDeps = enum.auto()
|
||||||
|
|
||||||
ConnectViewerNode = 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 (
|
from blender_maxwell.contracts import (
|
||||||
BLClass,
|
BLClass,
|
||||||
BLColorRGBA,
|
BLColorRGBA,
|
||||||
BLEnumElement,
|
BLEnumElement,
|
||||||
BLEnumID,
|
BLEnumID,
|
||||||
BLIcon,
|
BLIcon,
|
||||||
BLIconSet,
|
BLIconSet,
|
||||||
BLIDStruct,
|
BLIDStruct,
|
||||||
BLKeymapItem,
|
BLKeymapItem,
|
||||||
BLModifierType,
|
BLModifierType,
|
||||||
BLNodeTreeInterfaceID,
|
BLNodeTreeInterfaceID,
|
||||||
BLOperatorStatus,
|
BLOperatorStatus,
|
||||||
BLPropFlag,
|
BLPropFlag,
|
||||||
BLRegionType,
|
BLRegionType,
|
||||||
BLSpaceType,
|
BLSpaceType,
|
||||||
KeymapItemDef,
|
KeymapItemDef,
|
||||||
ManagedObjName,
|
ManagedObjName,
|
||||||
OperatorType,
|
OperatorType,
|
||||||
PanelType,
|
PanelType,
|
||||||
PresetName,
|
PresetName,
|
||||||
SocketName,
|
SocketName,
|
||||||
addon,
|
addon,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .bl_socket_desc_map import BL_SOCKET_DESCR_ANNOT_STRING, BL_SOCKET_DESCR_TYPE_MAP
|
from .bl_socket_desc_map import BL_SOCKET_DESCR_ANNOT_STRING, BL_SOCKET_DESCR_TYPE_MAP
|
||||||
|
@ -28,16 +28,15 @@ from .category_labels import NODE_CAT_LABELS
|
||||||
from .category_types import NodeCategory
|
from .category_types import NodeCategory
|
||||||
from .flow_events import FlowEvent
|
from .flow_events import FlowEvent
|
||||||
from .flow_kinds import (
|
from .flow_kinds import (
|
||||||
ArrayFlow,
|
ArrayFlow,
|
||||||
CapabilitiesFlow,
|
CapabilitiesFlow,
|
||||||
FlowKind,
|
FlowKind,
|
||||||
InfoFlow,
|
InfoFlow,
|
||||||
LazyArrayRangeFlow,
|
LazyArrayRangeFlow,
|
||||||
LazyValueFuncFlow,
|
LazyValueFuncFlow,
|
||||||
ParamsFlow,
|
ParamsFlow,
|
||||||
ValueFlow,
|
ValueFlow,
|
||||||
)
|
)
|
||||||
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
|
||||||
|
@ -94,5 +93,4 @@ __all__ = [
|
||||||
'LazyValueFuncFlow',
|
'LazyValueFuncFlow',
|
||||||
'ParamsFlow',
|
'ParamsFlow',
|
||||||
'ValueFlow',
|
'ValueFlow',
|
||||||
'FlowSignal',
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,71 +0,0 @@
|
||||||
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,7 +5,6 @@ import bpy
|
||||||
import jax
|
import jax
|
||||||
import jax.numpy as jnp
|
import jax.numpy as jnp
|
||||||
import sympy.physics.units as spu
|
import sympy.physics.units as spu
|
||||||
import tidy3d as td
|
|
||||||
|
|
||||||
from blender_maxwell.utils import bl_cache, logger
|
from blender_maxwell.utils import bl_cache, logger
|
||||||
from blender_maxwell.utils import extra_sympy_units as spux
|
from blender_maxwell.utils import extra_sympy_units as spux
|
||||||
|
@ -16,18 +15,9 @@ from .. import base, events
|
||||||
|
|
||||||
log = logger.get(__name__)
|
log = logger.get(__name__)
|
||||||
|
|
||||||
TDMonitorData: typ.TypeAlias = td.components.data.monitor_data.MonitorData
|
|
||||||
|
|
||||||
|
|
||||||
class ExtractDataNode(base.MaxwellSimNode):
|
class ExtractDataNode(base.MaxwellSimNode):
|
||||||
"""Extract data from sockets for further analysis.
|
"""Node for extracting data from particular objects.
|
||||||
|
|
||||||
# Socket Sets
|
|
||||||
## Sim Data
|
|
||||||
Extracts monitors from a `MaxwelFDTDSimDataSocket`.
|
|
||||||
|
|
||||||
## Monitor Data
|
|
||||||
Extracts array attributes from a `MaxwelFDTDSimDataSocket`.
|
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
extract_filter: Identifier for data to extract from the input.
|
extract_filter: Identifier for data to extract from the input.
|
||||||
|
@ -55,140 +45,31 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
enum_cb=lambda self, _: self.search_extract_filters(),
|
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: Sim Data
|
# - Computed Properties
|
||||||
####################
|
####################
|
||||||
@property
|
@property
|
||||||
def sim_data(self) -> td.SimulationData | None:
|
def has_sim_data(self) -> bool:
|
||||||
"""Computes the (cached) simulation data from the input socket.
|
return self.active_socket_set == 'Sim Data' and self.sim_data_monitor_nametype
|
||||||
|
|
||||||
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):
|
|
||||||
return 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
|
@property
|
||||||
def monitor_data(self) -> TDMonitorData | None:
|
def has_monitor_data(self) -> bool:
|
||||||
"""Computes the (cached) monitor data from the input socket.
|
return self.active_socket_set == 'Monitor Data' and self.monitor_data_type
|
||||||
|
|
||||||
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
|
# - Extraction Filter Search
|
||||||
####################
|
####################
|
||||||
def search_extract_filters(self) -> list[ct.BLEnumElement]:
|
def search_extract_filters(self) -> list[ct.BLEnumElement]:
|
||||||
"""Compute valid values for `self.extract_filter`, for a dynamic `EnumProperty`.
|
if self.has_sim_data:
|
||||||
|
|
||||||
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`.
|
|
||||||
"""
|
|
||||||
if self.sim_data_monitor_nametype is not None:
|
|
||||||
return [
|
return [
|
||||||
(monitor_name, monitor_name, monitor_type.removesuffix('Data'), '', i)
|
(monitor_name, monitor_name, monitor_type.removesuffix('Data'), '', i)
|
||||||
for i, (monitor_name, monitor_type) in enumerate(
|
for i, (monitor_name, monitor_type) in enumerate(
|
||||||
|
@ -196,15 +77,9 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.monitor_data_components is not None:
|
if self.has_monitor_data:
|
||||||
return [
|
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)
|
for i, component_name in enumerate(self.monitor_data_components)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -214,172 +89,156 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
def draw_props(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
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='')
|
col.prop(self, self.blfields['extract_filter'], text='')
|
||||||
|
|
||||||
def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
def draw_info(self, _: bpy.types.Context, col: bpy.types.UILayout) -> None:
|
||||||
"""Draw dynamic information in the node, for user consideration.
|
if self.has_sim_data or self.has_monitor_data:
|
||||||
|
|
||||||
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
|
# Header
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.alignment = 'CENTER'
|
row.alignment = 'CENTER'
|
||||||
if has_sim_data:
|
if self.has_sim_data:
|
||||||
row.label(text=f'{len(self.sim_data_monitor_nametype)} Monitors')
|
row.label(text=f'{len(self.sim_data_monitor_nametype)} Monitors')
|
||||||
elif has_monitor_data:
|
elif self.has_monitor_data:
|
||||||
row.label(text=f'{self.monitor_data_type} Monitor Data')
|
row.label(text=f'{self.monitor_data_type} Monitor Data')
|
||||||
|
|
||||||
# Monitor Data Contents
|
# Monitor Data Contents
|
||||||
## TODO: More compact double-split
|
|
||||||
## TODO: Output shape data.
|
|
||||||
## TODO: Local ENUM_MANY tabs for visible column selection?
|
|
||||||
row = col.row()
|
row = col.row()
|
||||||
box = row.box()
|
box = row.box()
|
||||||
grid = box.grid_flow(row_major=True, columns=2, even_columns=True)
|
grid = box.grid_flow(row_major=True, columns=2, even_columns=True)
|
||||||
for monitor_name, monitor_type in self.sim_data_monitor_nametype.items():
|
for name, desc in [
|
||||||
grid.label(text=monitor_name)
|
(name, desc) for idname, name, desc, *_ in self.search_extract_filters()
|
||||||
grid.label(text=monitor_type)
|
]:
|
||||||
|
grid.label(text=name)
|
||||||
|
grid.label(text=desc if desc else '')
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Events
|
# - Events
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
# Trigger
|
|
||||||
socket_name={'Sim Data', 'Monitor Data'},
|
socket_name={'Sim Data', 'Monitor Data'},
|
||||||
prop_name='active_socket_set',
|
prop_name='active_socket_set',
|
||||||
|
input_sockets={'Sim Data', 'Monitor Data'},
|
||||||
|
input_sockets_optional={'Sim Data': True, 'Monitor Data': True},
|
||||||
run_on_init=True,
|
run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_input_sockets_changed(self) -> None:
|
def on_sim_data_changed(self, input_sockets: dict):
|
||||||
"""Invalidate the cached properties for sim data / monitor data, and reset the extraction filter."""
|
if input_sockets['Sim Data'] is not None:
|
||||||
self.sim_data_monitor_nametype = bl_cache.Signal.InvalidateCache
|
# Sim Data Monitors: Set Name -> Type
|
||||||
self.monitor_data_type = bl_cache.Signal.InvalidateCache
|
self.sim_data_monitor_nametype = {
|
||||||
self.monitor_data_components = bl_cache.Signal.InvalidateCache
|
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
|
||||||
self.extract_filter = bl_cache.Signal.ResetEnumItems
|
self.extract_filter = bl_cache.Signal.ResetEnumItems
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Output: Sim Data -> Monitor Data
|
# - Output: Sim Data -> Monitor Data
|
||||||
####################
|
####################
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
|
||||||
'Monitor Data',
|
'Monitor Data',
|
||||||
kind=ct.FlowKind.Value,
|
kind=ct.FlowKind.Value,
|
||||||
# Loaded
|
|
||||||
props={'extract_filter'},
|
props={'extract_filter'},
|
||||||
input_sockets={'Sim Data'},
|
input_sockets={'Sim Data'},
|
||||||
)
|
)
|
||||||
def compute_monitor_data(
|
def compute_monitor_data(self, props: dict, input_sockets: dict):
|
||||||
self, props: dict, input_sockets: dict
|
if input_sockets['Sim Data'] is not None and props['extract_filter'] != 'NONE':
|
||||||
) -> 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 input_sockets['Sim Data'].monitor_data[props['extract_filter']]
|
||||||
|
|
||||||
# Propagate NoFlow
|
return None
|
||||||
if ct.FlowSignal.check_single(sim_data, ct.FlowSignal.NoFlow):
|
|
||||||
return ct.FlowSignal.NoFlow
|
|
||||||
|
|
||||||
return ct.FlowSignal.FlowPending
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Output: Monitor Data -> Data
|
# - Output: Monitor Data -> Data
|
||||||
####################
|
####################
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
|
||||||
'Data',
|
'Data',
|
||||||
kind=ct.FlowKind.Array,
|
kind=ct.FlowKind.Array,
|
||||||
# Loaded
|
|
||||||
props={'extract_filter'},
|
props={'extract_filter'},
|
||||||
input_sockets={'Monitor Data'},
|
input_sockets={'Monitor Data'},
|
||||||
input_socket_kinds={'Monitor Data': ct.FlowKind.Value},
|
input_socket_kinds={'Monitor Data': ct.FlowKind.Value},
|
||||||
)
|
)
|
||||||
def compute_data(
|
def compute_data(self, props: dict, input_sockets: dict) -> jax.Array | None:
|
||||||
self, props: dict, input_sockets: dict
|
if (
|
||||||
) -> jax.Array | ct.FlowSignal:
|
input_sockets['Monitor Data'] is not None
|
||||||
"""Compute `Data:Array` by querying an array-like attribute of `Monitor Data`, then constructing an `ct.ArrayFlow`.
|
and props['extract_filter'] != 'NONE'
|
||||||
|
):
|
||||||
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(
|
xarray_data = getattr(
|
||||||
input_sockets['Monitor Data'], props['extract_filter']
|
input_sockets['Monitor Data'], props['extract_filter']
|
||||||
)
|
)
|
||||||
return ct.ArrayFlow(values=jnp.array(xarray_data.data), unit=None)
|
return jnp.array(xarray_data.data)
|
||||||
## TODO: Try np.array instead, as it removes a copy, while still (I believe) being JIT-compatible.
|
## TODO: Let the array itself have its output unit too!
|
||||||
|
|
||||||
return ct.FlowSignal.FlowPending
|
return None
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
# Trigger
|
|
||||||
'Data',
|
'Data',
|
||||||
kind=ct.FlowKind.LazyValueFunc,
|
kind=ct.FlowKind.LazyValueFunc,
|
||||||
# Loaded
|
|
||||||
output_sockets={'Data'},
|
output_sockets={'Data'},
|
||||||
output_socket_kinds={'Data': ct.FlowKind.Array},
|
output_socket_kinds={'Data': ct.FlowKind.Array},
|
||||||
)
|
)
|
||||||
def compute_extracted_data_lazy(
|
def compute_extracted_data_lazy(
|
||||||
self, output_sockets: dict
|
self, output_sockets: dict
|
||||||
) -> ct.LazyValueFuncFlow | None:
|
) -> ct.LazyValueFuncFlow | None:
|
||||||
"""Declare `Data:LazyValueFunc` by creating a simple function that directly wraps `Data:Array`.
|
if output_sockets['Data'] is not None:
|
||||||
|
|
||||||
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(
|
return ct.LazyValueFuncFlow(
|
||||||
func=lambda: output_sockets['Data'].values, supports_jax=True
|
func=lambda: output_sockets['Data'], supports_jax=True
|
||||||
)
|
)
|
||||||
|
|
||||||
return ct.FlowSignal.FlowPending
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Auxiliary: Monitor Data -> Data
|
# - Auxiliary: Monitor Data -> Data
|
||||||
####################
|
####################
|
||||||
@events.computes_output_socket(
|
@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',
|
'Data',
|
||||||
kind=ct.FlowKind.Info,
|
kind=ct.FlowKind.Info,
|
||||||
# Loaded
|
|
||||||
props={'monitor_data_type', 'extract_filter'},
|
props={'monitor_data_type', 'extract_filter'},
|
||||||
input_sockets={'Monitor Data'},
|
input_sockets={'Monitor Data'},
|
||||||
input_socket_kinds={'Monitor Data': ct.FlowKind.Value},
|
input_socket_kinds={'Monitor Data': ct.FlowKind.Value},
|
||||||
|
@ -388,18 +247,14 @@ class ExtractDataNode(base.MaxwellSimNode):
|
||||||
def compute_extracted_data_info(
|
def compute_extracted_data_info(
|
||||||
self, props: dict, input_sockets: dict
|
self, props: dict, input_sockets: dict
|
||||||
) -> ct.InfoFlow:
|
) -> 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
|
# Retrieve XArray
|
||||||
if has_monitor_data and props['extract_filter'] != 'NONE':
|
if (
|
||||||
|
input_sockets['Monitor Data'] is not None
|
||||||
|
and props['extract_filter'] != 'NONE'
|
||||||
|
):
|
||||||
xarr = getattr(input_sockets['Monitor Data'], props['extract_filter'])
|
xarr = getattr(input_sockets['Monitor Data'], props['extract_filter'])
|
||||||
else:
|
else:
|
||||||
return ct.FlowSignal.FlowPending
|
return ct.InfoFlow()
|
||||||
|
|
||||||
info_output_names = {
|
info_output_names = {
|
||||||
'output_names': [props['extract_filter']],
|
'output_names': [props['extract_filter']],
|
||||||
|
|
|
@ -48,12 +48,8 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data_info(self) -> ct.InfoFlow | None:
|
def _info(self) -> ct.InfoFlow:
|
||||||
info = self._compute_input('Data', kind=ct.FlowKind.Info)
|
return self._compute_input('Data', kind=ct.FlowKind.Info)
|
||||||
if not ct.FlowSignal.check(info):
|
|
||||||
return info
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Operation Search
|
# - Operation Search
|
||||||
|
@ -75,18 +71,16 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
# - Dim Search
|
# - Dim Search
|
||||||
####################
|
####################
|
||||||
def search_dims(self) -> list[ct.BLEnumElement]:
|
def search_dims(self) -> list[ct.BLEnumElement]:
|
||||||
if self.data_info is not None:
|
if (info := self._info).dim_names:
|
||||||
dims = [
|
dims = [
|
||||||
(dim_name, dim_name, dim_name, '', i)
|
(dim_name, dim_name, dim_name, '', i)
|
||||||
for i, dim_name in enumerate(self.data_info.dim_names)
|
for i, dim_name in enumerate(info.dim_names)
|
||||||
]
|
]
|
||||||
|
|
||||||
# Squeeze: Dimension Must Have Length=1
|
# Squeeze: Dimension Must Have Length=1
|
||||||
## We must also correct the "NUMBER" of the enum.
|
## We must also correct the "NUMBER" of the enum.
|
||||||
if self.operation == 'SQUEEZE':
|
if self.operation == 'SQUEEZE':
|
||||||
filtered_dims = [
|
filtered_dims = [dim for dim in dims if info.dim_lens[dim[0]] == 1]
|
||||||
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 [(*dim[:-1], i) for i, dim in enumerate(filtered_dims)]
|
||||||
|
|
||||||
return dims
|
return dims
|
||||||
|
@ -97,43 +91,42 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
####################
|
####################
|
||||||
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
def draw_props(self, _: bpy.types.Context, layout: bpy.types.UILayout) -> None:
|
||||||
layout.prop(self, self.blfields['operation'], text='')
|
layout.prop(self, self.blfields['operation'], text='')
|
||||||
if self.data_info is not None and self.data_info.dim_names:
|
if self._info.dim_names:
|
||||||
layout.prop(self, self.blfields['dim'], text='')
|
layout.prop(self, self.blfields['dim'], text='')
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Events
|
# - Events
|
||||||
####################
|
####################
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name='Data',
|
|
||||||
prop_name='active_socket_set',
|
prop_name='active_socket_set',
|
||||||
run_on_init=True,
|
run_on_init=True,
|
||||||
input_sockets={'Data'},
|
|
||||||
)
|
)
|
||||||
def on_any_change(self, input_sockets: dict):
|
def on_socket_set_changed(self):
|
||||||
if all(
|
self.operation = bl_cache.Signal.ResetEnumItems
|
||||||
not ct.FlowSignal.check_single(
|
|
||||||
input_socket_value, ct.FlowSignal.FlowPending
|
@events.on_value_changed(
|
||||||
)
|
socket_name='Data',
|
||||||
for input_socket_value in input_sockets.values()
|
prop_name='active_socket_set',
|
||||||
):
|
props={'active_socket_set'},
|
||||||
self.operation = bl_cache.Signal.ResetEnumItems
|
input_sockets={'Data'},
|
||||||
self.dim = bl_cache.Signal.ResetEnumItems
|
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
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name='Data',
|
socket_name='Data',
|
||||||
prop_name='dim',
|
prop_name='dim',
|
||||||
## run_on_init: Implicitly triggered.
|
|
||||||
props={'active_socket_set', 'dim'},
|
props={'active_socket_set', 'dim'},
|
||||||
input_sockets={'Data'},
|
input_sockets={'Data'},
|
||||||
input_socket_kinds={'Data': ct.FlowKind.Info},
|
input_socket_kinds={'Data': ct.FlowKind.Info},
|
||||||
|
# run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_dim_change(self, props: dict, input_sockets: dict):
|
def on_dim_change(self, props: dict, input_sockets: dict):
|
||||||
if input_sockets['Data'] == ct.FlowSignal.FlowPending:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Add/Remove Input Socket "Value"
|
# Add/Remove Input Socket "Value"
|
||||||
if (
|
if (
|
||||||
not ct.Flowsignal.check(input_sockets['Data'])
|
input_sockets['Data'] != ct.InfoFlow()
|
||||||
and props['active_socket_set'] == 'By Dim Value'
|
and props['active_socket_set'] == 'By Dim Value'
|
||||||
and props['dim'] != 'NONE'
|
and props['dim'] != 'NONE'
|
||||||
):
|
):
|
||||||
|
@ -151,7 +144,7 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
):
|
):
|
||||||
self.loose_input_sockets = {
|
self.loose_input_sockets = {
|
||||||
'Value': wanted_socket_def(),
|
'Value': wanted_socket_def(),
|
||||||
} ## TODO: Can we do the boilerplate in base.py?
|
}
|
||||||
elif self.loose_input_sockets:
|
elif self.loose_input_sockets:
|
||||||
self.loose_input_sockets = {}
|
self.loose_input_sockets = {}
|
||||||
|
|
||||||
|
@ -170,16 +163,13 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
lazy_value_func = input_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
lazy_value_func = input_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
||||||
info = input_sockets['Data'][ct.FlowKind.Info]
|
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
|
# Compute Bound/Free Parameters
|
||||||
func_args = [int] if props['active_socket_set'] == 'By Dim Value' else []
|
func_args = [int] if props['active_socket_set'] == 'By Dim Value' else []
|
||||||
axis = info.dim_names.index(props['dim'])
|
if props['dim'] != 'NONE':
|
||||||
|
axis = info.dim_names.index(props['dim'])
|
||||||
|
else:
|
||||||
|
msg = 'Dimension cannot be empty'
|
||||||
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Select Function
|
# Select Function
|
||||||
filter_func: typ.Callable[[jax.Array], jax.Array] = {
|
filter_func: typ.Callable[[jax.Array], jax.Array] = {
|
||||||
|
@ -211,10 +201,6 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
||||||
params = output_sockets['Data'][ct.FlowKind.Params]
|
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
|
# Compute Array
|
||||||
return ct.ArrayFlow(
|
return ct.ArrayFlow(
|
||||||
values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs),
|
values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs),
|
||||||
|
@ -235,14 +221,15 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
# Retrieve Inputs
|
# Retrieve Inputs
|
||||||
info = input_sockets['Data']
|
info = input_sockets['Data']
|
||||||
|
|
||||||
# Check Flow
|
# Compute Bound/Free Parameters
|
||||||
if ct.FlowSignal.check(info) or props['dim'] == 'NONE':
|
## Empty Dimension -> Empty InfoFlow
|
||||||
return ct.FlowSignal.FlowPending
|
if input_sockets['Data'] != ct.InfoFlow() and props['dim'] != 'NONE':
|
||||||
|
axis = info.dim_names.index(props['dim'])
|
||||||
|
else:
|
||||||
|
return ct.InfoFlow()
|
||||||
|
|
||||||
# Compute Information
|
# Compute Information
|
||||||
## Compute Info w/By-Operation Change to Dimensions
|
## Compute Info w/By-Operation Change to Dimensions
|
||||||
axis = info.dim_names.index(props['dim'])
|
|
||||||
|
|
||||||
if (props['active_socket_set'], props['operation']) in [
|
if (props['active_socket_set'], props['operation']) in [
|
||||||
('By Dim', 'SQUEEZE'),
|
('By Dim', 'SQUEEZE'),
|
||||||
('By Dim Value', 'FIX'),
|
('By Dim Value', 'FIX'),
|
||||||
|
@ -259,8 +246,8 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
output_units=info.output_units,
|
output_units=info.output_units,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = f'Active socket set {props["active_socket_set"]} and operation {props["operation"]} don\'t have an InfoFlow defined'
|
# Fallback to Empty InfoFlow
|
||||||
raise RuntimeError(msg)
|
return ct.InfoFlow()
|
||||||
|
|
||||||
@events.computes_output_socket(
|
@events.computes_output_socket(
|
||||||
'Data',
|
'Data',
|
||||||
|
@ -277,9 +264,6 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
info = input_sockets['Data'][ct.FlowKind.Info]
|
info = input_sockets['Data'][ct.FlowKind.Info]
|
||||||
params = input_sockets['Data'][ct.FlowKind.Params]
|
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
|
# Compute Composed Parameters
|
||||||
## -> Only operations that add parameters.
|
## -> Only operations that add parameters.
|
||||||
## -> A dimension must be selected.
|
## -> A dimension must be selected.
|
||||||
|
@ -290,7 +274,7 @@ class FilterMathNode(base.MaxwellSimNode):
|
||||||
('By Dim Value', 'FIX'),
|
('By Dim Value', 'FIX'),
|
||||||
]
|
]
|
||||||
and props['dim'] != 'NONE'
|
and props['dim'] != 'NONE'
|
||||||
and not ct.FlowSignal.check(input_sockets['Value'])
|
and input_sockets['Value'] is not None
|
||||||
):
|
):
|
||||||
# Compute IDX Corresponding to Coordinate Value
|
# Compute IDX Corresponding to Coordinate Value
|
||||||
## -> Each dimension declares a unit-aware real number at each index.
|
## -> Each dimension declares a unit-aware real number at each index.
|
||||||
|
|
|
@ -54,8 +54,9 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
)
|
)
|
||||||
|
|
||||||
def search_operations(self) -> list[ct.BLEnumElement]:
|
def search_operations(self) -> list[ct.BLEnumElement]:
|
||||||
|
items = []
|
||||||
if self.active_socket_set == 'By Element':
|
if self.active_socket_set == 'By Element':
|
||||||
items = [
|
items += [
|
||||||
# General
|
# General
|
||||||
('REAL', 'ℝ(v)', 'real(v) (by el)'),
|
('REAL', 'ℝ(v)', 'real(v) (by el)'),
|
||||||
('IMAG', 'Im(v)', 'imag(v) (by el)'),
|
('IMAG', 'Im(v)', 'imag(v) (by el)'),
|
||||||
|
@ -72,11 +73,11 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
('ATAN', 'atan v', 'atan(v) (by el)'),
|
('ATAN', 'atan v', 'atan(v) (by el)'),
|
||||||
]
|
]
|
||||||
elif self.active_socket_set in 'By Vector':
|
elif self.active_socket_set in 'By Vector':
|
||||||
items = [
|
items += [
|
||||||
('NORM_2', '||v||₂', 'norm(v, 2) (by Vec)'),
|
('NORM_2', '||v||₂', 'norm(v, 2) (by Vec)'),
|
||||||
]
|
]
|
||||||
elif self.active_socket_set == 'By Matrix':
|
elif self.active_socket_set == 'By Matrix':
|
||||||
items = [
|
items += [
|
||||||
# Matrix -> Number
|
# Matrix -> Number
|
||||||
('DET', 'det V', 'det(V) (by Mat)'),
|
('DET', 'det V', 'det(V) (by Mat)'),
|
||||||
('COND', 'κ(V)', 'cond(V) (by Mat)'),
|
('COND', 'κ(V)', 'cond(V) (by Mat)'),
|
||||||
|
@ -95,10 +96,7 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
('SVD', 'svd V', 'svd(V) -> U·Σ·V† (by Mat)'),
|
('SVD', 'svd V', 'svd(V) -> U·Σ·V† (by Mat)'),
|
||||||
]
|
]
|
||||||
elif self.active_socket_set == 'Expr':
|
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)]
|
return [(*item, '', i) for i, item in enumerate(items)]
|
||||||
|
|
||||||
|
@ -129,14 +127,6 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
input_sockets_optional={'Mapper': True},
|
input_sockets_optional={'Mapper': True},
|
||||||
)
|
)
|
||||||
def compute_data(self, props: dict, input_sockets: dict):
|
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] = {
|
mapping_func: typ.Callable[[jax.Array], jax.Array] = {
|
||||||
'By Element': {
|
'By Element': {
|
||||||
'REAL': lambda data: jnp.real(data),
|
'REAL': lambda data: jnp.real(data),
|
||||||
|
@ -196,16 +186,10 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
def compute_array(self, output_sockets: dict) -> ct.ArrayFlow:
|
def compute_array(self, output_sockets: dict) -> ct.ArrayFlow:
|
||||||
lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
lazy_value_func = output_sockets['Data'][ct.FlowKind.LazyValueFunc]
|
||||||
params = output_sockets['Data'][ct.FlowKind.Params]
|
params = output_sockets['Data'][ct.FlowKind.Params]
|
||||||
|
return ct.ArrayFlow(
|
||||||
if all(not ct.FlowSignal.check(inp) for inp in [lazy_value_func, params]):
|
values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs),
|
||||||
return ct.ArrayFlow(
|
unit=None, ## TODO: Unit Propagation
|
||||||
values=lazy_value_func.func_jax(
|
)
|
||||||
*params.func_args, **params.func_kwargs
|
|
||||||
),
|
|
||||||
unit=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ct.FlowSignal.FlowPending
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Compute Auxiliary: Info / Params
|
# - Compute Auxiliary: Info / Params
|
||||||
|
@ -221,16 +205,11 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
info = input_sockets['Data']
|
info = input_sockets['Data']
|
||||||
|
|
||||||
# Complex -> Real
|
# Complex -> Real
|
||||||
if (
|
if props['active_socket_set'] == 'By Element' and props['operation'] in [
|
||||||
props['active_socket_set'] == 'By Element'
|
'REAL',
|
||||||
and props['operation']
|
'IMAG',
|
||||||
in [
|
'ABS',
|
||||||
'REAL',
|
]:
|
||||||
'IMAG',
|
|
||||||
'ABS',
|
|
||||||
]
|
|
||||||
and not ct.FlowSignal.check(info)
|
|
||||||
):
|
|
||||||
return ct.InfoFlow(
|
return ct.InfoFlow(
|
||||||
dim_names=info.dim_names,
|
dim_names=info.dim_names,
|
||||||
dim_idx=info.dim_idx,
|
dim_idx=info.dim_idx,
|
||||||
|
@ -253,7 +232,7 @@ class MapMathNode(base.MaxwellSimNode):
|
||||||
input_sockets={'Data'},
|
input_sockets={'Data'},
|
||||||
input_socket_kinds={'Data': ct.FlowKind.Params},
|
input_socket_kinds={'Data': ct.FlowKind.Params},
|
||||||
)
|
)
|
||||||
def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow | ct.FlowSignal:
|
def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow:
|
||||||
return input_sockets['Data']
|
return input_sockets['Data']
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -218,42 +218,36 @@ class VizNode(base.MaxwellSimNode):
|
||||||
## - Mode Searcher
|
## - Mode Searcher
|
||||||
#####################
|
#####################
|
||||||
@property
|
@property
|
||||||
def data_info(self) -> ct.InfoFlow:
|
def _info(self) -> ct.InfoFlow:
|
||||||
return self._compute_input('Data', kind=ct.FlowKind.Info)
|
return self._compute_input('Data', kind=ct.FlowKind.Info)
|
||||||
|
|
||||||
def search_modes(self) -> list[ct.BLEnumElement]:
|
def search_modes(self) -> list[ct.BLEnumElement]:
|
||||||
if not ct.FlowSignal.check(self.data_info):
|
info = self._info
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
viz_mode,
|
viz_mode,
|
||||||
VizMode.to_name(viz_mode),
|
VizMode.to_name(viz_mode),
|
||||||
VizMode.to_name(viz_mode),
|
VizMode.to_name(viz_mode),
|
||||||
VizMode.to_icon(viz_mode),
|
VizMode.to_icon(viz_mode),
|
||||||
i,
|
i,
|
||||||
)
|
)
|
||||||
for i, viz_mode in enumerate(VizMode.valid_modes_for(self.data_info))
|
for i, viz_mode in enumerate(VizMode.valid_modes_for(info))
|
||||||
]
|
]
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
## - Target Searcher
|
## - Target Searcher
|
||||||
#####################
|
#####################
|
||||||
def search_targets(self) -> list[ct.BLEnumElement]:
|
def search_targets(self) -> list[ct.BLEnumElement]:
|
||||||
if self.viz_mode != 'NONE':
|
return [
|
||||||
return [
|
(
|
||||||
(
|
viz_target,
|
||||||
viz_target,
|
VizTarget.to_name(viz_target),
|
||||||
VizTarget.to_name(viz_target),
|
VizTarget.to_name(viz_target),
|
||||||
VizTarget.to_name(viz_target),
|
VizTarget.to_icon(viz_target),
|
||||||
VizTarget.to_icon(viz_target),
|
i,
|
||||||
i,
|
)
|
||||||
)
|
for i, viz_target in enumerate(VizTarget.valid_targets_for(self.viz_mode))
|
||||||
for i, viz_target in enumerate(
|
]
|
||||||
VizTarget.valid_targets_for(self.viz_mode)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
return []
|
|
||||||
|
|
||||||
#####################
|
#####################
|
||||||
## - UI
|
## - UI
|
||||||
|
@ -270,20 +264,17 @@ class VizNode(base.MaxwellSimNode):
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
socket_name='Data',
|
socket_name='Data',
|
||||||
input_sockets={'Data'},
|
input_sockets={'Data'},
|
||||||
run_on_init=True,
|
|
||||||
input_socket_kinds={'Data': ct.FlowKind.Info},
|
input_socket_kinds={'Data': ct.FlowKind.Info},
|
||||||
input_sockets_optional={'Data': True},
|
input_sockets_optional={'Data': True},
|
||||||
|
run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_any_changed(self, input_sockets: dict):
|
def on_socket_set_changed(self, input_sockets: dict):
|
||||||
if not ct.FlowSignal.check_single(
|
self.viz_mode = bl_cache.Signal.ResetEnumItems
|
||||||
input_sockets['Data'], ct.FlowSignal.FlowPending
|
self.viz_target = bl_cache.Signal.ResetEnumItems
|
||||||
):
|
|
||||||
self.viz_mode = bl_cache.Signal.ResetEnumItems
|
|
||||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
|
||||||
|
|
||||||
@events.on_value_changed(
|
@events.on_value_changed(
|
||||||
prop_name='viz_mode',
|
prop_name='viz_mode',
|
||||||
## run_on_init: Implicitly triggered.
|
# run_on_init=True,
|
||||||
)
|
)
|
||||||
def on_viz_mode_changed(self):
|
def on_viz_mode_changed(self):
|
||||||
self.viz_target = bl_cache.Signal.ResetEnumItems
|
self.viz_target = bl_cache.Signal.ResetEnumItems
|
||||||
|
@ -296,6 +287,7 @@ class VizNode(base.MaxwellSimNode):
|
||||||
props={'viz_mode', 'viz_target', 'colormap'},
|
props={'viz_mode', 'viz_target', 'colormap'},
|
||||||
input_sockets={'Data'},
|
input_sockets={'Data'},
|
||||||
input_socket_kinds={'Data': {ct.FlowKind.Array, ct.FlowKind.Info}},
|
input_socket_kinds={'Data': {ct.FlowKind.Array, ct.FlowKind.Info}},
|
||||||
|
input_sockets_optional={'Data': True},
|
||||||
stop_propagation=True,
|
stop_propagation=True,
|
||||||
)
|
)
|
||||||
def on_show_plot(
|
def on_show_plot(
|
||||||
|
@ -304,19 +296,12 @@ class VizNode(base.MaxwellSimNode):
|
||||||
input_sockets: dict,
|
input_sockets: dict,
|
||||||
props: dict,
|
props: dict,
|
||||||
):
|
):
|
||||||
# Retrieve Inputs
|
|
||||||
array_flow = input_sockets['Data'][ct.FlowKind.Array]
|
array_flow = input_sockets['Data'][ct.FlowKind.Array]
|
||||||
info = input_sockets['Data'][ct.FlowKind.Info]
|
info = input_sockets['Data'][ct.FlowKind.Info]
|
||||||
|
|
||||||
# Check Flow
|
if input_sockets['Data'] is None:
|
||||||
if (
|
|
||||||
any(ct.FlowSignal.check(inp) for inp in [array_flow, info])
|
|
||||||
or props['viz_mode'] == 'NONE'
|
|
||||||
or props['viz_target'] == 'NONE'
|
|
||||||
):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Viz Target
|
|
||||||
if props['viz_target'] == VizTarget.Plot2D:
|
if props['viz_target'] == VizTarget.Plot2D:
|
||||||
managed_objs['plot'].mpl_plot_to_image(
|
managed_objs['plot'].mpl_plot_to_image(
|
||||||
lambda ax: VizMode.to_plotter(props['viz_mode'])(
|
lambda ax: VizMode.to_plotter(props['viz_mode'])(
|
||||||
|
|
|
@ -430,11 +430,6 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
# Remove Sockets
|
# Remove Sockets
|
||||||
for bl_socket in bl_sockets_to_remove:
|
for bl_socket in bl_sockets_to_remove:
|
||||||
node_tree.on_node_socket_removed(bl_socket)
|
node_tree.on_node_socket_removed(bl_socket)
|
||||||
self._compute_input.invalidate(
|
|
||||||
input_socket_name=bl_socket.name,
|
|
||||||
kind=...,
|
|
||||||
unit_system=...,
|
|
||||||
)
|
|
||||||
all_bl_sockets.remove(bl_socket)
|
all_bl_sockets.remove(bl_socket)
|
||||||
|
|
||||||
def _add_new_active_sockets(self):
|
def _add_new_active_sockets(self):
|
||||||
|
@ -602,7 +597,7 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
)
|
)
|
||||||
|
|
||||||
if optional:
|
if optional:
|
||||||
return ct.FlowSignal.NoFlow
|
return None
|
||||||
|
|
||||||
msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket'
|
msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
@ -650,8 +645,14 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
return output_socket_methods[0](self)
|
return output_socket_methods[0](self)
|
||||||
|
|
||||||
# Auxiliary Fallbacks
|
# Auxiliary Fallbacks
|
||||||
if optional or kind in [ct.FlowKind.Info, ct.FlowKind.Params]:
|
if kind == ct.FlowKind.Info:
|
||||||
return ct.FlowSignal.NoFlow
|
return ct.InfoFlow()
|
||||||
|
|
||||||
|
if kind == ct.FlowKind.Params:
|
||||||
|
return ct.ParamsFlow()
|
||||||
|
|
||||||
|
if optional:
|
||||||
|
return None
|
||||||
|
|
||||||
msg = f'No output method for ({output_socket_name}, {kind})'
|
msg = f'No output method for ({output_socket_name}, {kind})'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
@ -837,11 +838,6 @@ class MaxwellSimNode(bpy.types.Node):
|
||||||
)
|
)
|
||||||
for event_method in triggered_event_methods:
|
for event_method in triggered_event_methods:
|
||||||
stop_propagation |= event_method.stop_propagation
|
stop_propagation |= event_method.stop_propagation
|
||||||
# log.critical(
|
|
||||||
# '$[%s] [%s %s %s %s] Running: (%s)',
|
|
||||||
# self.sim_node_name,
|
|
||||||
# event_method.callback_info,
|
|
||||||
# )
|
|
||||||
event_method(self)
|
event_method(self)
|
||||||
|
|
||||||
# Propagate Event to All Sockets in "Trigger Direction"
|
# Propagate Event to All Sockets in "Trigger Direction"
|
||||||
|
|
|
@ -270,8 +270,6 @@ def event_decorator(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call Method
|
# Call Method
|
||||||
## If there is a FlowPending, then the method would fail.
|
|
||||||
## Therefore, propagate FlowPending if found.
|
|
||||||
return method(
|
return method(
|
||||||
node,
|
node,
|
||||||
**method_kw_args,
|
**method_kw_args,
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import typing as typ
|
import typing as typ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import bpy
|
from blender_maxwell.utils import logger
|
||||||
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 contracts as ct
|
||||||
from .... import sockets
|
from .... import sockets
|
||||||
from ... import base, events
|
from ... import base, events
|
||||||
|
@ -14,40 +11,6 @@ from ... import base, events
|
||||||
log = logger.get(__name__)
|
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:
|
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.
|
"""Compute an appropriate location for caching simulations downloaded from the internet, unique to each task ID.
|
||||||
|
|
||||||
|
@ -71,89 +34,68 @@ class Tidy3DWebImporterNode(base.MaxwellSimNode):
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
sim_data_loaded: bool = bl_cache.BLField(False)
|
####################
|
||||||
|
# - 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
|
||||||
|
|
||||||
@bl_cache.cached_bl_property()
|
for module_name, module in sys.modules.copy().items():
|
||||||
def sim_data(self) -> td.SimulationData | None:
|
if module_name == '__mp_main__':
|
||||||
cloud_task = self._compute_input(
|
print('Problematic Module Entry', module_name)
|
||||||
'Cloud Task', kind=ct.FlowKind.Value, optional=True
|
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)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@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 (
|
if (
|
||||||
# Check Flow
|
(cloud_task := input_sockets['Cloud Task']) is not None
|
||||||
not ct.FlowSignal.check(cloud_task)
|
|
||||||
# Check Task
|
|
||||||
and cloud_task is not None
|
|
||||||
and isinstance(cloud_task, tdcloud.CloudTask)
|
and isinstance(cloud_task, tdcloud.CloudTask)
|
||||||
and cloud_task.status == 'success'
|
and cloud_task.status == 'success'
|
||||||
):
|
):
|
||||||
sim_data = tdcloud.TidyCloudTasks.download_task_sim_data(
|
self.loose_output_sockets = {
|
||||||
cloud_task, _sim_data_cache_path(cloud_task.task_id)
|
'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(),
|
||||||
)
|
}
|
||||||
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:
|
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 = {}
|
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
|
# - Blender Registration
|
||||||
####################
|
####################
|
||||||
BL_REGISTER = [
|
BL_REGISTER = [
|
||||||
LoadCloudSim,
|
|
||||||
Tidy3DWebImporterNode,
|
Tidy3DWebImporterNode,
|
||||||
]
|
]
|
||||||
BL_NODES = {
|
BL_NODES = {
|
||||||
|
|
|
@ -209,7 +209,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
else:
|
else:
|
||||||
self.cache_est_cost = -1.0
|
self.cache_est_cost = -1.0
|
||||||
self.loose_output_sockets = {}
|
self.loose_output_sockets = {}
|
||||||
self.inputs['Cloud Task'].on_prepare_new_task()
|
self.inputs['Cloud Task'].sync_prepare_new_task()
|
||||||
self.inputs['Cloud Task'].locked = False
|
self.inputs['Cloud Task'].locked = False
|
||||||
|
|
||||||
self.on_prop_changed('tracked_task_id', context)
|
self.on_prop_changed('tracked_task_id', context)
|
||||||
|
@ -249,7 +249,7 @@ class Tidy3DWebExporterNode(base.MaxwellSimNode):
|
||||||
# Declare to Cloud Task that it Exists Now
|
# Declare to Cloud Task that it Exists Now
|
||||||
## This will change the UI to not allow free-text input.
|
## This will change the UI to not allow free-text input.
|
||||||
## If the socket is linked, this errors.
|
## If the socket is linked, this errors.
|
||||||
self.inputs['Cloud Task'].on_new_task_created(cloud_task)
|
self.inputs['Cloud Task'].sync_created_new_task(cloud_task)
|
||||||
|
|
||||||
# Track the Newly Uploaded Task ID
|
# Track the Newly Uploaded Task ID
|
||||||
self.tracked_task_id = cloud_task.task_id
|
self.tracked_task_id = cloud_task.task_id
|
||||||
|
|
|
@ -548,7 +548,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Returns:
|
Returns:
|
||||||
An empty `ct.InfoFlow`.
|
An empty `ct.InfoFlow`.
|
||||||
"""
|
"""
|
||||||
return ct.FlowSignal.NoFlow
|
return ct.InfoFlow()
|
||||||
|
|
||||||
# Param
|
# Param
|
||||||
@property
|
@property
|
||||||
|
@ -561,7 +561,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Returns:
|
Returns:
|
||||||
An empty `ct.ParamsFlow`.
|
An empty `ct.ParamsFlow`.
|
||||||
"""
|
"""
|
||||||
return ct.FlowSignal.NoFlow
|
return ct.ParamsFlow()
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - FlowKind: Auxiliary
|
# - FlowKind: Auxiliary
|
||||||
|
@ -577,7 +577,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: When used without being overridden.
|
NotImplementedError: When used without being overridden.
|
||||||
"""
|
"""
|
||||||
return ct.FlowSignal.NoFlow
|
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.Value", but socket does not define it'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self, value: ct.ValueFlow) -> None:
|
def value(self, value: ct.ValueFlow) -> None:
|
||||||
|
@ -603,7 +604,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: When used without being overridden.
|
NotImplementedError: When used without being overridden.
|
||||||
"""
|
"""
|
||||||
return ct.FlowSignal.NoFlow
|
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.Array", but socket does not define it'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
@array.setter
|
@array.setter
|
||||||
def array(self, value: ct.ArrayFlow) -> None:
|
def array(self, value: ct.ArrayFlow) -> None:
|
||||||
|
@ -629,7 +631,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: When used without being overridden.
|
NotImplementedError: When used without being overridden.
|
||||||
"""
|
"""
|
||||||
return ct.FlowSignal.NoFlow
|
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.LazyValueFunc", but socket does not define it'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
@lazy_value_func.setter
|
@lazy_value_func.setter
|
||||||
def lazy_value_func(self, lazy_value_func: ct.LazyValueFuncFlow) -> None:
|
def lazy_value_func(self, lazy_value_func: ct.LazyValueFuncFlow) -> None:
|
||||||
|
@ -655,7 +658,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
Raises:
|
Raises:
|
||||||
NotImplementedError: When used without being overridden.
|
NotImplementedError: When used without being overridden.
|
||||||
"""
|
"""
|
||||||
return ct.FlowSignal.NoFlow
|
msg = f'Socket {self.bl_label} {self.socket_type}): Tried to get "ct.FlowKind.LazyArrayRange", but socket does not define it'
|
||||||
|
raise NotImplementedError(msg)
|
||||||
|
|
||||||
@lazy_array_range.setter
|
@lazy_array_range.setter
|
||||||
def lazy_array_range(self, value: ct.LazyArrayRangeFlow) -> None:
|
def lazy_array_range(self, value: ct.LazyArrayRangeFlow) -> None:
|
||||||
|
@ -888,8 +892,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
# Info Drawing
|
# Info Drawing
|
||||||
if self.use_info_draw:
|
if self.use_info_draw:
|
||||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||||
if not ct.FlowSignal.check(info):
|
self.draw_info(info, col)
|
||||||
self.draw_info(info, col)
|
|
||||||
|
|
||||||
def draw_output(
|
def draw_output(
|
||||||
self,
|
self,
|
||||||
|
@ -917,8 +920,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
|
||||||
# Draw FlowKind.Info related Information
|
# Draw FlowKind.Info related Information
|
||||||
if self.use_info_draw:
|
if self.use_info_draw:
|
||||||
info = self.compute_data(kind=ct.FlowKind.Info)
|
info = self.compute_data(kind=ct.FlowKind.Info)
|
||||||
if not ct.FlowSignal.check(info):
|
self.draw_info(info, col)
|
||||||
self.draw_info(info, col)
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI Methods: Active FlowKind
|
# - UI Methods: Active FlowKind
|
||||||
|
|
|
@ -34,6 +34,10 @@ class DataBLSocket(base.MaxwellSimSocket):
|
||||||
must_match={'format': self.format},
|
must_match={'format': self.format},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return None
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - UI
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -7,6 +7,10 @@ class MaxwellFDTDSimBLSocket(base.MaxwellSimSocket):
|
||||||
socket_type = ct.SocketType.MaxwellFDTDSim
|
socket_type = ct.SocketType.MaxwellFDTDSim
|
||||||
bl_label = 'Maxwell FDTD Simulation'
|
bl_label = 'Maxwell FDTD Simulation'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Socket Configuration
|
# - Socket Configuration
|
||||||
|
|
|
@ -6,6 +6,10 @@ class MaxwellFDTDSimDataBLSocket(base.MaxwellSimSocket):
|
||||||
socket_type = ct.SocketType.MaxwellFDTDSimData
|
socket_type = ct.SocketType.MaxwellFDTDSimData
|
||||||
bl_label = 'Maxwell FDTD Simulation'
|
bl_label = 'Maxwell FDTD Simulation'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Socket Configuration
|
# - Socket Configuration
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import enum
|
|
||||||
|
|
||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
from blender_maxwell.services import tdcloud
|
from .....services import tdcloud
|
||||||
from blender_maxwell.utils import bl_cache
|
|
||||||
|
|
||||||
from ... import contracts as ct
|
from ... import contracts as ct
|
||||||
from .. import base
|
from .. import base
|
||||||
|
|
||||||
|
@ -13,32 +9,30 @@ from .. import base
|
||||||
# - Operators
|
# - Operators
|
||||||
####################
|
####################
|
||||||
class ReloadFolderList(bpy.types.Operator):
|
class ReloadFolderList(bpy.types.Operator):
|
||||||
bl_idname = ct.OperatorType.SocketReloadCloudFolderList
|
bl_idname = 'blender_maxwell.sockets__reload_folder_list'
|
||||||
bl_label = 'Reload Tidy3D Folder List'
|
bl_label = 'Reload Tidy3D Folder List'
|
||||||
bl_description = 'Reload the the cached Tidy3D folder list'
|
bl_description = 'Reload the the cached Tidy3D folder list'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def poll(cls, context):
|
def poll(cls, context):
|
||||||
return (
|
return (
|
||||||
tdcloud.IS_ONLINE
|
tdcloud.IS_AUTHENTICATED
|
||||||
and tdcloud.IS_AUTHENTICATED
|
|
||||||
and hasattr(context, 'socket')
|
and hasattr(context, 'socket')
|
||||||
and hasattr(context.socket, 'socket_type')
|
and hasattr(context.socket, 'socket_type')
|
||||||
and context.socket.socket_type == ct.SocketType.Tidy3DCloudTask
|
and context.socket.socket_type == ct.SocketType.Tidy3DCloudTask
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
bl_socket = context.socket
|
socket = context.socket
|
||||||
|
|
||||||
tdcloud.TidyCloudFolders.update_folders()
|
tdcloud.TidyCloudFolders.update_folders()
|
||||||
tdcloud.TidyCloudTasks.update_tasks(bl_socket.existing_folder_id)
|
tdcloud.TidyCloudTasks.update_tasks(socket.existing_folder_id)
|
||||||
bl_socket.on_cloud_updated()
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
class Authenticate(bpy.types.Operator):
|
class Authenticate(bpy.types.Operator):
|
||||||
bl_idname = ct.OperatorType.SocketCloudAuthenticate
|
bl_idname = 'blender_maxwell.sockets__authenticate'
|
||||||
bl_label = 'Authenticate Tidy3D'
|
bl_label = 'Authenticate Tidy3D'
|
||||||
bl_description = 'Authenticate the Tidy3D Web API from a Cloud Task socket'
|
bl_description = 'Authenticate the Tidy3D Web API from a Cloud Task socket'
|
||||||
|
|
||||||
|
@ -57,7 +51,6 @@ 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()
|
|
||||||
|
|
||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
@ -66,16 +59,6 @@ class Authenticate(bpy.types.Operator):
|
||||||
# - Socket
|
# - Socket
|
||||||
####################
|
####################
|
||||||
class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
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
|
socket_type = ct.SocketType.Tidy3DCloudTask
|
||||||
bl_label = 'Tidy3D Cloud Task'
|
bl_label = 'Tidy3D Cloud Task'
|
||||||
|
|
||||||
|
@ -84,90 +67,81 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
####################
|
####################
|
||||||
# - Properties
|
# - Properties
|
||||||
####################
|
####################
|
||||||
api_key: str = bl_cache.BLField('', prop_ui=True, str_secret=True)
|
# Authentication
|
||||||
should_exist: bool = bl_cache.BLField(False)
|
api_key: bpy.props.StringProperty(
|
||||||
|
name='API Key',
|
||||||
existing_folder_id: enum.Enum = bl_cache.BLField(
|
description='API Key for the Tidy3D Cloud',
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
|
default='',
|
||||||
)
|
options={'SKIP_SAVE'},
|
||||||
existing_task_id: enum.Enum = bl_cache.BLField(
|
subtype='PASSWORD',
|
||||||
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_tasks()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
new_task_name: str = bl_cache.BLField('', prop_ui=True)
|
# 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,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
# Identifiers
|
||||||
def capabilities(self) -> ct.CapabilitiesFlow:
|
existing_folder_id: bpy.props.EnumProperty(
|
||||||
return ct.CapabilitiesFlow(
|
name='Folder of Cloud Tasks',
|
||||||
socket_type=self.socket_type,
|
description='An existing folder on the Tidy3D Cloud',
|
||||||
active_kind=self.active_kind,
|
items=lambda self, _: self.retrieve_folders(),
|
||||||
must_match={'should_exist': self.should_exist},
|
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
|
# (Potential) New Task
|
||||||
def value(
|
new_task_name: bpy.props.StringProperty(
|
||||||
self,
|
name='New Cloud Task Name',
|
||||||
) -> tuple[tdcloud.CloudTaskName, tdcloud.CloudFolder] | tdcloud.CloudTask | None:
|
description='Name of a new task to submit to the Tidy3D Cloud',
|
||||||
if tdcloud.IS_AUTHENTICATED:
|
default='',
|
||||||
# Retrieve Folder
|
update=(lambda self, context: self.on_prop_changed('new_task_name', context)),
|
||||||
cloud_folder = tdcloud.TidyCloudFolders.folders().get(
|
)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - 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(
|
||||||
self.existing_folder_id
|
self.existing_folder_id
|
||||||
)
|
)
|
||||||
if cloud_folder is None:
|
) is None:
|
||||||
msg = f"Selected folder {cloud_folder} doesn't exist (it was probably deleted elsewhere)"
|
return [('NONE', 'None', "Folder doesn't exist")]
|
||||||
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)
|
tasks = tdcloud.TidyCloudTasks.tasks(cloud_folder)
|
||||||
|
if not tasks:
|
||||||
|
return [('NONE', 'None', 'No tasks in folder')]
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
## Task ID
|
## Task ID
|
||||||
|
@ -184,9 +158,9 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
## Task Description
|
## Task Description
|
||||||
f'Task Status: {task.status}',
|
f'Task Status: {task.status}',
|
||||||
## Status Icon
|
## Status Icon
|
||||||
icon
|
_icon
|
||||||
if (
|
if (
|
||||||
icon := {
|
_icon := {
|
||||||
'draft': 'SEQUENCE_COLOR_08',
|
'draft': 'SEQUENCE_COLOR_08',
|
||||||
'initialized': 'SHADING_SOLID',
|
'initialized': 'SHADING_SOLID',
|
||||||
'queued': 'SEQUENCE_COLOR_03',
|
'queued': 'SEQUENCE_COLOR_03',
|
||||||
|
@ -211,28 +185,52 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
]
|
]
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Node-Initiated Updates
|
# - Task Sync Methods
|
||||||
####################
|
####################
|
||||||
def on_new_task_created(self, cloud_task: tdcloud.CloudTask) -> None:
|
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
|
||||||
self.existing_folder_id = cloud_task.folder_id
|
self.existing_folder_id = cloud_task.folder_id
|
||||||
self.existing_task_id = cloud_task.task_id
|
self.existing_task_id = cloud_task.task_id
|
||||||
self.should_exist = True
|
self.should_exist = True
|
||||||
|
|
||||||
def on_prepare_new_task(self):
|
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
|
||||||
self.should_exist = False
|
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
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - UI
|
# - Socket UI
|
||||||
####################
|
####################
|
||||||
def draw_label_row(self, row: bpy.types.UILayout, text: str):
|
def draw_label_row(self, row: bpy.types.UILayout, text: str):
|
||||||
row.label(text=text)
|
row.label(text=text)
|
||||||
|
|
||||||
auth_icon = 'LOCKVIEW_ON' if tdcloud.IS_AUTHENTICATED else 'LOCKVIEW_OFF'
|
auth_icon = 'LOCKVIEW_ON' if tdcloud.IS_AUTHENTICATED else 'LOCKVIEW_OFF'
|
||||||
row.label(text='', icon=auth_icon)
|
row.operator(
|
||||||
|
Authenticate.bl_idname,
|
||||||
|
text='',
|
||||||
|
icon=auth_icon,
|
||||||
|
)
|
||||||
|
|
||||||
def draw_prelock(
|
def draw_prelock(
|
||||||
self,
|
self,
|
||||||
|
@ -247,11 +245,11 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
row.label(text='Tidy3D API Key')
|
row.label(text='Tidy3D API Key')
|
||||||
|
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.prop(self, self.blfields['api_key'], text='')
|
row.prop(self, 'api_key', text='')
|
||||||
|
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.operator(
|
row.operator(
|
||||||
ct.OperatorType.SocketCloudAuthenticate,
|
Authenticate.bl_idname,
|
||||||
text='Connect',
|
text='Connect',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -262,9 +260,9 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
# Cloud Folder Selector
|
# Cloud Folder Selector
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.label(icon='FILE_FOLDER')
|
row.label(icon='FILE_FOLDER')
|
||||||
row.prop(self, self.blfields['existing_folder_id'], text='')
|
row.prop(self, 'existing_folder_id', text='')
|
||||||
row.operator(
|
row.operator(
|
||||||
ct.OperatorType.SocketReloadCloudFolderList,
|
ReloadFolderList.bl_idname,
|
||||||
text='',
|
text='',
|
||||||
icon='FILE_REFRESH',
|
icon='FILE_REFRESH',
|
||||||
)
|
)
|
||||||
|
@ -274,14 +272,47 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
|
||||||
if not self.should_exist:
|
if not self.should_exist:
|
||||||
row = col.row()
|
row = col.row()
|
||||||
row.label(icon='NETWORK_DRIVE')
|
row.label(icon='NETWORK_DRIVE')
|
||||||
row.prop(self, self.blfields['new_task_name'], text='')
|
row.prop(self, 'new_task_name', text='')
|
||||||
|
|
||||||
col.separator(factor=1.0)
|
col.separator(factor=1.0)
|
||||||
|
|
||||||
box = col.box()
|
box = col.box()
|
||||||
row = box.row()
|
row = box.row()
|
||||||
|
|
||||||
row.prop(self, self.blfields['existing_task_id'], text='')
|
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)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -268,7 +268,7 @@ class TidyCloudTasks:
|
||||||
|
|
||||||
# Get Sim Data (from file and/or download)
|
# Get Sim Data (from file and/or download)
|
||||||
if path_sim.is_file():
|
if path_sim.is_file():
|
||||||
log.info('Loading Cloud Task "%s" from "%s"', cloud_task.task_id, path_sim)
|
log.info('Loading Cloud Task "%s" from "%s"', cloud_task.cloud_id, path_sim)
|
||||||
sim_data = td.SimulationData.from_file(str(path_sim))
|
sim_data = td.SimulationData.from_file(str(path_sim))
|
||||||
else:
|
else:
|
||||||
log.info(
|
log.info(
|
||||||
|
@ -420,7 +420,7 @@ class TidyCloudTasks:
|
||||||
|
|
||||||
# Repopulate All Caches
|
# Repopulate All Caches
|
||||||
## By deleting the folder ID, all tasks within will be reloaded
|
## By deleting the folder ID, all tasks within will be reloaded
|
||||||
cls.cache_folder_tasks.pop(folder_id, None)
|
del cls.cache_folder_tasks[folder_id]
|
||||||
|
|
||||||
return dict(cls.tasks(cloud_folder).items())
|
return dict(cls.tasks(cloud_folder).items())
|
||||||
|
|
||||||
|
|
|
@ -375,7 +375,6 @@ class CachedBLProperty:
|
||||||
return
|
return
|
||||||
|
|
||||||
if value == Signal.InvalidateCache:
|
if value == Signal.InvalidateCache:
|
||||||
log.critical('![%s] Invalidating %s', str(bl_instance), str(self))
|
|
||||||
self._invalidate_cache(bl_instance)
|
self._invalidate_cache(bl_instance)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -448,7 +447,7 @@ class CachedBLProperty:
|
||||||
####################
|
####################
|
||||||
# - Property Decorators
|
# - Property Decorators
|
||||||
####################
|
####################
|
||||||
def cached_bl_property(persist: bool = False):
|
def cached_bl_property(persist: bool = ...):
|
||||||
"""Decorator creating a descriptor that caches a computed attribute of a Blender node/socket.
|
"""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.
|
Many such `bl_instance`s rely on fast access to computed, cached properties, for example to ensure that `draw()` remains effectively non-blocking.
|
||||||
|
@ -546,7 +545,6 @@ class BLField:
|
||||||
self._str_cb = str_cb
|
self._str_cb = str_cb
|
||||||
self._enum_cb = enum_cb
|
self._enum_cb = enum_cb
|
||||||
|
|
||||||
## HUGE TODO: Persist these
|
|
||||||
self._str_cb_cache = {}
|
self._str_cb_cache = {}
|
||||||
self._enum_cb_cache = {}
|
self._enum_cb_cache = {}
|
||||||
|
|
||||||
|
@ -577,7 +575,6 @@ 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`.
|
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:
|
if self._enum_cb_cache.get(_self.instance_id) is None:
|
||||||
log.critical('REGEN ENUM')
|
|
||||||
# Retrieve Dynamic Enum Items
|
# Retrieve Dynamic Enum Items
|
||||||
enum_items = self._enum_cb(_self, context)
|
enum_items = self._enum_cb(_self, context)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue