fix: local invalidation chains w/extract fixes
parent
e889d20284
commit
035d8971f3
|
@ -14,7 +14,7 @@
|
|||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""Declares `ExtractDataNode`."""
|
||||
"""Implements `ExtractDataNode`."""
|
||||
|
||||
import enum
|
||||
import typing as typ
|
||||
|
@ -40,16 +40,12 @@ TDMonitorData: typ.TypeAlias = td.components.data.monitor_data.MonitorData
|
|||
class ExtractDataNode(base.MaxwellSimNode):
|
||||
"""Extract data from sockets for further analysis.
|
||||
|
||||
# Socket Sets
|
||||
## Sim Data
|
||||
Extracts monitors from a `MaxwelFDTDSimDataSocket`.
|
||||
|
||||
## Monitor Data
|
||||
Extracts array attributes from a `MaxwelFDTDSimDataSocket`.
|
||||
Socket Sets:
|
||||
Sim Data: Extract monitor data from simulation data by-name.
|
||||
Monitor Data: Extract `Expr`s from monitor data by-component.
|
||||
|
||||
Attributes:
|
||||
extract_filter: Identifier for data to extract from the input.
|
||||
|
||||
"""
|
||||
|
||||
node_type = ct.NodeType.ExtractData
|
||||
|
@ -71,12 +67,18 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
####################
|
||||
extract_filter: enum.StrEnum = bl_cache.BLField(
|
||||
enum_cb=lambda self, _: self.search_extract_filters(),
|
||||
cb_depends_on={'sim_data_monitor_nametype', 'monitor_data_type'},
|
||||
)
|
||||
|
||||
####################
|
||||
# - Computed: Sim Data
|
||||
####################
|
||||
@property
|
||||
@events.on_value_changed(socket_name='Sim Data')
|
||||
def on_sim_data_changed(self) -> None: # noqa: D102
|
||||
log.critical('On Value Changed: Sim Data')
|
||||
self.sim_data = bl_cache.Signal.InvalidateCache
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
def sim_data(self) -> td.SimulationData | None:
|
||||
"""Extracts the simulation data from the input socket.
|
||||
|
||||
|
@ -92,7 +94,7 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
|
||||
return None
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
@bl_cache.cached_bl_property(depends_on={'sim_data'})
|
||||
def sim_data_monitor_nametype(self) -> dict[str, str] | None:
|
||||
"""For simulation data, deduces a map from the monitor name to the monitor "type".
|
||||
|
||||
|
@ -110,7 +112,12 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
####################
|
||||
# - Computed Properties: Monitor Data
|
||||
####################
|
||||
@property
|
||||
@events.on_value_changed(socket_name='Monitor Data')
|
||||
def on_monitor_data_changed(self) -> None: # noqa: D102
|
||||
log.critical('On Value Changed: Sim Data')
|
||||
self.monitor_data = bl_cache.Signal.InvalidateCache
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
def monitor_data(self) -> TDMonitorData | None:
|
||||
"""Extracts the monitor data from the input socket.
|
||||
|
||||
|
@ -126,7 +133,7 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
|
||||
return None
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
@bl_cache.cached_bl_property(depends_on={'monitor_data'})
|
||||
def monitor_data_type(self) -> str | None:
|
||||
r"""For monitor data, deduces the monitor "type".
|
||||
|
||||
|
@ -149,7 +156,7 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
|
||||
return None
|
||||
|
||||
@bl_cache.cached_bl_property()
|
||||
@bl_cache.cached_bl_property(depends_on={'monitor_data_type'})
|
||||
def monitor_data_attrs(self) -> list[str] | None:
|
||||
r"""For monitor data, deduces the valid data-containing attributes.
|
||||
|
||||
|
@ -304,26 +311,9 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
col.prop(self, self.blfields['extract_filter'], text='')
|
||||
|
||||
####################
|
||||
# - Events
|
||||
####################
|
||||
@events.on_value_changed(
|
||||
# Trigger
|
||||
socket_name={'Sim Data', 'Monitor Data'},
|
||||
prop_name='active_socket_set',
|
||||
run_on_init=True,
|
||||
)
|
||||
def on_input_sockets_changed(self) -> None:
|
||||
"""Invalidate the cached properties for sim data / monitor data, and reset the extraction filter."""
|
||||
self.sim_data_monitor_nametype = bl_cache.Signal.InvalidateCache
|
||||
self.monitor_data_type = bl_cache.Signal.InvalidateCache
|
||||
self.monitor_data_attrs = bl_cache.Signal.InvalidateCache
|
||||
self.extract_filter = bl_cache.Signal.ResetEnumItems
|
||||
|
||||
####################
|
||||
# - Output (Value): Sim Data -> Monitor Data
|
||||
# - FlowKind.Value: Sim Data -> Monitor Data
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Monitor Data',
|
||||
kind=ct.FlowKind.Value,
|
||||
# Loaded
|
||||
|
@ -348,10 +338,9 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
return ct.FlowSignal.FlowPending
|
||||
|
||||
####################
|
||||
# - Output (Array): Monitor Data -> Expr
|
||||
# - FlowKind.Array|LazyValueFunc: Monitor Data -> Expr
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Expr',
|
||||
kind=ct.FlowKind.Array,
|
||||
# Loaded
|
||||
|
@ -407,7 +396,7 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
return ct.FlowSignal.FlowPending
|
||||
|
||||
####################
|
||||
# - Auxiliary (Params): Monitor Data -> Expr
|
||||
# - FlowKind.Params: Monitor Data -> Expr
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
'Expr',
|
||||
|
@ -422,10 +411,9 @@ class ExtractDataNode(base.MaxwellSimNode):
|
|||
return ct.ParamsFlow()
|
||||
|
||||
####################
|
||||
# - Auxiliary (Info): Monitor Data -> Expr
|
||||
# - FlowKind.Info: Monitor Data -> Expr
|
||||
####################
|
||||
@events.computes_output_socket(
|
||||
# Trigger
|
||||
'Expr',
|
||||
kind=ct.FlowKind.Info,
|
||||
# Loaded
|
||||
|
|
|
@ -416,14 +416,46 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
|||
|
||||
# Remove Sockets
|
||||
for bl_socket in bl_sockets_to_remove:
|
||||
bl_socket_name = bl_socket.name
|
||||
|
||||
# 1. Report the socket removal to the NodeTree.
|
||||
## -> The NodeLinkCache needs to be adjusted manually.
|
||||
node_tree.on_node_socket_removed(bl_socket)
|
||||
|
||||
# 2. Invalidate the input socket cache across all kinds.
|
||||
## -> Prevents phantom values from remaining available.
|
||||
self._compute_input.invalidate(
|
||||
input_socket_name=bl_socket.name,
|
||||
input_socket_name=bl_socket_name,
|
||||
kind=...,
|
||||
unit_system=...,
|
||||
)
|
||||
|
||||
# 3. Perform the removal using Blender's API.
|
||||
## -> Actually removes the socket.
|
||||
all_bl_sockets.remove(bl_socket)
|
||||
|
||||
if direc == 'input':
|
||||
# 4. Run all trigger-only `on_value_changed` callbacks.
|
||||
## -> Runs any event methods that relied on the socket.
|
||||
## -> Only methods that don't **require** the socket.
|
||||
## Trigger-Only: If method loads no socket data, it runs.
|
||||
## `optional`: If method optional-loads socket, it runs.
|
||||
triggered_event_methods = [
|
||||
event_method
|
||||
for event_method in self.filtered_event_methods_by_event(
|
||||
ct.FlowEvent.DataChanged, (bl_socket_name, None, None)
|
||||
)
|
||||
if bl_socket_name
|
||||
not in event_method.callback_info.must_load_sockets
|
||||
]
|
||||
for event_method in triggered_event_methods:
|
||||
log.critical(
|
||||
'%s: Running %s',
|
||||
self.sim_node_name,
|
||||
str(event_method),
|
||||
)
|
||||
event_method(self)
|
||||
|
||||
def _add_new_active_sockets(self):
|
||||
"""Add and initialize all "active" sockets that aren't on the node.
|
||||
|
||||
|
@ -737,14 +769,14 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
|||
socket_name: The input socket that was altered, if any, in order to trigger this event.
|
||||
pop_name: The property that was altered, if any, in order to trigger this event.
|
||||
"""
|
||||
# log.debug(
|
||||
# '%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
|
||||
# self.sim_node_name,
|
||||
# event,
|
||||
# str(socket_name),
|
||||
# str(socket_kinds),
|
||||
# str(prop_name),
|
||||
# )
|
||||
log.debug(
|
||||
'%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
|
||||
self.sim_node_name,
|
||||
event,
|
||||
str(socket_name),
|
||||
str(socket_kinds),
|
||||
str(prop_name),
|
||||
)
|
||||
# Outflow Socket Kinds
|
||||
## -> Something has happened!
|
||||
## -> The effect is yet to be determined...
|
||||
|
@ -860,12 +892,18 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
|
|||
Parameters:
|
||||
prop_name: The name of the property that changed.
|
||||
"""
|
||||
# All Attributes: Trigger Event
|
||||
## -> This declares that the single property has changed.
|
||||
## -> This should happen first, in case dependents need a cache.
|
||||
if hasattr(self, prop_name):
|
||||
# Trigger Event
|
||||
self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
|
||||
else:
|
||||
msg = f'Property {prop_name} not defined on node {self}'
|
||||
raise RuntimeError(msg)
|
||||
|
||||
# BLField Attributes: Invalidate BLField Dependents
|
||||
## -> Dependent props will generally also trigger on_prop_changed.
|
||||
## -> The recursion ends with the depschain.
|
||||
## -> WARNING: The chain is not checked for ex. cycles.
|
||||
if prop_name in self.blfields:
|
||||
self.invalidate_blfield_deps(prop_name)
|
||||
|
||||
####################
|
||||
# - UI Methods
|
||||
|
|
|
@ -38,6 +38,7 @@ class InfoDataChanged:
|
|||
on_changed_sockets: set[ct.SocketName]
|
||||
on_changed_props: set[str]
|
||||
on_any_changed_loose_input: set[str]
|
||||
must_load_sockets: set[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass(kw_only=True, frozen=True)
|
||||
|
@ -356,12 +357,19 @@ def on_value_changed(
|
|||
return event_decorator(
|
||||
event=ct.FlowEvent.DataChanged,
|
||||
callback_info=InfoDataChanged(
|
||||
# Triggers
|
||||
run_on_init=run_on_init,
|
||||
on_changed_sockets=(
|
||||
socket_name if isinstance(socket_name, set) else {socket_name}
|
||||
),
|
||||
on_changed_props=(prop_name if isinstance(prop_name, set) else {prop_name}),
|
||||
on_any_changed_loose_input=any_loose_input_socket,
|
||||
# Loaded
|
||||
must_load_sockets={
|
||||
socket_name
|
||||
for socket_name in kwargs.get('input_sockets', {})
|
||||
if socket_name not in kwargs.get('input_sockets_optional', {})
|
||||
},
|
||||
),
|
||||
**kwargs,
|
||||
)
|
||||
|
|
|
@ -200,32 +200,37 @@ class MaxwellSimSocket(bpy.types.NodeSocket, bl_instance.BLInstance):
|
|||
Contrary to `node.on_prop_changed()`, socket-specific callbacks are baked into this function:
|
||||
|
||||
- **Active Kind** (`self.active_kind`): Sets the socket shape to reflect the active `FlowKind`.
|
||||
**MAY NOT** rely on `FlowEvent` driven caches.
|
||||
- **Overrided Local Events** (`self.active_kind`): Sets the socket shape to reflect the active `FlowKind`.
|
||||
**MAY NOT** rely on `FlowEvent` driven caches.
|
||||
|
||||
Attributes:
|
||||
prop_name: The name of the property that was changed.
|
||||
"""
|
||||
## TODO: Evaluate this properly
|
||||
if self.is_initializing:
|
||||
pass
|
||||
# log.debug(
|
||||
# '%s: Rejected on_prop_changed("%s") while initializing',
|
||||
# self.bl_label,
|
||||
# prop_name,
|
||||
# )
|
||||
elif hasattr(self, prop_name):
|
||||
# All Attributes: Trigger Local Event
|
||||
## -> While initializing, only `DataChanged` won't trigger.
|
||||
if hasattr(self, prop_name):
|
||||
# Property Callbacks: Active Kind
|
||||
## -> WARNING: May NOT rely on flow.
|
||||
if prop_name == 'active_kind':
|
||||
self.on_active_kind_changed()
|
||||
|
||||
# Property Callbacks: Per-Socket
|
||||
## -> WARNING: May NOT rely on flow.
|
||||
self.on_socket_prop_changed(prop_name)
|
||||
|
||||
# Trigger Event
|
||||
# Not Initializing: Trigger Event
|
||||
## -> This declares that the socket has changed.
|
||||
## -> This should happen first, in case dependents need a cache.
|
||||
if not self.is_initializing:
|
||||
self.trigger_event(ct.FlowEvent.DataChanged)
|
||||
|
||||
else:
|
||||
msg = f'Property {prop_name} not defined on socket {self.bl_label} ({self.socket_type})'
|
||||
raise RuntimeError(msg)
|
||||
# BLField Attributes: Invalidate BLField Dependents
|
||||
## -> Dependent props will generally also trigger on_prop_changed.
|
||||
## -> The recursion ends with the depschain.
|
||||
## -> WARNING: The chain is not checked for ex. cycles.
|
||||
if prop_name in self.blfields:
|
||||
self.invalidate_blfield_deps(prop_name)
|
||||
|
||||
####################
|
||||
# - Link Event: Consent / On Change
|
||||
|
|
|
@ -219,41 +219,21 @@ class BLInstance:
|
|||
for str_search_prop_name in self.blfields_str_search:
|
||||
setattr(self, str_search_prop_name, bl_cache.Signal.ResetStrSearch)
|
||||
|
||||
def on_bl_prop_changed(self, bl_prop_name: str, _: bpy.types.Context) -> None:
|
||||
"""Called when a property has been updated via the Blender UI.
|
||||
def invalidate_blfield_deps(self, prop_name: str) -> None:
|
||||
"""Invalidates all properties that depend on `prop_name`.
|
||||
|
||||
The only effect is to invalidate the non-persistent cache of the associated BLField.
|
||||
The BLField then decides whether to take any other action, ex. calling `self.on_prop_changed()`.
|
||||
A property can recursively depend on other properties, including specificity as to whether the cache should be invalidated, the enum items be recomputed, or the string search items be recomputed.
|
||||
|
||||
This method actually implements this, by correctly invalidating all immediate dependents of `prop_name`.
|
||||
As it is generally called during `self.on_bl_prop_changed()` / `self.on_prop_changed()`, invalidating immediate dependents is an implicitly recursive action.
|
||||
|
||||
Notes:
|
||||
The dictionaries governing exactly what invalidates what, and how, are encoded as `self.blfield_deps`, `self.blfield_dynamic_enum_deps`, and `self.blfield_str_search_deps`.
|
||||
All of these are filled when creating the `BLInstance` subclass, using `self.declare_blfield_dep()`, generally via the `BLField` descriptor (which internally uses `BLProp`).
|
||||
"""
|
||||
## TODO: What about non-Blender set properties?
|
||||
|
||||
# Strip the Internal Prefix
|
||||
## -> TODO: This is a bit of a hack. Use a contracts constant.
|
||||
prop_name = bl_prop_name.removeprefix('blfield__')
|
||||
# log.debug(
|
||||
# 'Callback on Property %s (stripped: %s)',
|
||||
# bl_prop_name,
|
||||
# prop_name,
|
||||
# )
|
||||
# log.debug(
|
||||
# 'Dependencies (PROP: %s) (ENUM: %s) (SEAR: %s)',
|
||||
# self.blfield_deps,
|
||||
# self.blfield_dynamic_enum_deps,
|
||||
# self.blfield_str_search_deps,
|
||||
# )
|
||||
|
||||
# Invalidate Property Cache
|
||||
## -> Only the non-persistent cache is regenerated.
|
||||
## -> The BLField decides whether to trigger `on_prop_changed`.
|
||||
if prop_name in self.blfields:
|
||||
# RULE: =1 DataChanged per Dependency Chain
|
||||
## -> We MUST invalidate the cache, but might not want to update.
|
||||
## -> Update should only be triggered when ==0 dependents.
|
||||
setattr(self, prop_name, bl_cache.Signal.InvalidateCacheNoUpdate)
|
||||
|
||||
# Invalidate Dependent Properties (incl. DynEnums and StrSearch)
|
||||
## -> NOTE: Dependent props may also trigger `on_prop_changed`.
|
||||
## -> Meaning, don't use extraneous dependencies (as usual).
|
||||
## -> Don't abuse dependencies :)
|
||||
for deps, invalidate_signal in zip(
|
||||
[
|
||||
self.blfield_deps,
|
||||
|
@ -269,22 +249,40 @@ class BLInstance:
|
|||
):
|
||||
if prop_name in deps:
|
||||
for dst_prop_name in deps[prop_name]:
|
||||
# log.debug(
|
||||
# 'Property %s is invalidating %s',
|
||||
# prop_name,
|
||||
# dst_prop_name,
|
||||
# )
|
||||
log.debug(
|
||||
'Property %s is invalidating %s',
|
||||
prop_name,
|
||||
dst_prop_name,
|
||||
)
|
||||
setattr(
|
||||
self,
|
||||
dst_prop_name,
|
||||
invalidate_signal,
|
||||
)
|
||||
|
||||
# Do Update AFTER Dependencies
|
||||
## -> Yes, update will run once per dependency.
|
||||
## -> Don't abuse dependencies :)
|
||||
## -> If no-update is important, use_prop_update is still respected.
|
||||
setattr(self, prop_name, bl_cache.Signal.DoUpdate)
|
||||
def on_bl_prop_changed(self, bl_prop_name: str, _: bpy.types.Context) -> None:
|
||||
"""Called when a property has been updated via the Blender UI.
|
||||
|
||||
In general, **all** Blender UI properties in the entire program will call this method using `update`.
|
||||
Whether anything further happens is a little more nuanced.
|
||||
|
||||
1. The cache of the `prop_name` associated with `bl_prop_name` is invalidated, but without invoking a cache update.
|
||||
|
||||
Primarily, `self.invalidate_blfield_deps()`
|
||||
|
||||
The only effect is to invalidate the non-persistent cache of the associated BLField.
|
||||
The BLField then decides whether to take any other action, ex. calling `self.on_prop_changed()`.
|
||||
"""
|
||||
## TODO: What about non-Blender set properties?
|
||||
|
||||
# Strip the Internal Prefix
|
||||
## -> TODO: This is a bit of a hack. Use a contracts constant.
|
||||
prop_name = bl_prop_name.removeprefix('blfield__')
|
||||
|
||||
# Invalidate Property Cache
|
||||
## -> The BLField decides whether to trigger `on_prop_changed`.
|
||||
if prop_name in self.blfields:
|
||||
setattr(self, prop_name, bl_cache.Signal.InvalidateCache)
|
||||
|
||||
def on_prop_changed(self, prop_name: str) -> None:
|
||||
"""Triggers changes/an event chain based on a changed property.
|
||||
|
|
Loading…
Reference in New Issue