fix: local invalidation chains w/extract fixes

main
Sofus Albert Høgsbro Rose 2024-05-18 12:09:37 +02:00
parent e889d20284
commit 035d8971f3
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
5 changed files with 151 additions and 114 deletions

View File

@ -14,7 +14,7 @@
# You should have received a copy of the GNU Affero General Public License # 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/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
"""Declares `ExtractDataNode`.""" """Implements `ExtractDataNode`."""
import enum import enum
import typing as typ import typing as typ
@ -40,16 +40,12 @@ TDMonitorData: typ.TypeAlias = td.components.data.monitor_data.MonitorData
class ExtractDataNode(base.MaxwellSimNode): class ExtractDataNode(base.MaxwellSimNode):
"""Extract data from sockets for further analysis. """Extract data from sockets for further analysis.
# Socket Sets Socket Sets:
## Sim Data Sim Data: Extract monitor data from simulation data by-name.
Extracts monitors from a `MaxwelFDTDSimDataSocket`. Monitor Data: Extract `Expr`s from monitor data by-component.
## 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.
""" """
node_type = ct.NodeType.ExtractData node_type = ct.NodeType.ExtractData
@ -71,12 +67,18 @@ class ExtractDataNode(base.MaxwellSimNode):
#################### ####################
extract_filter: enum.StrEnum = bl_cache.BLField( extract_filter: enum.StrEnum = bl_cache.BLField(
enum_cb=lambda self, _: self.search_extract_filters(), enum_cb=lambda self, _: self.search_extract_filters(),
cb_depends_on={'sim_data_monitor_nametype', 'monitor_data_type'},
) )
#################### ####################
# - Computed: Sim Data # - 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: def sim_data(self) -> td.SimulationData | None:
"""Extracts the simulation data from the input socket. """Extracts the simulation data from the input socket.
@ -92,7 +94,7 @@ class ExtractDataNode(base.MaxwellSimNode):
return None 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: def sim_data_monitor_nametype(self) -> dict[str, str] | None:
"""For simulation data, deduces a map from the monitor name to the monitor "type". """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 # - 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: def monitor_data(self) -> TDMonitorData | None:
"""Extracts the monitor data from the input socket. """Extracts the monitor data from the input socket.
@ -126,7 +133,7 @@ class ExtractDataNode(base.MaxwellSimNode):
return None return None
@bl_cache.cached_bl_property() @bl_cache.cached_bl_property(depends_on={'monitor_data'})
def monitor_data_type(self) -> str | None: def monitor_data_type(self) -> str | None:
r"""For monitor data, deduces the monitor "type". r"""For monitor data, deduces the monitor "type".
@ -149,7 +156,7 @@ class ExtractDataNode(base.MaxwellSimNode):
return None 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: def monitor_data_attrs(self) -> list[str] | None:
r"""For monitor data, deduces the valid data-containing attributes. 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='') col.prop(self, self.blfields['extract_filter'], text='')
#################### ####################
# - Events # - FlowKind.Value: Sim Data -> Monitor Data
####################
@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
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
# Trigger
'Monitor Data', 'Monitor Data',
kind=ct.FlowKind.Value, kind=ct.FlowKind.Value,
# Loaded # Loaded
@ -348,10 +338,9 @@ class ExtractDataNode(base.MaxwellSimNode):
return ct.FlowSignal.FlowPending return ct.FlowSignal.FlowPending
#################### ####################
# - Output (Array): Monitor Data -> Expr # - FlowKind.Array|LazyValueFunc: Monitor Data -> Expr
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
# Trigger
'Expr', 'Expr',
kind=ct.FlowKind.Array, kind=ct.FlowKind.Array,
# Loaded # Loaded
@ -407,7 +396,7 @@ class ExtractDataNode(base.MaxwellSimNode):
return ct.FlowSignal.FlowPending return ct.FlowSignal.FlowPending
#################### ####################
# - Auxiliary (Params): Monitor Data -> Expr # - FlowKind.Params: Monitor Data -> Expr
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
'Expr', 'Expr',
@ -422,10 +411,9 @@ class ExtractDataNode(base.MaxwellSimNode):
return ct.ParamsFlow() return ct.ParamsFlow()
#################### ####################
# - Auxiliary (Info): Monitor Data -> Expr # - FlowKind.Info: Monitor Data -> Expr
#################### ####################
@events.computes_output_socket( @events.computes_output_socket(
# Trigger
'Expr', 'Expr',
kind=ct.FlowKind.Info, kind=ct.FlowKind.Info,
# Loaded # Loaded

View File

@ -416,14 +416,46 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
# Remove Sockets # Remove Sockets
for bl_socket in bl_sockets_to_remove: 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) 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( self._compute_input.invalidate(
input_socket_name=bl_socket.name, input_socket_name=bl_socket_name,
kind=..., kind=...,
unit_system=..., unit_system=...,
) )
# 3. Perform the removal using Blender's API.
## -> Actually removes the socket.
all_bl_sockets.remove(bl_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): def _add_new_active_sockets(self):
"""Add and initialize all "active" sockets that aren't on the node. """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. 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. pop_name: The property that was altered, if any, in order to trigger this event.
""" """
# log.debug( log.debug(
# '%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)', '%s: Triggered Event %s (socket_name=%s, socket_kinds=%s, prop_name=%s)',
# self.sim_node_name, self.sim_node_name,
# event, event,
# str(socket_name), str(socket_name),
# str(socket_kinds), str(socket_kinds),
# str(prop_name), str(prop_name),
# ) )
# Outflow Socket Kinds # Outflow Socket Kinds
## -> Something has happened! ## -> Something has happened!
## -> The effect is yet to be determined... ## -> The effect is yet to be determined...
@ -860,12 +892,18 @@ class MaxwellSimNode(bpy.types.Node, bl_instance.BLInstance):
Parameters: Parameters:
prop_name: The name of the property that changed. 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): if hasattr(self, prop_name):
# Trigger Event
self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name) self.trigger_event(ct.FlowEvent.DataChanged, prop_name=prop_name)
else:
msg = f'Property {prop_name} not defined on node {self}' # BLField Attributes: Invalidate BLField Dependents
raise RuntimeError(msg) ## -> 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 # - UI Methods

View File

@ -38,6 +38,7 @@ class InfoDataChanged:
on_changed_sockets: set[ct.SocketName] on_changed_sockets: set[ct.SocketName]
on_changed_props: set[str] on_changed_props: set[str]
on_any_changed_loose_input: set[str] on_any_changed_loose_input: set[str]
must_load_sockets: set[str]
@dataclasses.dataclass(kw_only=True, frozen=True) @dataclasses.dataclass(kw_only=True, frozen=True)
@ -356,12 +357,19 @@ def on_value_changed(
return event_decorator( return event_decorator(
event=ct.FlowEvent.DataChanged, event=ct.FlowEvent.DataChanged,
callback_info=InfoDataChanged( callback_info=InfoDataChanged(
# Triggers
run_on_init=run_on_init, run_on_init=run_on_init,
on_changed_sockets=( on_changed_sockets=(
socket_name if isinstance(socket_name, set) else {socket_name} socket_name if isinstance(socket_name, set) else {socket_name}
), ),
on_changed_props=(prop_name if isinstance(prop_name, set) else {prop_name}), on_changed_props=(prop_name if isinstance(prop_name, set) else {prop_name}),
on_any_changed_loose_input=any_loose_input_socket, 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, **kwargs,
) )

View File

@ -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: 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`. - **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: Attributes:
prop_name: The name of the property that was changed. prop_name: The name of the property that was changed.
""" """
## TODO: Evaluate this properly # All Attributes: Trigger Local Event
if self.is_initializing: ## -> While initializing, only `DataChanged` won't trigger.
pass if hasattr(self, prop_name):
# log.debug(
# '%s: Rejected on_prop_changed("%s") while initializing',
# self.bl_label,
# prop_name,
# )
elif hasattr(self, prop_name):
# Property Callbacks: Active Kind # Property Callbacks: Active Kind
## -> WARNING: May NOT rely on flow.
if prop_name == 'active_kind': if prop_name == 'active_kind':
self.on_active_kind_changed() self.on_active_kind_changed()
# Property Callbacks: Per-Socket # Property Callbacks: Per-Socket
## -> WARNING: May NOT rely on flow.
self.on_socket_prop_changed(prop_name) self.on_socket_prop_changed(prop_name)
# Trigger Event # Not Initializing: Trigger Event
self.trigger_event(ct.FlowEvent.DataChanged) ## -> 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: # BLField Attributes: Invalidate BLField Dependents
msg = f'Property {prop_name} not defined on socket {self.bl_label} ({self.socket_type})' ## -> Dependent props will generally also trigger on_prop_changed.
raise RuntimeError(msg) ## -> 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 # - Link Event: Consent / On Change

View File

@ -219,9 +219,57 @@ class BLInstance:
for str_search_prop_name in self.blfields_str_search: for str_search_prop_name in self.blfields_str_search:
setattr(self, str_search_prop_name, bl_cache.Signal.ResetStrSearch) setattr(self, str_search_prop_name, bl_cache.Signal.ResetStrSearch)
def invalidate_blfield_deps(self, prop_name: str) -> None:
"""Invalidates all properties that depend on `prop_name`.
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`).
"""
# Invalidate Dependent Properties (incl. DynEnums and StrSearch)
## -> NOTE: Dependent props may also trigger `on_prop_changed`.
## -> Don't abuse dependencies :)
for deps, invalidate_signal in zip(
[
self.blfield_deps,
self.blfield_dynamic_enum_deps,
self.blfield_str_search_deps,
],
[
bl_cache.Signal.InvalidateCache,
bl_cache.Signal.ResetEnumItems,
bl_cache.Signal.ResetStrSearch,
],
strict=True,
):
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,
)
setattr(
self,
dst_prop_name,
invalidate_signal,
)
def on_bl_prop_changed(self, bl_prop_name: str, _: bpy.types.Context) -> None: 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. """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 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()`. The BLField then decides whether to take any other action, ex. calling `self.on_prop_changed()`.
""" """
@ -230,61 +278,11 @@ class BLInstance:
# Strip the Internal Prefix # Strip the Internal Prefix
## -> TODO: This is a bit of a hack. Use a contracts constant. ## -> TODO: This is a bit of a hack. Use a contracts constant.
prop_name = bl_prop_name.removeprefix('blfield__') 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 # Invalidate Property Cache
## -> Only the non-persistent cache is regenerated.
## -> The BLField decides whether to trigger `on_prop_changed`. ## -> The BLField decides whether to trigger `on_prop_changed`.
if prop_name in self.blfields: if prop_name in self.blfields:
# RULE: =1 DataChanged per Dependency Chain setattr(self, prop_name, bl_cache.Signal.InvalidateCache)
## -> 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).
for deps, invalidate_signal in zip(
[
self.blfield_deps,
self.blfield_dynamic_enum_deps,
self.blfield_str_search_deps,
],
[
bl_cache.Signal.InvalidateCache,
bl_cache.Signal.ResetEnumItems,
bl_cache.Signal.ResetStrSearch,
],
strict=True,
):
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,
# )
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_prop_changed(self, prop_name: str) -> None: def on_prop_changed(self, prop_name: str) -> None:
"""Triggers changes/an event chain based on a changed property. """Triggers changes/an event chain based on a changed property.