fix: BLFields in FilterMath, bug fixes.

main
Sofus Albert Høgsbro Rose 2024-04-23 09:30:26 +02:00
parent 44d61b5639
commit f09b58e0e7
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
6 changed files with 100 additions and 62 deletions

View File

@ -24,7 +24,7 @@ class FlowEvent(enum.StrEnum):
ShowPlot: Indicates that the node/socket should enable its plotted preview. ShowPlot: Indicates that the node/socket should enable its plotted preview.
This should generally be used if the node is rendering to an image, for viewing through the Blender image editor. This should generally be used if the node is rendering to an image, for viewing through the Blender image editor.
LinkChanged: Indicates that a link to a node/socket was added/removed. LinkChanged: Indicates that a link to a node/socket was added/removed.
In nodes, this is accompanied by a `socket_name` to indicate which socket it is that had its links altered. Is translated to `DataChanged` on sockets before propagation.
DataChanged: Indicates that data flowing through a node/socket was altered. DataChanged: Indicates that data flowing through a node/socket was altered.
In nodes, this event is accompanied by a `socket_name` or `prop_name`, to indicate which socket/property it is that was changed. In nodes, this event is accompanied by a `socket_name` or `prop_name`, to indicate which socket/property it is that was changed.
**This event is essential**, as it invalidates all input/output socket caches along its path. **This event is essential**, as it invalidates all input/output socket caches along its path.

View File

@ -195,7 +195,7 @@ class ExtractDataNode(base.MaxwellSimNode):
@events.computes_output_socket( @events.computes_output_socket(
'Data', 'Data',
kind=ct.FlowKind.Value, kind=ct.FlowKind.Array,
props={'extract_filter'}, props={'extract_filter'},
input_sockets={'Monitor Data'}, input_sockets={'Monitor Data'},
) )
@ -210,7 +210,7 @@ class ExtractDataNode(base.MaxwellSimNode):
'Data', 'Data',
kind=ct.FlowKind.LazyValueFunc, kind=ct.FlowKind.LazyValueFunc,
output_sockets={'Data'}, output_sockets={'Data'},
output_socket_kinds={'Data': ct.FlowKind.Value}, output_socket_kinds={'Data': ct.FlowKind.Array},
) )
def compute_extracted_data_lazy(self, output_sockets: dict) -> ct.LazyValueFuncFlow: def compute_extracted_data_lazy(self, output_sockets: dict) -> ct.LazyValueFuncFlow:
return ct.LazyValueFuncFlow( return ct.LazyValueFuncFlow(

View File

@ -1,3 +1,4 @@
import enum
import typing as typ import typing as typ
import bpy import bpy
@ -14,6 +15,13 @@ log = logger.get(__name__)
class FilterMathNode(base.MaxwellSimNode): class FilterMathNode(base.MaxwellSimNode):
"""Reduces the dimensionality of data.
Attributes:
operation: Operation to apply to the input.
dim: Dims to use when filtering data
"""
node_type = ct.NodeType.FilterMath node_type = ct.NodeType.FilterMath
bl_label = 'Filter Math' bl_label = 'Filter Math'
@ -31,32 +39,17 @@ class FilterMathNode(base.MaxwellSimNode):
#################### ####################
# - Properties # - Properties
#################### ####################
operation: bpy.props.EnumProperty( operation: enum.Enum = bl_cache.BLField(
name='Op', prop_ui=True, enum_cb=lambda self, _: self.search_operations()
description='Operation to filter with',
items=lambda self, _: self.search_operations(),
update=lambda self, context: self.on_prop_changed('operation', context),
) )
dim: bpy.props.StringProperty( dim: str = bl_cache.BLField(
name='Dim', '', prop_ui=True, str_cb=lambda self, _, edit_text: self.search_dims(edit_text)
description='Dims to use when filtering data',
default='',
search=lambda self, _, edit_text: self.search_dims(edit_text),
update=lambda self, context: self.on_prop_changed('dim', context),
) )
dim_names: list[str] = bl_cache.BLField([]) dim_names: list[str] = bl_cache.BLField([])
dim_lens: dict[str, int] = bl_cache.BLField({}) dim_lens: dict[str, int] = bl_cache.BLField({})
@property
def has_dim(self) -> bool:
return (
self.active_socket_set in ['By Dim', 'By Dim Value']
and self.inputs['Data'].is_linked
and self.dim_names
)
#################### ####################
# - Operation Search # - Operation Search
#################### ####################
@ -77,7 +70,7 @@ class FilterMathNode(base.MaxwellSimNode):
# - Dim Search # - Dim Search
#################### ####################
def search_dims(self, edit_text: str) -> list[tuple[str, str, str]]: def search_dims(self, edit_text: str) -> list[tuple[str, str, str]]:
if self.has_dim: if self.dim_names:
dims = [ dims = [
(dim_name, dim_name) (dim_name, dim_name)
for dim_name in self.dim_names for dim_name in self.dim_names
@ -94,17 +87,23 @@ class FilterMathNode(base.MaxwellSimNode):
# - UI # - UI
#################### ####################
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, 'operation', text='') layout.prop(self, self.blfields['operation'], text='')
if self.has_dim: if self.dim_names:
layout.prop(self, 'dim', text='') layout.prop(self, self.blfields['dim'], text='')
#################### ####################
# - Events # - Events
#################### ####################
@events.on_value_changed(
prop_name='active_socket_set',
)
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', 'dim'}, prop_name={'active_socket_set'},
props={'active_socket_set', 'dim'}, props={'active_socket_set'},
input_sockets={'Data'}, input_sockets={'Data'},
input_socket_kinds={'Data': ct.FlowKind.Info}, input_socket_kinds={'Data': ct.FlowKind.Info},
input_sockets_optional={'Data': True}, input_sockets_optional={'Data': True},
@ -121,21 +120,32 @@ class FilterMathNode(base.MaxwellSimNode):
self.dim_names = [] self.dim_names = []
self.dim_lens = {} self.dim_lens = {}
# Add Input Value w/Unit from InfoFlow # Reset String Searcher
## Socket Type is determined from the Unit self.dim = bl_cache.Signal.ResetStrSearch
@events.on_value_changed(
prop_name='dim',
props={'active_socket_set', 'dim'},
input_sockets={'Data'},
input_socket_kinds={'Data': ct.FlowKind.Info},
input_sockets_optional={'Data': True},
)
def on_dim_change(self, props: dict, input_sockets: dict):
# Add/Remove Input Socket "Value"
if ( if (
props['active_socket_set'] == 'By Dim Value' props['active_socket_set'] == 'By Dim Value'
and props['dim'] != ''
and props['dim'] in input_sockets['Data'].dim_names and props['dim'] in input_sockets['Data'].dim_names
): ):
socket_def = sockets.SOCKET_DEFS[ # Get Current and Wanted Socket Defs
current_socket_def = self.loose_input_sockets.get('Value')
wanted_socket_def = sockets.SOCKET_DEFS[
ct.unit_to_socket_type(input_sockets['Data'].dim_idx[props['dim']].unit) ct.unit_to_socket_type(input_sockets['Data'].dim_idx[props['dim']].unit)
] ]
if (
_val_socket_def := self.loose_input_sockets.get('Value') # Determine Whether to Declare New Loose Input SOcket
) is None or _val_socket_def != socket_def: if current_socket_def is None or current_socket_def != wanted_socket_def:
self.loose_input_sockets = { self.loose_input_sockets = {
'Value': socket_def(), 'Value': wanted_socket_def(),
} }
elif self.loose_input_sockets: elif self.loose_input_sockets:
self.loose_input_sockets = {} self.loose_input_sockets = {}
@ -151,18 +161,18 @@ class FilterMathNode(base.MaxwellSimNode):
input_socket_kinds={'Data': {ct.FlowKind.LazyValueFunc, ct.FlowKind.Info}}, input_socket_kinds={'Data': {ct.FlowKind.LazyValueFunc, ct.FlowKind.Info}},
) )
def compute_data(self, props: dict, input_sockets: dict): def compute_data(self, props: dict, input_sockets: dict):
# Retrieve Inputs
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]
# Determine Bound/Free Parameters # Compute Bound/Free Parameters
if props['dim'] in info.dim_names: func_args = [int] if props['active_socket_set'] == 'By Dim Value' else []
if props['dim']:
axis = info.dim_names.index(props['dim']) axis = info.dim_names.index(props['dim'])
else: else:
msg = 'Dimension invalid' msg = 'Dimension cannot be empty'
raise ValueError(msg) raise ValueError(msg)
func_args = [int] if props['active_socket_set'] == 'By Dim Value' else []
# Select Function # Select Function
filter_func: typ.Callable[[jax.Array], jax.Array] = { filter_func: typ.Callable[[jax.Array], jax.Array] = {
'By Dim': {'SQUEEZE': lambda data: jnp.squeeze(data, axis)}, 'By Dim': {'SQUEEZE': lambda data: jnp.squeeze(data, axis)},
@ -189,8 +199,11 @@ class FilterMathNode(base.MaxwellSimNode):
}, },
) )
def compute_array(self, output_sockets: dict) -> ct.ArrayFlow: def compute_array(self, output_sockets: dict) -> ct.ArrayFlow:
# Retrieve Inputs
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]
# 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),
unit=None, ## TODO: Unit Propagation unit=None, ## TODO: Unit Propagation
@ -207,14 +220,18 @@ class FilterMathNode(base.MaxwellSimNode):
input_socket_kinds={'Data': ct.FlowKind.Info}, input_socket_kinds={'Data': ct.FlowKind.Info},
) )
def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow: def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow:
# Retrieve Inputs
info = input_sockets['Data'] info = input_sockets['Data']
if props['dim'] in info.dim_names: # Compute Bound/Free Parameters
## Empty Dimension -> Empty InfoFlow
if props['dim']:
axis = info.dim_names.index(props['dim']) axis = info.dim_names.index(props['dim'])
else: else:
return ct.InfoFlow() return ct.InfoFlow()
# Compute Axis # Compute Information
## Compute Info w/By-Operation Change to Dimensions
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'),
@ -228,6 +245,7 @@ class FilterMathNode(base.MaxwellSimNode):
}, },
) )
# Fallback to Empty InfoFlow
return ct.InfoFlow() return ct.InfoFlow()
@events.computes_output_socket( @events.computes_output_socket(
@ -238,20 +256,30 @@ class FilterMathNode(base.MaxwellSimNode):
input_socket_kinds={'Data': {ct.FlowKind.Info, ct.FlowKind.Params}}, input_socket_kinds={'Data': {ct.FlowKind.Info, ct.FlowKind.Params}},
input_sockets_optional={'Value': True}, input_sockets_optional={'Value': True},
) )
def compute_data_params(self, props: dict, input_sockets: dict) -> ct.ParamsFlow: def compute_composed_params(
self, props: dict, input_sockets: dict
) -> ct.ParamsFlow:
# Retrieve Inputs
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]
# Compute Composed Parameters
## -> Only operations that add parameters.
## -> A dimension must be selected.
## -> There must be an input value.
if ( if (
(props['active_socket_set'], props['operation']) (props['active_socket_set'], props['operation'])
in [ in [
('By Dim Value', 'FIX'), ('By Dim Value', 'FIX'),
] ]
and props['dim'] in info.dim_names and props['dim']
and input_sockets['Value'] is not None and input_sockets['Value'] is not None
): ):
# Compute IDX Corresponding to Value # Compute IDX Corresponding to Coordinate Value
## Aka. "indexing by a float" ## -> Each dimension declares a unit-aware real number at each index.
## -> "Value" is a unit-aware real number from loose input socket.
## -> This finds the dimensional index closest to "Value".
## Total Effect: Indexing by a unit-aware real number.
nearest_idx_to_value = info.dim_idx[props['dim']].nearest_idx_of( nearest_idx_to_value = info.dim_idx[props['dim']].nearest_idx_of(
input_sockets['Value'], require_sorted=True input_sockets['Value'], require_sorted=True
) )
@ -269,6 +297,3 @@ BL_REGISTER = [
FilterMathNode, FilterMathNode,
] ]
BL_NODES = {ct.NodeType.FilterMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)} BL_NODES = {ct.NodeType.FilterMath: (ct.NodeCategory.MAXWELLSIM_ANALYSIS_MATH)}
## TODO TODO Okay so just like, Value needs to be a Loose socket, events needs to be able to handle sets of kinds, the invalidator needs to be able to handle sets of kinds too. Given all that, we only need to propagate the output array unit; given all all that, we are 100% goddamn ready to fix that goddamn coordinate.

View File

@ -753,8 +753,8 @@ class MaxwellSimNode(bpy.types.Node):
Notes: Notes:
This can be an unpredictably heavy function, depending on the node graph topology. This can be an unpredictably heavy function, depending on the node graph topology.
Doesn't currently accept `LinkChanged` (->Output) events; rather, these propagate as `DataChanged` events. Doesn't accept `LinkChanged` events; they are translated to `DataChanged` on the socket.
**This may change** if it becomes important for the node to differentiate between "change in data" and "change in link". This is on purpose: It seems to be a bad idea to try and differentiate between "changes in data" and "changes in linkage".
Parameters: Parameters:
event: The event to report forwards/backwards along the node tree. event: The event to report forwards/backwards along the node tree.
@ -762,10 +762,12 @@ class MaxwellSimNode(bpy.types.Node):
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.
""" """
# Outflow Socket Kinds # Outflow Socket Kinds
## Something has happened, that much is for sure. ## -> Something has happened!
## Output methods might require invalidation of (outsck, FlowKind)s. ## -> The effect is yet to be determined...
## Whichever FlowKinds we do happen to invalidate, we should mark. ## -> We will watch for which kinds actually invalidate.
## This way, each FlowKind gets its own invalidation chain. ## -> ...Then ONLY propagate kinds that have an invalidated outsck.
## -> This way, kinds get "their own" invalidation chains.
## -> ...While still respecting "crossovers".
altered_socket_kinds = set() altered_socket_kinds = set()
# Invalidate Caches on DataChanged # Invalidate Caches on DataChanged
@ -869,7 +871,6 @@ class MaxwellSimNode(bpy.types.Node):
""" """
if hasattr(self, prop_name): if hasattr(self, prop_name):
# Invalidate UI BLField Caches # Invalidate UI BLField Caches
log.critical((prop_name, self.ui_blfields))
if prop_name in self.ui_blfields: if prop_name in self.ui_blfields:
setattr(self, prop_name, bl_cache.Signal.InvalidateCache) setattr(self, prop_name, bl_cache.Signal.InvalidateCache)

View File

@ -137,7 +137,7 @@ class ViewerNode(base.MaxwellSimNode):
props={'auto_plot'}, props={'auto_plot'},
) )
def on_changed_plot_preview(self, props): def on_changed_plot_preview(self, props):
if self.inputs['Any'].is_linked and props['auto_plot']: if props['auto_plot']:
self.trigger_event(ct.FlowEvent.ShowPlot) self.trigger_event(ct.FlowEvent.ShowPlot)
@events.on_value_changed( @events.on_value_changed(
@ -150,7 +150,7 @@ class ViewerNode(base.MaxwellSimNode):
# Remove Non-Repreviewed Previews on Close # Remove Non-Repreviewed Previews on Close
with node_tree.repreview_all(): with node_tree.repreview_all():
if self.inputs['Any'].is_linked and props['auto_3d_preview']: if props['auto_3d_preview']:
self.trigger_event(ct.FlowEvent.ShowPreview) self.trigger_event(ct.FlowEvent.ShowPreview)

View File

@ -712,9 +712,11 @@ class BLField:
kwargs_prop['options'].add('SKIP_SAVE') kwargs_prop['options'].add('SKIP_SAVE')
if self._str_cb is not None: if self._str_cb is not None:
kwargs_prop |= lambda _self, context, edit_text: self._safe_str_cb( kwargs_prop |= {
'search': lambda _self, context, edit_text: self._safe_str_cb(
_self, context, edit_text _self, context, edit_text
) )
}
## Path ## Path
elif AttrType is Path: elif AttrType is Path:
@ -845,6 +847,16 @@ class BLField:
self._cached_bl_property.__set__(bl_instance, Signal.InvalidateCache) self._cached_bl_property.__set__(bl_instance, Signal.InvalidateCache)
elif value == Signal.ResetStrSearch: elif value == Signal.ResetStrSearch:
# Set String to ''
## Prevents the presence of an invalid value not in the new search.
## -> Infinite recursion if we don't check current value for ''.
## -> May cause a hiccup (chains will trigger twice)
current_value = self._cached_bl_property.__get__(
bl_instance, bl_instance.__class__
)
if current_value != '':
self._cached_bl_property.__set__(bl_instance, '')
# Pop the Cached String Search Items # Pop the Cached String Search Items
## The next time Blender does a str search, it'll update. ## The next time Blender does a str search, it'll update.
self._str_cb_cache.pop(bl_instance.instance_id, None) self._str_cb_cache.pop(bl_instance.instance_id, None)