ui: Data socket UI is now spiffy.
parent
badadfbfc2
commit
7d944a704e
|
@ -2,4 +2,9 @@ import enum
|
|||
|
||||
|
||||
class Icon(enum.StrEnum):
|
||||
# Node Tree
|
||||
SimNodeEditor = 'MOD_SIMPLEDEFORM'
|
||||
|
||||
# Sockets
|
||||
ToggleSocketInfo = 'TRIA_DOWN'
|
||||
DataSocketOutput = 'HOLDOUT_OFF'
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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]))
|
||||
|
||||
|
||||
####################
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue