ui: Data socket UI is now spiffy.
parent
badadfbfc2
commit
7d944a704e
|
@ -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'
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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]))
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue