Compare commits

...

2 Commits

Author SHA1 Message Date
Sofus Albert Høgsbro Rose 785995117e
fix: Invalidate cache of removed input sockets.
This was a nasty (interesting?) one - usually, input sockets are not attempted used
after the socket no longer exists.
Various checks in ex. `events` tend to help that process along.

Unfortunately (fortunately?), `Extract` uses a `_compute_input` query with
`optional=True`, which results in a direct attempt to hit the cache
without any other checks.
Because old input socket caches were never deleted, it would
**continue to get cached data from sockets that no longer exist**.

While on the surface this could be considered a case of "the
private method (`_compute_input`) is private for a reason", or
alternatively, "don't hijack the graph flow", I'm more convinced that
the usage is actually quite clean, being read-only and generally
well-conceived. It's reasonable to presume that asking for a thing that
doesn't exist won't produce output!

Moreover, I wouldn't be surprised if several other mysterious bugs were
caused by this. Not to mention the memory leak of endless caching! (Well,
until the node is deleted). It's a good things we noticed!
2024-04-24 18:41:06 +02:00
Sofus Albert Høgsbro Rose c82862dde9
fix: Implement explicit no-flow w/FlowSignal 2024-04-24 18:36:29 +02:00
19 changed files with 775 additions and 450 deletions

34
TODO.md
View File

@ -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.
- [ ] 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. - [ ] 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__'.
- [ ] 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. - **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.
- [ ] 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. - Should also throw appropriate errors for invalid access from Python, while Blender handles access from the inside.
- This allows us - 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 ### Parallel Features
- [x] Move serialization work to a `utils`. - [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: 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).

View File

@ -15,3 +15,10 @@ 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()

View File

@ -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,15 +28,16 @@ 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
@ -93,4 +94,5 @@ __all__ = [
'LazyValueFuncFlow', 'LazyValueFuncFlow',
'ParamsFlow', 'ParamsFlow',
'ValueFlow', 'ValueFlow',
'FlowSignal',
] ]

View File

@ -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

View File

@ -5,6 +5,7 @@ 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
@ -15,9 +16,18 @@ 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):
"""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: Attributes:
extract_filter: Identifier for data to extract from the input. extract_filter: Identifier for data to extract from the input.
@ -45,31 +55,140 @@ 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 Properties # - Computed: Sim Data
#################### ####################
@property @property
def has_sim_data(self) -> bool: def sim_data(self) -> td.SimulationData | None:
return self.active_socket_set == 'Sim Data' and self.sim_data_monitor_nametype """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):
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 has_monitor_data(self) -> bool: def monitor_data(self) -> TDMonitorData | None:
return self.active_socket_set == 'Monitor Data' and self.monitor_data_type """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 # - Extraction Filter Search
#################### ####################
def search_extract_filters(self) -> list[ct.BLEnumElement]: 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`.
"""
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(
@ -77,9 +196,15 @@ class ExtractDataNode(base.MaxwellSimNode):
) )
] ]
if self.has_monitor_data: if self.monitor_data_components is not None:
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)
] ]
@ -89,156 +214,172 @@ 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:
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 # Header
row = col.row() row = col.row()
row.alignment = 'CENTER' row.alignment = 'CENTER'
if self.has_sim_data: if 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 self.has_monitor_data: elif 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 name, desc in [ for monitor_name, monitor_type in self.sim_data_monitor_nametype.items():
(name, desc) for idname, name, desc, *_ in self.search_extract_filters() grid.label(text=monitor_name)
]: 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_sim_data_changed(self, input_sockets: dict): def on_input_sockets_changed(self) -> None:
if input_sockets['Sim Data'] is not None: """Invalidate the cached properties for sim data / monitor data, and reset the extraction filter."""
# Sim Data Monitors: Set Name -> Type self.sim_data_monitor_nametype = bl_cache.Signal.InvalidateCache
self.sim_data_monitor_nametype = { self.monitor_data_type = bl_cache.Signal.InvalidateCache
monitor_name: monitor_data.type self.monitor_data_components = bl_cache.Signal.InvalidateCache
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(self, props: dict, input_sockets: dict): def compute_monitor_data(
if input_sockets['Sim Data'] is not None and props['extract_filter'] != 'NONE': 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 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 # - 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(self, props: dict, input_sockets: dict) -> jax.Array | None: def compute_data(
if ( self, props: dict, input_sockets: dict
input_sockets['Monitor Data'] is not None ) -> jax.Array | ct.FlowSignal:
and props['extract_filter'] != 'NONE' """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( xarray_data = getattr(
input_sockets['Monitor Data'], props['extract_filter'] input_sockets['Monitor Data'], props['extract_filter']
) )
return jnp.array(xarray_data.data) return ct.ArrayFlow(values=jnp.array(xarray_data.data), unit=None)
## TODO: Let the array itself have its output unit too! ## 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( @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:
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( 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 # - 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},
@ -247,14 +388,18 @@ 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 ( if has_monitor_data and props['extract_filter'] != 'NONE':
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.InfoFlow() return ct.FlowSignal.FlowPending
info_output_names = { info_output_names = {
'output_names': [props['extract_filter']], 'output_names': [props['extract_filter']],

View File

@ -48,8 +48,12 @@ class FilterMathNode(base.MaxwellSimNode):
) )
@property @property
def _info(self) -> ct.InfoFlow: def data_info(self) -> ct.InfoFlow | None:
return self._compute_input('Data', kind=ct.FlowKind.Info) info = self._compute_input('Data', kind=ct.FlowKind.Info)
if not ct.FlowSignal.check(info):
return info
return None
#################### ####################
# - Operation Search # - Operation Search
@ -71,16 +75,18 @@ class FilterMathNode(base.MaxwellSimNode):
# - Dim Search # - Dim Search
#################### ####################
def search_dims(self) -> list[ct.BLEnumElement]: def search_dims(self) -> list[ct.BLEnumElement]:
if (info := self._info).dim_names: if self.data_info is not None:
dims = [ dims = [
(dim_name, dim_name, dim_name, '', i) (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 # 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 = [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 [(*dim[:-1], i) for i, dim in enumerate(filtered_dims)]
return dims return dims
@ -91,42 +97,43 @@ 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._info.dim_names: if self.data_info is not None and self.data_info.dim_names:
layout.prop(self, self.blfields['dim'], text='') layout.prop(self, self.blfields['dim'], text='')
#################### ####################
# - Events # - 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( @events.on_value_changed(
socket_name='Data', socket_name='Data',
prop_name='active_socket_set', prop_name='active_socket_set',
props={'active_socket_set'}, run_on_init=True,
input_sockets={'Data'}, input_sockets={'Data'},
input_socket_kinds={'Data': ct.FlowKind.Info},
# run_on_init=True,
) )
def on_any_change(self, props: dict, input_sockets: dict): def on_any_change(self, input_sockets: dict):
self.dim = bl_cache.Signal.ResetEnumItems 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( @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 (
input_sockets['Data'] != ct.InfoFlow() not ct.Flowsignal.check(input_sockets['Data'])
and props['active_socket_set'] == 'By Dim Value' and props['active_socket_set'] == 'By Dim Value'
and props['dim'] != 'NONE' and props['dim'] != 'NONE'
): ):
@ -144,7 +151,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 = {}
@ -163,13 +170,16 @@ 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 []
if props['dim'] != 'NONE': axis = info.dim_names.index(props['dim'])
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] = {
@ -201,6 +211,10 @@ 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),
@ -221,15 +235,14 @@ class FilterMathNode(base.MaxwellSimNode):
# Retrieve Inputs # Retrieve Inputs
info = input_sockets['Data'] info = input_sockets['Data']
# Compute Bound/Free Parameters # Check Flow
## Empty Dimension -> Empty InfoFlow if ct.FlowSignal.check(info) or props['dim'] == 'NONE':
if input_sockets['Data'] != ct.InfoFlow() and props['dim'] != 'NONE': return ct.FlowSignal.FlowPending
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'),
@ -246,8 +259,8 @@ class FilterMathNode(base.MaxwellSimNode):
output_units=info.output_units, output_units=info.output_units,
) )
# Fallback to Empty InfoFlow msg = f'Active socket set {props["active_socket_set"]} and operation {props["operation"]} don\'t have an InfoFlow defined'
return ct.InfoFlow() raise RuntimeError(msg)
@events.computes_output_socket( @events.computes_output_socket(
'Data', 'Data',
@ -264,6 +277,9 @@ 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.
@ -274,7 +290,7 @@ class FilterMathNode(base.MaxwellSimNode):
('By Dim Value', 'FIX'), ('By Dim Value', 'FIX'),
] ]
and props['dim'] != 'NONE' 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 # 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.

View File

@ -54,9 +54,8 @@ 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)'),
@ -73,11 +72,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)'),
@ -96,7 +95,10 @@ 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)]
@ -127,6 +129,14 @@ 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),
@ -186,10 +196,16 @@ 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(
values=lazy_value_func.func_jax(*params.func_args, **params.func_kwargs), if all(not ct.FlowSignal.check(inp) for inp in [lazy_value_func, params]):
unit=None, ## TODO: Unit Propagation 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 # - Compute Auxiliary: Info / Params
@ -205,11 +221,16 @@ class MapMathNode(base.MaxwellSimNode):
info = input_sockets['Data'] info = input_sockets['Data']
# Complex -> Real # Complex -> Real
if props['active_socket_set'] == 'By Element' and props['operation'] in [ if (
'REAL', props['active_socket_set'] == 'By Element'
'IMAG', and props['operation']
'ABS', in [
]: '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,
@ -232,7 +253,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: def compute_data_params(self, input_sockets: dict) -> ct.ParamsFlow | ct.FlowSignal:
return input_sockets['Data'] return input_sockets['Data']

View File

@ -218,36 +218,42 @@ class VizNode(base.MaxwellSimNode):
## - Mode Searcher ## - Mode Searcher
##################### #####################
@property @property
def _info(self) -> ct.InfoFlow: def data_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]:
info = self._info if not ct.FlowSignal.check(self.data_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(info)) for i, viz_mode in enumerate(VizMode.valid_modes_for(self.data_info))
] ]
return []
##################### #####################
## - Target Searcher ## - Target Searcher
##################### #####################
def search_targets(self) -> list[ct.BLEnumElement]: def search_targets(self) -> list[ct.BLEnumElement]:
return [ if self.viz_mode != 'NONE':
( return [
viz_target, (
VizTarget.to_name(viz_target), viz_target,
VizTarget.to_name(viz_target), VizTarget.to_name(viz_target),
VizTarget.to_icon(viz_target), VizTarget.to_name(viz_target),
i, VizTarget.to_icon(viz_target),
) 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
@ -264,17 +270,20 @@ 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_socket_set_changed(self, input_sockets: dict): def on_any_changed(self, input_sockets: dict):
self.viz_mode = bl_cache.Signal.ResetEnumItems if not ct.FlowSignal.check_single(
self.viz_target = bl_cache.Signal.ResetEnumItems input_sockets['Data'], ct.FlowSignal.FlowPending
):
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=True, ## run_on_init: Implicitly triggered.
) )
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
@ -287,7 +296,6 @@ 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(
@ -296,12 +304,19 @@ 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]
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 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'])(

View File

@ -430,6 +430,11 @@ 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):
@ -597,7 +602,7 @@ class MaxwellSimNode(bpy.types.Node):
) )
if optional: 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' msg = f'Input socket "{input_socket_name}" on "{self.bl_idname}" is not an active input socket'
raise ValueError(msg) raise ValueError(msg)
@ -645,14 +650,8 @@ class MaxwellSimNode(bpy.types.Node):
return output_socket_methods[0](self) return output_socket_methods[0](self)
# Auxiliary Fallbacks # Auxiliary Fallbacks
if kind == ct.FlowKind.Info: if optional or kind in [ct.FlowKind.Info, ct.FlowKind.Params]:
return ct.InfoFlow() return ct.FlowSignal.NoFlow
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)
@ -838,6 +837,11 @@ 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"

View File

@ -270,6 +270,8 @@ 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,

View File

@ -1,9 +1,12 @@
import typing as typ import typing as typ
from pathlib import Path 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 contracts as ct
from .... import sockets from .... import sockets
from ... import base, events from ... import base, events
@ -11,6 +14,40 @@ 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.
@ -34,68 +71,89 @@ 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
for module_name, module in sys.modules.copy().items(): @bl_cache.cached_bl_property()
if module_name == '__mp_main__': def sim_data(self) -> td.SimulationData | None:
print('Problematic Module Entry', module_name) cloud_task = self._compute_input(
print(module) 'Cloud Task', kind=ct.FlowKind.Value, optional=True
# 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 (
(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 isinstance(cloud_task, tdcloud.CloudTask)
and cloud_task.status == 'success' and cloud_task.status == 'success'
): ):
self.loose_output_sockets = { sim_data = tdcloud.TidyCloudTasks.download_task_sim_data(
'FDTD Sim Data': sockets.MaxwellFDTDSimDataSocketDef(), 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: 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 = {

View File

@ -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'].sync_prepare_new_task() self.inputs['Cloud Task'].on_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'].sync_created_new_task(cloud_task) self.inputs['Cloud Task'].on_new_task_created(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

View File

@ -548,7 +548,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Returns: Returns:
An empty `ct.InfoFlow`. An empty `ct.InfoFlow`.
""" """
return ct.InfoFlow() return ct.FlowSignal.NoFlow
# 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.ParamsFlow() return ct.FlowSignal.NoFlow
#################### ####################
# - FlowKind: Auxiliary # - FlowKind: Auxiliary
@ -577,8 +577,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Raises: Raises:
NotImplementedError: When used without being overridden. 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' return ct.FlowSignal.NoFlow
raise NotImplementedError(msg)
@value.setter @value.setter
def value(self, value: ct.ValueFlow) -> None: def value(self, value: ct.ValueFlow) -> None:
@ -604,8 +603,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Raises: Raises:
NotImplementedError: When used without being overridden. 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' return ct.FlowSignal.NoFlow
raise NotImplementedError(msg)
@array.setter @array.setter
def array(self, value: ct.ArrayFlow) -> None: def array(self, value: ct.ArrayFlow) -> None:
@ -631,8 +629,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Raises: Raises:
NotImplementedError: When used without being overridden. 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' return ct.FlowSignal.NoFlow
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:
@ -658,8 +655,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
Raises: Raises:
NotImplementedError: When used without being overridden. 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' return ct.FlowSignal.NoFlow
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:
@ -892,7 +888,8 @@ 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)
self.draw_info(info, col) if not ct.FlowSignal.check(info):
self.draw_info(info, col)
def draw_output( def draw_output(
self, self,
@ -920,7 +917,8 @@ 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)
self.draw_info(info, col) if not ct.FlowSignal.check(info):
self.draw_info(info, col)
#################### ####################
# - UI Methods: Active FlowKind # - UI Methods: Active FlowKind

View File

@ -34,10 +34,6 @@ class DataBLSocket(base.MaxwellSimSocket):
must_match={'format': self.format}, must_match={'format': self.format},
) )
@property
def value(self):
return None
#################### ####################
# - UI # - UI
#################### ####################

View File

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

View File

@ -6,10 +6,6 @@ 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

View File

@ -1,6 +1,10 @@
import enum
import bpy 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 contracts as ct
from .. import base from .. import base
@ -9,30 +13,32 @@ from .. import base
# - Operators # - Operators
#################### ####################
class ReloadFolderList(bpy.types.Operator): 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_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_AUTHENTICATED tdcloud.IS_ONLINE
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):
socket = context.socket bl_socket = context.socket
tdcloud.TidyCloudFolders.update_folders() 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'} return {'FINISHED'}
class Authenticate(bpy.types.Operator): class Authenticate(bpy.types.Operator):
bl_idname = 'blender_maxwell.sockets__authenticate' bl_idname = ct.OperatorType.SocketCloudAuthenticate
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'
@ -51,6 +57,7 @@ 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'}
@ -59,6 +66,16 @@ 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'
@ -67,81 +84,90 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
#################### ####################
# - Properties # - Properties
#################### ####################
# Authentication api_key: str = bl_cache.BLField('', prop_ui=True, str_secret=True)
api_key: bpy.props.StringProperty( should_exist: bool = bl_cache.BLField(False)
name='API Key',
description='API Key for the Tidy3D Cloud', existing_folder_id: enum.Enum = bl_cache.BLField(
default='', prop_ui=True, enum_cb=lambda self, _: self.search_cloud_folders()
options={'SKIP_SAVE'}, )
subtype='PASSWORD', existing_task_id: enum.Enum = bl_cache.BLField(
prop_ui=True, enum_cb=lambda self, _: self.search_cloud_tasks()
) )
# Task Existance Presumption new_task_name: str = bl_cache.BLField('', prop_ui=True)
should_exist: bpy.props.BoolProperty(
name='Cloud Task Should Exist',
description='Whether or not the cloud task should already exist',
default=False,
)
# Identifiers @property
existing_folder_id: bpy.props.EnumProperty( def capabilities(self) -> ct.CapabilitiesFlow:
name='Folder of Cloud Tasks', return ct.CapabilitiesFlow(
description='An existing folder on the Tidy3D Cloud', socket_type=self.socket_type,
items=lambda self, _: self.retrieve_folders(), active_kind=self.active_kind,
update=(lambda self, context: self.on_prop_changed('existing_folder_id', context)), must_match={'should_exist': self.should_exist},
) )
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)),
)
# (Potential) New Task @property
new_task_name: bpy.props.StringProperty( def value(
name='New Cloud Task Name', self,
description='Name of a new task to submit to the Tidy3D Cloud', ) -> tuple[tdcloud.CloudTaskName, tdcloud.CloudFolder] | tdcloud.CloudTask | None:
default='', if tdcloud.IS_AUTHENTICATED:
update=(lambda self, context: self.on_prop_changed('new_task_name', context)), # Retrieve Folder
) 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
) )
) is None: if cloud_folder is None:
return [('NONE', 'None', "Folder doesn't exist")] 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) tasks = tdcloud.TidyCloudTasks.tasks(cloud_folder)
if not tasks:
return [('NONE', 'None', 'No tasks in folder')]
return [ return [
( (
## Task ID ## Task ID
@ -158,9 +184,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',
@ -185,52 +211,28 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
] ]
#################### ####################
# - Task Sync Methods # - Node-Initiated Updates
#################### ####################
def sync_created_new_task(self, cloud_task): def on_new_task_created(self, cloud_task: tdcloud.CloudTask) -> None:
"""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 sync_prepare_new_task(self): def on_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
#################### ####################
# - Socket UI # - 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.operator( row.label(text='', icon=auth_icon)
Authenticate.bl_idname,
text='',
icon=auth_icon,
)
def draw_prelock( def draw_prelock(
self, self,
@ -245,11 +247,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, 'api_key', text='') row.prop(self, self.blfields['api_key'], text='')
row = col.row() row = col.row()
row.operator( row.operator(
Authenticate.bl_idname, ct.OperatorType.SocketCloudAuthenticate,
text='Connect', text='Connect',
) )
@ -260,9 +262,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, 'existing_folder_id', text='') row.prop(self, self.blfields['existing_folder_id'], text='')
row.operator( row.operator(
ReloadFolderList.bl_idname, ct.OperatorType.SocketReloadCloudFolderList,
text='', text='',
icon='FILE_REFRESH', icon='FILE_REFRESH',
) )
@ -272,47 +274,14 @@ 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, 'new_task_name', text='') row.prop(self, self.blfields['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, 'existing_task_id', text='') row.prop(self, self.blfields['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)
#################### ####################

View File

@ -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.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)) 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
del cls.cache_folder_tasks[folder_id] cls.cache_folder_tasks.pop(folder_id, None)
return dict(cls.tasks(cloud_folder).items()) return dict(cls.tasks(cloud_folder).items())

View File

@ -375,6 +375,7 @@ 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
@ -447,7 +448,7 @@ class CachedBLProperty:
#################### ####################
# - Property Decorators # - 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. """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.
@ -545,6 +546,7 @@ 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 = {}
@ -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`. 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)