From 7d944a704ea7b2a693920d91ffe0495c4ca0fbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sofus=20Albert=20H=C3=B8gsbro=20Rose?= Date: Thu, 25 Apr 2024 09:56:21 +0200 Subject: [PATCH] ui: Data socket UI is now spiffy. --- .../maxwell_sim_nodes/contracts/icons.py | 5 + .../nodes/analysis/math/map_math.py | 119 ++++++++++++++---- .../maxwell_sim_nodes/sockets/base.py | 50 +++++++- .../maxwell_sim_nodes/sockets/basic/data.py | 90 +++++++++++-- .../utils/extra_sympy_units.py | 10 ++ 5 files changed, 237 insertions(+), 37 deletions(-) diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py index f0ef0f3..64483d8 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/icons.py @@ -2,4 +2,9 @@ import enum class Icon(enum.StrEnum): + # Node Tree SimNodeEditor = 'MOD_SIMPLEDEFORM' + + # Sockets + ToggleSocketInfo = 'TRIA_DOWN' + DataSocketOutput = 'HOLDOUT_OFF' diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py index 7681941..8752965 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/analysis/math/map_math.py @@ -1,3 +1,5 @@ +"""Declares `MapMathNode`.""" + import enum import typing as typ @@ -19,7 +21,80 @@ X_COMPLEX = sp.Symbol('x', complex=True) class MapMathNode(base.MaxwellSimNode): - """Applies a function by-structure to the data. + r"""Applies a function by-structure to the data. + + The shape, type, and interpretation of the input/output data is dynamically shown. + + # Socket Sets + The line between a "map" and a "filter" is generally a matter of taste. + In general, "map" provides "do something to each of x" operations. + + While it is often generally assumed that `numpy` broadcasting rules are well-known, dimensional data is inherently complicated. + Therefore, we choose an explicit, opinionated approach to "how things are mapped", prioritizing predictability over flexibility. + + ## By Element + Applies a function to each scalar number of the array. + + :::{.callout-tip title="Example"} + Say we have a standard `(50, 3)` array with a `float32` (`f32`) datatype. + We could interpret such an indexed structure as an **element map**: + + $$ + A:\,\,\underbrace{(\mathbb{Z}_{50}, \mathbb{Z}_3)}_{\texttt{(50,3)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}} + $$ + + `By Element` simply applies a function to each output value, $\mathbb{R}$, producing a new $A$ with the same dimensions. + Note that the datatype might be altered, ex. `\mathbb{C} \to \mathbb{R}`, as part of the function. + ::: + + + ## By Vector + Applies a function to each vector, the elements of which span the **last axis**. + + This **might** produce a well-known dimensionality change, depending on what each vector maps to. + + :::{.callout-tip title="Example"} + Let's build on the `By Element` example, by interpreting it as a list of column vectors, and taking the length of each. + + `By Vector` operates on the same data, but interpreted in a slightly deconstructed way: + + $$ + A:\,\,\underbrace{(\mathbb{Z}_{50})}_{\texttt{(50,)}} \to (\underbrace{(\mathbb{Z}_3)}_{\texttt{(3,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}) + $$ + + `By Vector` applies a function to each $\underbrace{(\mathbb{Z}_3)}_{\texttt{(3,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}$. + Applying a standard 2-norm + + $$ + ||\cdot||_2:\,\,\,\,(\underbrace{(\mathbb{Z}_3)}_{\texttt{(3,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}}) \to \underbrace{(\mathbb{R})}_{\texttt{f32}} + $$ + + to our $A$ results in a new, reduced-dimension array: + + $$ + A_{||\cdot||_2}:\,\,\underbrace{(\mathbb{Z}_{50})}_{\texttt{(50,)}} \to \underbrace{(\mathbb{R})}_{\texttt{f32}} + $$ + ::: + + + ## By Matrix + Applies a function to each matrix, the elements of which span the **last two axes**. + + This **might** produce a well-known dimensionality change, depending on what each matrix maps to. + + :::{.callout-tip title="Just Like Vectors"} + At this point, we reach 3D, and mental models become more difficult. + + When dealing with high-dimensional arrays, it is suggested to draw out the math, ex. with the explicit notation introduced earlier. + ::: + + ## Expr + Applies a user-sourced symbolic expression to a single symbol, with the symbol either representing (selectably) a single element, vector, or matrix. + The name and type of the available symbol is clearly shown, and most valid `sympy` expressions that you would expect to work, should work. + + Use of expressions generally imposes no performance penalty: Just like the baked-in operations, it is compiled to a high-performance `jax` function. + Thus, it participates in the `ct.FlowKind.LazyValueFunc` composition chain. + Attributes: operation: Operation to apply to the input. @@ -219,32 +294,32 @@ class MapMathNode(base.MaxwellSimNode): ) def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow: info = input_sockets['Data'] + if ct.FlowSignal.check(info): + return ct.FlowSignal.FlowPending # Complex -> Real - if ( - props['active_socket_set'] == 'By Element' - and props['operation'] - in [ + if props['active_socket_set'] == 'By Element': + if props['operation'] in [ 'REAL', 'IMAG', 'ABS', - ] - and not ct.FlowSignal.check(info) - ): - return ct.InfoFlow( - dim_names=info.dim_names, - dim_idx=info.dim_idx, - output_names=info.output_names, - output_mathtypes={ - output_name: ( - spux.MathType.Real - if output_mathtype == spux.MathType.Complex - else output_mathtype - ) - for output_name, output_mathtype in info.output_mathtypes.items() - }, - output_units=info.output_units, - ) + ]: + return ct.InfoFlow( + dim_names=info.dim_names, + dim_idx=info.dim_idx, + output_names=info.output_names, + output_mathtypes={ + output_name: ( + spux.MathType.Real + if output_mathtype == spux.MathType.Complex + else output_mathtype + ) + for output_name, output_mathtype in info.output_mathtypes.items() + }, + output_units=info.output_units, + ) + if props['active_socket_set'] == 'By Vector': + pass return info @events.computes_output_socket( 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 9208e36..e7df2b5 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 @@ -838,10 +838,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket): node: The node within which the socket is embedded. text: The socket's name in the UI. """ - col = layout.column(align=False) + col = layout.column() # Row: Label - row = col.row(align=False) + row = col.row() + row.alignment = 'LEFT' ## Lock Check if self.locked: @@ -849,7 +850,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket): ## Link Check if self.is_linked: - row.label(text=text) + self.draw_input_label_row(row, text) else: # User Label Row (incl. Units) if self.use_units: @@ -912,7 +913,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket): col = layout.column() row = col.row() row.alignment = 'RIGHT' - row.label(text=text) + if self.is_linked: + self.draw_output_label_row(row, text) + else: + row.label(text=text) # Draw FlowKind.Info related Information if self.use_info_draw: @@ -930,6 +934,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket): ) -> None: """Draw the label row, which is at the same height as the socket shape. + Will only display if the socket is an **unlinked input socket**. + Notes: Can be overriden by individual socket classes, if they need to alter the way that the label row is drawn. @@ -939,6 +945,42 @@ class MaxwellSimSocket(bpy.types.NodeSocket): """ row.label(text=text) + def draw_input_label_row( + self, + row: bpy.types.UILayout, + text: str, + ) -> None: + """Draw the label row, which is at the same height as the socket shape. + + Will only display if the socket is a **linked input socket**. + + Notes: + Can be overriden by individual socket classes, if they need to alter the way that the label row is drawn. + + Parameters: + row: Target for defining UI elements. + text: The socket's name in the UI. + """ + row.label(text=text) + + def draw_output_label_row( + self, + row: bpy.types.UILayout, + text: str, + ) -> None: + """Draw the output label row, which is at the same height as the socket shape. + + Will only display if the socket is an **output socket**. + + Notes: + Can be overriden by individual socket classes, if they need to alter the way that the output label row is drawn. + + Parameters: + row: Target for defining UI elements. + text: The socket's name in the UI. + """ + row.label(text=text) + def draw_value(self, col: bpy.types.UILayout) -> None: """Draws the socket value on its own line. diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/data.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/data.py index db53d9e..a8f5c03 100644 --- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/data.py +++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/basic/data.py @@ -1,13 +1,38 @@ +import enum import typing as typ import bpy -from blender_maxwell.utils import bl_cache +from blender_maxwell.utils import bl_cache, logger from blender_maxwell.utils import extra_sympy_units as spux from ... import contracts as ct from .. import base +log = logger.get(__name__) + + +class DataInfoColumn(enum.StrEnum): + Length = enum.auto() + MathType = enum.auto() + Unit = enum.auto() + + @staticmethod + def to_name(value: typ.Self) -> str: + return { + DataInfoColumn.Length: 'L', + DataInfoColumn.MathType: '∈', + DataInfoColumn.Unit: 'U', + }[value] + + @staticmethod + def to_icon(value: typ.Self) -> str: + return { + DataInfoColumn.Length: '', + DataInfoColumn.MathType: '', + DataInfoColumn.Unit: '', + }[value] + #################### # - Blender Socket @@ -23,6 +48,14 @@ class DataBLSocket(base.MaxwellSimSocket): format: str = bl_cache.BLField('') ## TODO: typ.Literal['xarray', 'jax'] + show_info_columns: bool = bl_cache.BLField( + False, + prop_ui=True, + ) + info_columns: DataInfoColumn = bl_cache.BLField( + {DataInfoColumn.MathType, DataInfoColumn.Length}, prop_ui=True, enum_many=True + ) + #################### # - FlowKind #################### @@ -37,29 +70,64 @@ class DataBLSocket(base.MaxwellSimSocket): #################### # - UI #################### + def draw_input_label_row(self, row: bpy.types.UILayout, text) -> None: + if self.format == 'jax': + row.label(text=text) + row.prop(self, self.blfields['info_columns']) + row.prop( + self, + self.blfields['show_info_columns'], + toggle=True, + text='', + icon=ct.Icon.ToggleSocketInfo, + ) + + def draw_output_label_row(self, row: bpy.types.UILayout, text) -> None: + if self.format == 'jax': + row.prop( + self, + self.blfields['show_info_columns'], + toggle=True, + text='', + icon=ct.Icon.ToggleSocketInfo, + ) + row.prop(self, self.blfields['info_columns']) + row.label(text=text) + def draw_info(self, info: ct.InfoFlow, col: bpy.types.UILayout) -> None: - if self.format == 'jax' and info.dim_names: + if self.format == 'jax' and info.dim_names and self.show_info_columns: row = col.row() box = row.box() grid = box.grid_flow( - columns=3, + columns=len(self.info_columns) + 1, row_major=True, even_columns=True, # even_rows=True, align=True, ) - # Grid Header - # grid.label(text='Dim') - # grid.label(text='Len') - # grid.label(text='Unit') - - # Dimension Names + # Dimensions for dim_name in info.dim_names: dim_idx = info.dim_idx[dim_name] grid.label(text=dim_name) - grid.label(text=str(len(dim_idx))) - grid.label(text=spux.sp_to_str(dim_idx.unit)) + if DataInfoColumn.Length in self.info_columns: + grid.label(text=str(len(dim_idx))) + if DataInfoColumn.MathType in self.info_columns: + grid.label(text=spux.MathType.to_str(dim_idx.mathtype)) + if DataInfoColumn.Unit in self.info_columns: + grid.label(text=spux.sp_to_str(dim_idx.unit)) + + # Outputs + for output_name in info.output_names: + grid.label(text=output_name) + if DataInfoColumn.Length in self.info_columns: + grid.label(text='', icon=ct.Icon.DataSocketOutput) + if DataInfoColumn.MathType in self.info_columns: + grid.label( + text=spux.MathType.to_str(info.output_mathtypes[output_name]) + ) + if DataInfoColumn.Unit in self.info_columns: + grid.label(text=spux.sp_to_str(info.output_units[output_name])) #################### diff --git a/src/blender_maxwell/utils/extra_sympy_units.py b/src/blender_maxwell/utils/extra_sympy_units.py index 14ad141..a3de641 100644 --- a/src/blender_maxwell/utils/extra_sympy_units.py +++ b/src/blender_maxwell/utils/extra_sympy_units.py @@ -71,6 +71,16 @@ class MathType(enum.StrEnum): MathType.Complex: complex, }[value] + @staticmethod + def to_str(value: typ.Self) -> type: + return { + MathType.Bool: 'T|F', + MathType.Integer: 'ℤ', + MathType.Rational: 'ℚ', + MathType.Real: 'ℝ', + MathType.Complex: 'ℂ', + }[value] + #################### # - Units