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

View File

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

View File

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

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:
- **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

View File

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