From 035d8971f347181f15d83a19f7c376ba5c5bdd20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Sat, 18 May 2024 12:09:37 +0200 Subject: [PATCH] fix: local invalidation chains w/extract fixes --- .../nodes/analysis/extract_data.py | 60 +++++------ .../maxwell_sim_nodes/nodes/base.py | 64 ++++++++--- .../maxwell_sim_nodes/nodes/events.py | 8 ++ .../maxwell_sim_nodes/sockets/base.py | 33 +++--- src/blender_maxwell/utils/bl_instance.py | 100 +++++++++--------- 5 files changed, 151 insertions(+), 114 deletions(-) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py index 3c0eb98..f669aa6 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/extract_data.py @@ -14,7 +14,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -"""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 diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py index 4fca6f1..5e5f9bd 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py @@ -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 diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py index 24dcc0d..389c311 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/events.py @@ -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, ) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py index 1ab5ede..c115165 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/base.py @@ -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 - self.trigger_event(ct.FlowEvent.DataChanged) + # 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 diff --git a/src/blender_maxwell/utils/bl_instance.py b/src/blender_maxwell/utils/bl_instance.py index bafb6c9..260e2d9 100644 --- a/src/blender_maxwell/utils/bl_instance.py +++ b/src/blender_maxwell/utils/bl_instance.py @@ -219,9 +219,57 @@ class BLInstance: for str_search_prop_name in self.blfields_str_search: 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: """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()`. """ @@ -230,61 +278,11 @@ class BLInstance: # 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). - 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) + 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.