ui: Data socket UI is now spiffy.

main
Sofus Albert Høgsbro Rose 2024-04-25 09:56:21 +02:00
parent badadfbfc2
commit 7d944a704e
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
5 changed files with 237 additions and 37 deletions

View File

@ -2,4 +2,9 @@ import enum
class Icon(enum.StrEnum): class Icon(enum.StrEnum):
# Node Tree
SimNodeEditor = 'MOD_SIMPLEDEFORM' SimNodeEditor = 'MOD_SIMPLEDEFORM'
# Sockets
ToggleSocketInfo = 'TRIA_DOWN'
DataSocketOutput = 'HOLDOUT_OFF'

View File

@ -1,3 +1,5 @@
"""Declares `MapMathNode`."""
import enum import enum
import typing as typ import typing as typ
@ -19,7 +21,80 @@ X_COMPLEX = sp.Symbol('x', complex=True)
class MapMathNode(base.MaxwellSimNode): 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: Attributes:
operation: Operation to apply to the input. 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: def compute_data_info(self, props: dict, input_sockets: dict) -> ct.InfoFlow:
info = input_sockets['Data'] info = input_sockets['Data']
if ct.FlowSignal.check(info):
return ct.FlowSignal.FlowPending
# Complex -> Real # Complex -> Real
if ( if props['active_socket_set'] == 'By Element':
props['active_socket_set'] == 'By Element' if props['operation'] in [
and props['operation']
in [
'REAL', 'REAL',
'IMAG', 'IMAG',
'ABS', 'ABS',
] ]:
and not ct.FlowSignal.check(info) return ct.InfoFlow(
): dim_names=info.dim_names,
return ct.InfoFlow( dim_idx=info.dim_idx,
dim_names=info.dim_names, output_names=info.output_names,
dim_idx=info.dim_idx, output_mathtypes={
output_names=info.output_names, output_name: (
output_mathtypes={ spux.MathType.Real
output_name: ( if output_mathtype == spux.MathType.Complex
spux.MathType.Real else output_mathtype
if output_mathtype == spux.MathType.Complex )
else output_mathtype for output_name, output_mathtype in info.output_mathtypes.items()
) },
for output_name, output_mathtype in info.output_mathtypes.items() output_units=info.output_units,
}, )
output_units=info.output_units, if props['active_socket_set'] == 'By Vector':
) pass
return info return info
@events.computes_output_socket( @events.computes_output_socket(

View File

@ -838,10 +838,11 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
node: The node within which the socket is embedded. node: The node within which the socket is embedded.
text: The socket's name in the UI. text: The socket's name in the UI.
""" """
col = layout.column(align=False) col = layout.column()
# Row: Label # Row: Label
row = col.row(align=False) row = col.row()
row.alignment = 'LEFT'
## Lock Check ## Lock Check
if self.locked: if self.locked:
@ -849,7 +850,7 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
## Link Check ## Link Check
if self.is_linked: if self.is_linked:
row.label(text=text) self.draw_input_label_row(row, text)
else: else:
# User Label Row (incl. Units) # User Label Row (incl. Units)
if self.use_units: if self.use_units:
@ -912,7 +913,10 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
col = layout.column() col = layout.column()
row = col.row() row = col.row()
row.alignment = 'RIGHT' 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 # Draw FlowKind.Info related Information
if self.use_info_draw: if self.use_info_draw:
@ -930,6 +934,8 @@ class MaxwellSimSocket(bpy.types.NodeSocket):
) -> None: ) -> None:
"""Draw the label row, which is at the same height as the socket shape. """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: Notes:
Can be overriden by individual socket classes, if they need to alter the way that the label row is drawn. 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) 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: def draw_value(self, col: bpy.types.UILayout) -> None:
"""Draws the socket value on its own line. """Draws the socket value on its own line.

View File

@ -1,13 +1,38 @@
import enum
import typing as typ import typing as typ
import bpy 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 blender_maxwell.utils import extra_sympy_units as spux
from ... import contracts as ct from ... import contracts as ct
from .. import base 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 # - Blender Socket
@ -23,6 +48,14 @@ class DataBLSocket(base.MaxwellSimSocket):
format: str = bl_cache.BLField('') format: str = bl_cache.BLField('')
## TODO: typ.Literal['xarray', 'jax'] ## 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 # - FlowKind
#################### ####################
@ -37,29 +70,64 @@ class DataBLSocket(base.MaxwellSimSocket):
#################### ####################
# - UI # - 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: 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() row = col.row()
box = row.box() box = row.box()
grid = box.grid_flow( grid = box.grid_flow(
columns=3, columns=len(self.info_columns) + 1,
row_major=True, row_major=True,
even_columns=True, even_columns=True,
# even_rows=True, # even_rows=True,
align=True, align=True,
) )
# Grid Header # Dimensions
# grid.label(text='Dim')
# grid.label(text='Len')
# grid.label(text='Unit')
# Dimension Names
for dim_name in info.dim_names: for dim_name in info.dim_names:
dim_idx = info.dim_idx[dim_name] dim_idx = info.dim_idx[dim_name]
grid.label(text=dim_name) grid.label(text=dim_name)
grid.label(text=str(len(dim_idx))) if DataInfoColumn.Length in self.info_columns:
grid.label(text=spux.sp_to_str(dim_idx.unit)) 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]))
#################### ####################

View File

@ -71,6 +71,16 @@ class MathType(enum.StrEnum):
MathType.Complex: complex, MathType.Complex: complex,
}[value] }[value]
@staticmethod
def to_str(value: typ.Self) -> type:
return {
MathType.Bool: 'T|F',
MathType.Integer: '',
MathType.Rational: '',
MathType.Real: '',
MathType.Complex: '',
}[value]
#################### ####################
# - Units # - Units