Skip to content

Panels

The dock panels and what they draw: build123d compounds in the scene, plotly figures and grouped tables.

femforge_workbench.panels

Panel components: one trame template per dock panel, built inside its DivLayout with state namespaced by panel id.

Modules:

  • artifacts –

    The artifact browser: the telemetry artifact index in a table, with one artifact's record, files and spec beside it.

  • browsers –

    The browsers workbench: the run index over one run's detail, beside the artifact index over one artifact's detail.

  • field_viewer –

    The field viewer: a controller that opens a run's VTU as a FieldScene, the controls that steer it, and the 3D and plotly 2D views that show it.

  • figure –

    The plotly panel: a figure that fills its dock tab, updated in place and bound to plotly events.

  • launcher –

    The launcher panel: the catalogue as cards that open docks and renderables and run and stop scripts.

  • log –

    The log panel: an xterm terminal fed line by line from the server loop, with a ring buffer replayed to clients that mount later.

  • log_browser –

    The log browser: the telemetry log frame narrowed in polars, and a live mode following this process's tap and every other process's journal.

  • monitors –

    The live monitors workbench: the run monitor beside the sweep monitor, over the log browser and the terminal as tabs.

  • outputs –

    The outputs panel: one toggle per declared output of a catalogue card, each shown by its kind, the whole set swapped by show().

  • run_monitor –

    The run monitor: the selected run's heartbeat, stage strip, convergence chart and a log tail, polled every second while visible.

  • runs –

    The run browser: the telemetry run index in a table, with one bundle's stages, results, files and retention beside it.

  • scene –

    The scene panels: one off-screen pyvista plotter each, mirrored in the browser by VTK.wasm or streamed by trame-rca, under a preset toolbar.

  • sweep_monitor –

    The sweep monitor: a sweep's progress, its points grouped by state with dead workers marked stalled, and the failures with their tracebacks.

  • table –

    The table panel: a TanStack table over a polars frame that sorts, filters, facets, hides and groups in the client, with the layout persisted per panel.

  • terminal –

    The terminal panel: an xterm attached to a local shell or an Elmer container through a pseudo-terminal bridged on the server loop.

Type Aliases:

  • Handler –

    A client event handler, called with the values its JS arguments evaluate to.

  • Listener –

    A state listener, called with the changed state as keyword arguments.

Classes:

  • Cls –

    Tailwind compositions the panels share; the strings appear literally so the stylesheet build scans them.

  • Shared –

    The state keys panels share: a selection made in one panel is read by the others.

  • ItemField –

    The keys of a picker item: the label shown, the value chosen and, optionally, whether it is offered but unavailable.

  • Carry –

    What a disposed panel leaves in legacy for the panel rebuilt in its place.

  • PanelComponent –

    A dock panel: its template is built once inside layout; state variables go through key.

Functions:

  • labelled –

    A caption beside the control built inside the returned field: a toolbar row has no room for a floating label.

  • chooser –

    A single-choice select over fixed values, bound to a state variable.

  • picker –

    A single-choice select over the {title, value} items a state variable holds, bound to another.

  • select_run –

    Publishes run_id as the shared selection; picking the selected run again still notifies every listener, so each one re-reads the run.

Attributes:

  • FILL –

    The style of a panel's root: a column filling the dock body.

  • TRIGGER_WIDTH –

    The default width of a toolbar select; a picker over long ids widens it.

  • ITEM –

    The loop variable a picker's items are named by.

FILL module-attribute

FILL = {'height': '100%', 'width': '100%', 'display': 'flex', 'flexDirection': 'column', 'minHeight': '0'}

The style of a panel's root: a column filling the dock body.

TRIGGER_WIDTH module-attribute

TRIGGER_WIDTH = 'w-36'

The default width of a toolbar select; a picker over long ids widens it.

ITEM module-attribute

ITEM = 'item'

The loop variable a picker's items are named by.

Listener

Listener = Callable[..., object]

A state listener, called with the changed state as keyword arguments.

Handler

Handler = Callable[..., object]

A client event handler, called with the values its JS arguments evaluate to.

Cls

Bases: StrEnum

Tailwind compositions the panels share; the strings appear literally so the stylesheet build scans them.

Shared

Bases: StrEnum

The state keys panels share: a selection made in one panel is read by the others.

ItemField

Bases: StrEnum

The keys of a picker item: the label shown, the value chosen and, optionally, whether it is offered but unavailable.

Carry

Bases: StrEnum

What a disposed panel leaves in legacy for the panel rebuilt in its place.

PanelComponent

PanelComponent(app: Workbench, id: str, title: str)

Bases: TrameComponent

A dock panel: its template is built once inside layout; state variables go through key.

Parameters:

  • app (Workbench) –

    The workbench shell the panel is built in.

  • id (str) –

    The panel id, which names its template and namespaces its state.

  • title (str) –

    The panel's tab title.

Methods:

  • build –

    Adds the panel's widgets to its layout.

  • key –

    A state variable or trigger name namespaced to this panel.

  • status –

    Reports in the shell's status line.

  • refresh –

    Pushes the template again after its widgets changed.

  • callback –

    A client event handler calling fn with the JS args, unregistered again when the panel is disposed.

  • listen –

    Registers a state listener like state.change, one the panel drops again when disposed.

  • dispose –

    Releases what the panel registered on the server once it left the dock: triggers, state listeners and controller slots.

  • succeed –

    Called on a panel rebuilt under the id of previous, already disposed, to carry over what a fresh build would lose.

  • on_visible –

    Called when dockview shows or hides the panel.

  • on_layout_changed –

    Called after the dock re-laid out its groups.

Attributes:

  • listeners (list[Listener]) –

    The state listeners registered through listen, dropped again by dispose.

  • bound (list[Handler]) –

    The callables handed to callback, whose triggers dispose unregisters.

  • legacy (dict[Carry, object]) –

    What dispose leaves for a successor; read by its succeed, never through the reloadable classes.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def __init__(self, app: "Workbench", id: str, title: str) -> None:
    """Builds the panel's template inside its own layout.

    Args:
        app: The workbench shell the panel is built in.
        id: The panel id, which names its template and namespaces its state.
        title: The panel's tab title.
    """
    self.listeners: list[Listener] = []
    """The state listeners registered through `listen`, dropped again by `dispose`."""
    self.bound: list[Handler] = []
    """The callables handed to `callback`, whose triggers `dispose` unregisters."""
    self.legacy: dict[Carry, object] = {}
    """What `dispose` leaves for a successor; read by its `succeed`, never through the reloadable classes."""

    super().__init__(app.server)
    self.app = app
    self.id = id
    self.title = title
    self.template_name = template_name(id)
    self.layout = DivLayout(app.server, template_name=self.template_name, style=FILL)

    with self.layout:
        _ = app.theme()
        self.build()

listeners instance-attribute

listeners: list[Listener] = []

The state listeners registered through listen, dropped again by dispose.

bound instance-attribute

bound: list[Handler] = []

The callables handed to callback, whose triggers dispose unregisters.

legacy instance-attribute

legacy: dict[Carry, object] = {}

What dispose leaves for a successor; read by its succeed, never through the reloadable classes.

build

build() -> None

Adds the panel's widgets to its layout.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def build(self) -> None:
    """Adds the panel's widgets to its layout."""
    raise NotImplementedError

key

key(name: str) -> str

A state variable or trigger name namespaced to this panel.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def key(self, name: str) -> str:
    """A state variable or trigger name namespaced to this panel."""
    return f"{self.id}_{name}".replace("-", "_")

status

status(message: str) -> None

Reports in the shell's status line.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def status(self, message: str) -> None:
    """Reports in the shell's status line."""
    self.app.status(message)

refresh

refresh() -> None

Pushes the template again after its widgets changed.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def refresh(self) -> None:
    """Pushes the template again after its widgets changed."""
    self.layout.flush_content()

callback

callback(fn: Handler, args: str | None = None) -> Callback

A client event handler calling fn with the JS args, unregistered again when the panel is disposed.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def callback(self, fn: Handler, args: str | None = None) -> react.Callback:
    """A client event handler calling `fn` with the JS `args`, unregistered again when the panel is disposed."""
    self.bound.append(fn)

    return react.Callback(fn, args=args)

listen

listen(*names: str) -> Callable[[L], L]

Registers a state listener like state.change, one the panel drops again when disposed.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def listen[L: Listener](self, *names: str) -> Callable[[L], L]:
    """Registers a state listener like `state.change`, one the panel drops again when disposed."""

    def register(listener: L) -> L:
        _ = self.state.change(*names)(listener)
        self.listeners.append(listener)

        return listener

    return register

dispose

dispose() -> None

Releases what the panel registered on the server once it left the dock: triggers, state listeners and controller slots.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def dispose(self) -> None:
    """Releases what the panel registered on the server once it left the dock: triggers, state listeners and controller slots."""
    self._unbind_annotated_methods()

    # trame offers no public removal of a listener, nor a list of the triggers it named for a widget's callbacks; its own unbind reaches into the same dicts.
    for registered in self.state._change_callbacks.values():
        registered[:] = [(fn, translator) for fn, translator in registered if fn not in self.listeners]

    owned = [fn for fn in self.ctrl._triggers_fn2name if getattr(fn, "__self__", None) is self or fn in self.bound]
    for fn in owned:
        _ = self.ctrl.trigger_unregister(fn)

    self.listeners.clear()
    self.bound.clear()

succeed

succeed(previous: PanelComponent) -> None

Called on a panel rebuilt under the id of previous, already disposed, to carry over what a fresh build would lose.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def succeed(self, previous: "PanelComponent") -> None:
    """Called on a panel rebuilt under the id of `previous`, already disposed, to carry over what a fresh build would lose."""

on_visible

on_visible(visible: bool) -> None

Called when dockview shows or hides the panel.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def on_visible(self, visible: bool) -> None:
    """Called when dockview shows or hides the panel."""

on_layout_changed

on_layout_changed() -> None

Called after the dock re-laid out its groups.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def on_layout_changed(self) -> None:
    """Called after the dock re-laid out its groups."""

labelled

labelled(label: str) -> Field

A caption beside the control built inside the returned field: a toolbar row has no room for a floating label.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def labelled(label: str) -> Field:
    """A caption beside the control built inside the returned field: a toolbar row has no room for a floating label."""
    field = Field(orientation="horizontal", classes="w-auto gap-1")
    with field:
        # A crowded app bar would otherwise break a two-word caption over two lines and grow the bar.
        FieldLabel(title(label), classes=f"{Cls.CAPTION} whitespace-nowrap")

    return field

chooser

chooser(model: str, values: Sequence[str], *, width: str = TRIGGER_WIDTH, disabled: bool = False, label: Callable[[str], str] = title) -> None

A single-choice select over fixed values, bound to a state variable.

Parameters:

  • model (str) –

    The state variable the choice is bound to.

  • values (Sequence[str]) –

    The values offered, in order.

  • width (str, default: TRIGGER_WIDTH ) –

    The trigger's Tailwind width class.

  • disabled (bool, default: False ) –

    Whether the select is disabled.

  • label (Callable[[str], str], default: title ) –

    Makes the text each value reads as; Title Case by default.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def chooser(model: str, values: Sequence[str], *, width: str = TRIGGER_WIDTH, disabled: bool = False, label: Callable[[str], str] = title) -> None:
    """A single-choice select over fixed values, bound to a state variable.

    Args:
        model: The state variable the choice is bound to.
        values: The values offered, in order.
        width: The trigger's Tailwind width class.
        disabled: Whether the select is disabled.
        label: Makes the text each value reads as; Title Case by default.
    """
    with Select(model=model, disabled=disabled):
        with SelectTrigger(size=TRIGGER_SIZE, classes=width):
            SelectValue()

        with SelectContent():
            for value in values:
                SelectItem(label(value), value=value)

picker

picker(model: str, items: str, *, placeholder: str = '', width: str = TRIGGER_WIDTH, disabled: bool = False) -> None

A single-choice select over the {title, value} items a state variable holds, bound to another.

Parameters:

  • model (str) –

    The state variable the choice is bound to.

  • items (str) –

    The state variable holding the items.

  • placeholder (str, default: '' ) –

    The text shown while nothing is chosen.

  • width (str, default: TRIGGER_WIDTH ) –

    The trigger's Tailwind width class.

  • disabled (bool, default: False ) –

    Whether the select is disabled.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def picker(model: str, items: str, *, placeholder: str = "", width: str = TRIGGER_WIDTH, disabled: bool = False) -> None:
    """A single-choice select over the `{title, value}` items a state variable holds, bound to another.

    Args:
        model: The state variable the choice is bound to.
        items: The state variable holding the items.
        placeholder: The text shown while nothing is chosen.
        width: The trigger's Tailwind width class.
        disabled: Whether the select is disabled.
    """
    with Select(model=model, disabled=disabled):
        with SelectTrigger(size=TRIGGER_SIZE, classes=width):
            SelectValue(placeholder=placeholder)

        with SelectContent(), react.For(items=items, name=ITEM):
            SelectItem(
                [react.Bind(f"{ITEM}.{ItemField.TITLE}")],
                value=react.Bind(f"{ITEM}.{ItemField.VALUE}"),
                disabled=react.Bind(f"{ITEM}.{ItemField.DISABLED}"),
            )

select_run

select_run(state: State, run_id: str) -> None

Publishes run_id as the shared selection; picking the selected run again still notifies every listener, so each one re-reads the run.

Source code in packages/femforge-workbench/src/femforge_workbench/panels/__init__.py
def select_run(state: State, run_id: str) -> None:
    """Publishes `run_id` as the shared selection; picking the selected run again still notifies every listener, so each one re-reads the run."""
    with state:
        state[Shared.SELECTED_RUN_ID] = run_id
        state.dirty(Shared.SELECTED_RUN_ID)

femforge_workbench.compound

A build123d Compound as a Renderable: instanced VTK actors with view-dependent silhouettes.

Classes:

  • CompoundRenderable –

    A Renderable over a build123d Compound; the push to the OCP CAD Viewer is opt-in through ocp_vscode or FEMFORGE_OCP_VSCODE.

CompoundRenderable

CompoundRenderable(compound: Compound, *, ocp_vscode: bool | None = None)

A Renderable over a build123d Compound; the push to the OCP CAD Viewer is opt-in through ocp_vscode or FEMFORGE_OCP_VSCODE.

Methods:

  • render –

    Draw the compound into plotter under a top-down parallel projection; the plotter keeps its own background and anti-aliasing.

Source code in packages/femforge-workbench/src/femforge_workbench/compound.py
def __init__(self, compound: b3d.Compound, *, ocp_vscode: bool | None = None) -> None:
    """Instances the compound, or a placeholder label when it is empty, and pushes it to the OCP CAD Viewer when asked."""

    def placeholder() -> b3d.Compound:
        """A red label standing in for a compound with nothing to draw."""
        text = b3d.Text(EMPTY_TEXT, font_size=EMPTY_FONT_SIZE, font=EMPTY_FONT)

        return b3d.Compound(children=[text], label=EMPTY_LABEL, color=b3d.Color(1, 0, 0, 1))

    self.compound = compound if compound.children else placeholder()  # pyright: ignore[reportUnknownMemberType]  # build123d ships no type information

    to_viewer = enabled(Setting.OCP_VSCODE) if ocp_vscode is None else ocp_vscode
    if to_viewer:
        try:
            from ocp_vscode import show  # pyright: ignore[reportUnknownVariableType]  # ocp_vscode ships no type information
        except ModuleNotFoundError as error:
            if error.name != "ocp_vscode":
                raise

            raise ModuleNotFoundError('Showing compounds in the OCP CAD Viewer needs the ocp extra: pip install "femforge-workbench[ocp]".', name="ocp_vscode") from error

        show(
            self.compound,
            edge_accuracy=VIEWER_EDGE_ACCURACY,
            render_edges=True,
            tree_width=VIEWER_TREE_WIDTH,
            glass=False,
        )

    self.inst = Build123Instancer().parse(self.compound)

render

render(plotter: Plotter) -> None

Draw the compound into plotter under a top-down parallel projection; the plotter keeps its own background and anti-aliasing.

Source code in packages/femforge-workbench/src/femforge_workbench/compound.py
def render(self, plotter: pv.Plotter) -> None:
    """Draw the compound into `plotter` under a top-down parallel projection; the plotter keeps its own background and anti-aliasing."""
    plotter.clear()
    plotter.suppress_rendering = True
    plotter.parallel_projection = True

    _ = self.inst.add_to_plotter(plotter)
    _ = self.inst.add_silhouettes(plotter)

    plotter.view_xy()  # pyright: ignore[reportCallIssue, reportUnknownMemberType]  # pyvista forwards the renderer method onto the plotter at runtime
    plotter.reset_camera()  # pyright: ignore[reportCallIssue, reportUnknownMemberType]

    plotter.suppress_rendering = False
    plotter.render()

femforge_workbench.b3d_pv

Instanced VTK actors for a build123d Compound, with silhouettes and feature edges drawn over them.

Classes:

  • Build123Instancer –

    Converts a build123d Compound hierarchy into instanced VTK actors: each unique shape meshed once, every occurrence an actor with its own world matrix and color.

Build123Instancer

Build123Instancer(tolerance: float = 0.1, angular_tolerance: float = 0.5, default_color: tuple[float, float, float] = (0.9137, 0.6941, 0.1529))

Converts a build123d Compound hierarchy into instanced VTK actors: each unique shape meshed once, every occurrence an actor with its own world matrix and color.

Parameters:

  • tolerance (float, default: 0.1 ) –

    The linear deflection the shapes are meshed to.

  • angular_tolerance (float, default: 0.5 ) –

    The angular deflection the shapes are meshed to, in radians.

  • default_color (tuple[float, float, float], default: (0.9137, 0.6941, 0.1529) ) –

    The RGB color of a shape that carries none.

Methods:

  • parse –

    Replaces the actors with one per leaf of obj's hierarchy.

  • add_to_plotter –

    Adds every actor to plotter.

  • show –

    Shows the actors in a new plotter window, passing kwargs to its show.

  • add_silhouettes –

    Adds a view-dependent silhouette per instance, recomputed on every camera move, so add_feature_edges suits large assemblies better.

  • add_feature_edges –

    Adds each instance's crease and boundary edges, computed once per unique shape; smooth bodies show little.

Source code in packages/femforge-workbench/src/femforge_workbench/b3d_pv.py
def __init__(
    self,
    tolerance: float = 0.1,
    angular_tolerance: float = 0.5,
    default_color: tuple[float, float, float] = (0.9137, 0.6941, 0.1529),
):
    """Sets the meshing tolerances and the color of a shape that carries none.

    Args:
        tolerance: The linear deflection the shapes are meshed to.
        angular_tolerance: The angular deflection the shapes are meshed to, in radians.
        default_color: The RGB color of a shape that carries none.
    """
    self.tolerance = tolerance
    self.angular_tolerance = angular_tolerance
    self.default_color = tuple(default_color)

    # TShape -> shared vtkPolyDataMapper (None if the shape produced no geometry)
    self._mapper_cache: dict = {}
    # canonical location-free PolyData per unique mapper, kept alive for export/picking
    self._geom: dict = {}
    # canonical-mesh id -> shared feature-edge mapper (see add_feature_edges)
    self._edge_mapper_cache: dict = {}
    # one actor per leaf occurrence
    self.actors: list[vtk.vtkActor] = []

parse

parse(obj: Compound) -> Build123Instancer

Replaces the actors with one per leaf of obj's hierarchy.

Source code in packages/femforge-workbench/src/femforge_workbench/b3d_pv.py
def parse(self, obj: "b3d.Compound") -> "Build123Instancer":
    """Replaces the actors with one per leaf of `obj`'s hierarchy."""
    self.actors.clear()
    self._walk(obj, np.eye(4))
    return self

add_to_plotter

add_to_plotter(plotter: Plotter) -> Plotter

Adds every actor to plotter.

Source code in packages/femforge-workbench/src/femforge_workbench/b3d_pv.py
def add_to_plotter(self, plotter: pv.Plotter) -> pv.Plotter:
    """Adds every actor to `plotter`."""
    for actor in self.actors:
        plotter.add_actor(actor)
    return plotter

show

show(**kwargs) -> Plotter

Shows the actors in a new plotter window, passing kwargs to its show.

Source code in packages/femforge-workbench/src/femforge_workbench/b3d_pv.py
def show(self, **kwargs) -> pv.Plotter:
    """Shows the actors in a new plotter window, passing `kwargs` to its show."""
    pl = pv.Plotter()
    self.add_to_plotter(pl)
    pl.show(**kwargs)
    return pl

add_silhouettes

add_silhouettes(plotter: Plotter, color: str | tuple[float, float, float] = 'black', line_width: float = 1.5, opacity: float = 1.0, feature_angle: float | None = 70.0, border_edges: bool = True) -> list[vtkActor]

Adds a view-dependent silhouette per instance, recomputed on every camera move, so add_feature_edges suits large assemblies better.

Parameters:

  • plotter (Plotter) –

    The plotter to add the silhouettes to.

  • color (str | tuple[float, float, float], default: 'black' ) –

    The line color.

  • line_width (float, default: 1.5 ) –

    The line width, in pixels.

  • opacity (float, default: 1.0 ) –

    The line opacity.

  • feature_angle (float | None, default: 70.0 ) –

    The angle above which a crease draws too, in degrees; None draws the rim alone.

  • border_edges (bool, default: True ) –

    Draws the mesh's border edges too.

Source code in packages/femforge-workbench/src/femforge_workbench/b3d_pv.py
def add_silhouettes(
    self,
    plotter: pv.Plotter,
    color: str | tuple[float, float, float] = "black",
    line_width: float = 1.5,
    opacity: float = 1.0,
    feature_angle: "float | None" = 70.0,  # None -> pure view rim, no crease edges
    border_edges: bool = True,
) -> list[vtk.vtkActor]:
    """Adds a view-dependent silhouette per instance, recomputed on every camera move, so `add_feature_edges` suits large assemblies better.

    Args:
        plotter: The plotter to add the silhouettes to.
        color: The line color.
        line_width: The line width, in pixels.
        opacity: The line opacity.
        feature_angle: The angle above which a crease draws too, in degrees; None draws the rim alone.
        border_edges: Draws the mesh's border edges too.
    """
    cam = plotter.renderer.GetActiveCamera()
    col = pv.Color(color).float_rgb
    sil_actors: list[vtk.vtkActor] = []

    for actor in self.actors:
        mesh = actor.GetMapper().GetInput()  # shared canonical PolyData

        sil = vtk.vtkPolyDataSilhouette()
        sil.SetInputData(mesh)
        sil.SetCamera(cam)
        sil.SetProp3D(actor)
        sil.SetBorderEdges(border_edges)
        if feature_angle is None:
            sil.SetEnableFeatureAngle(0)
        else:
            sil.SetEnableFeatureAngle(1)
            sil.SetFeatureAngle(feature_angle)

        smap = vtk.vtkPolyDataMapper()
        smap.SetInputConnection(sil.GetOutputPort())
        smap.ScalarVisibilityOff()

        sa = vtk.vtkActor()
        sa.SetMapper(smap)
        sa.SetUserMatrix(actor.GetUserMatrix())  # silhouette output is in local frame
        prop = sa.GetProperty()
        prop.SetColor(*col)
        prop.SetLineWidth(line_width)
        prop.SetOpacity(opacity)
        prop.LightingOff()
        sa.SetPickable(False)

        plotter.add_actor(sa)
        sil_actors.append(sa)

    return sil_actors

add_feature_edges

add_feature_edges(plotter: Plotter, color: str | tuple[float, float, float] = 'black', line_width: float = 1.5, opacity: float = 1.0, feature_angle: float = 30.0, boundary_edges: bool = True) -> list[vtkActor]

Adds each instance's crease and boundary edges, computed once per unique shape; smooth bodies show little.

Parameters:

  • plotter (Plotter) –

    The plotter to add the edges to.

  • color (str | tuple[float, float, float], default: 'black' ) –

    The line color.

  • line_width (float, default: 1.5 ) –

    The line width, in pixels.

  • opacity (float, default: 1.0 ) –

    The line opacity.

  • feature_angle (float, default: 30.0 ) –

    The angle above which a crease draws, in degrees.

  • boundary_edges (bool, default: True ) –

    Draws the mesh's boundary edges too.

Source code in packages/femforge-workbench/src/femforge_workbench/b3d_pv.py
def add_feature_edges(
    self,
    plotter: pv.Plotter,
    color: str | tuple[float, float, float] = "black",
    line_width: float = 1.5,
    opacity: float = 1.0,
    feature_angle: float = 30.0,
    boundary_edges: bool = True,
) -> list[vtk.vtkActor]:
    """Adds each instance's crease and boundary edges, computed once per unique shape; smooth bodies show little.

    Args:
        plotter: The plotter to add the edges to.
        color: The line color.
        line_width: The line width, in pixels.
        opacity: The line opacity.
        feature_angle: The angle above which a crease draws, in degrees.
        boundary_edges: Draws the mesh's boundary edges too.
    """
    col = pv.Color(color).float_rgb
    edge_actors: list[vtk.vtkActor] = []

    for actor in self.actors:
        mesh = actor.GetMapper().GetInput()
        key = id(mesh)  # one shared canonical mesh per unique TShape

        edge_mapper = self._edge_mapper_cache.get(key)
        if edge_mapper is None:
            fe = vtk.vtkFeatureEdges()
            fe.SetInputData(mesh)
            fe.SetBoundaryEdges(boundary_edges)
            fe.FeatureEdgesOn()
            fe.SetFeatureAngle(feature_angle)
            fe.NonManifoldEdgesOff()
            fe.ManifoldEdgesOff()
            fe.ColoringOff()
            fe.Update()
            edge_mapper = vtk.vtkPolyDataMapper()
            edge_mapper.SetInputData(fe.GetOutput())  # bake once, not a live pipeline
            edge_mapper.ScalarVisibilityOff()
            self._edge_mapper_cache[key] = edge_mapper

        ea = vtk.vtkActor()
        ea.SetMapper(edge_mapper)
        ea.SetUserMatrix(actor.GetUserMatrix())
        prop = ea.GetProperty()
        prop.SetColor(*col)
        prop.SetLineWidth(line_width)
        prop.SetOpacity(opacity)
        prop.LightingOff()
        ea.SetPickable(False)

        plotter.add_actor(ea)
        edge_actors.append(ea)

    return edge_actors

femforge_workbench.plotly

Figure conventions in the dock: the tab carries the title on screen, layout.meta.title names the figure for export.

Functions:

  • layout_json –

    The figure's layout as plain JSON.

  • title_of –

    The name a figure gives itself: layout.meta.title, else its on-screen title, else empty.

  • export_options –

    The modebar's toImageButtonOptions: a 2x PNG named after the figure's title.

  • with_title –

    A copy of the figure carrying its export title centred at the top, with the margin that needs.

layout_json

layout_json(figure: Figure) -> Json

The figure's layout as plain JSON.

Source code in packages/femforge-workbench/src/femforge_workbench/plotly.py
def layout_json(figure: go.Figure) -> Json:
    """The figure's layout as plain JSON."""
    encoded = cast("Json", figure.to_plotly_json())
    layout = encoded.get("layout")
    if not isinstance(layout, Mapping):
        return {}

    return dict(cast("Mapping[str, object]", layout))

title_of

title_of(figure: Figure) -> str

The name a figure gives itself: layout.meta.title, else its on-screen title, else empty.

Source code in packages/femforge-workbench/src/femforge_workbench/plotly.py
def title_of(figure: go.Figure) -> str:
    """The name a figure gives itself: layout.meta.title, else its on-screen title, else empty."""
    layout = layout_json(figure)
    named = text(section(layout, "meta"), "title", "")
    if named:
        return named

    title = layout.get("title")
    plain = title if isinstance(title, str) else ""

    return text(section(layout, "title"), "text", plain)

export_options

export_options(figure: Figure) -> Json

The modebar's toImageButtonOptions: a 2x PNG named after the figure's title.

Source code in packages/femforge-workbench/src/femforge_workbench/plotly.py
def export_options(figure: go.Figure) -> Json:
    """The modebar's toImageButtonOptions: a 2x PNG named after the figure's title."""
    stem = re.sub(r"[^A-Za-z0-9._-]+", "-", title_of(figure)).strip("-")

    return {"format": EXPORT_FORMAT, "scale": EXPORT_SCALE, "filename": stem or DEFAULT_NAME}

with_title

with_title(figure: Figure) -> Figure

A copy of the figure carrying its export title centred at the top, with the margin that needs.

Source code in packages/femforge-workbench/src/femforge_workbench/plotly.py
def with_title(figure: go.Figure) -> go.Figure:
    """A copy of the figure carrying its export title centred at the top, with the margin that needs."""
    text = title_of(figure)
    titled = go.Figure(figure)
    if not text:
        return titled

    titled.update_layout(title={"text": text, "x": 0.5, "xanchor": "center"}, margin={"t": TITLED_TOP_MARGIN})

    return titled

femforge_workbench.plotting

Figure helpers: the regular grid a scatter of samples needs before it can draw as a surface.

Functions:

  • xyz_to_meshgrid –

    Cubic-interpolates scattered (x, y, z) samples onto a regular grid for surface plotting.

xyz_to_meshgrid

xyz_to_meshgrid(x: PlotData, y: PlotData, z: PlotData, grid_points: int = 100) -> tuple[PlotData, PlotData, PlotData]

Cubic-interpolates scattered (x, y, z) samples onto a regular grid for surface plotting.

Parameters:

  • x (PlotData) –

    The samples' x coordinates.

  • y (PlotData) –

    The samples' y coordinates.

  • z (PlotData) –

    The sample values.

  • grid_points (int, default: 100 ) –

    The grid's point count along each axis.

Source code in packages/femforge-workbench/src/femforge_workbench/plotting.py
def xyz_to_meshgrid(x: PlotData, y: PlotData, z: PlotData, grid_points: int = 100) -> tuple[PlotData, PlotData, PlotData]:
    """Cubic-interpolates scattered (x, y, z) samples onto a regular grid for surface plotting.

    Args:
        x: The samples' x coordinates.
        y: The samples' y coordinates.
        z: The sample values.
        grid_points: The grid's point count along each axis.
    """
    from scipy.interpolate import griddata  # pyright: ignore[reportUnknownVariableType]

    grid_x = np.linspace(min(x), max(x), grid_points)
    grid_y = np.linspace(min(y), max(y), grid_points)
    x2, y2 = np.meshgrid(grid_x, grid_y)
    grid_z = cast(
        np.ndarray,
        griddata(
            points=(x, y),
            values=z,
            xi=(x2, y2),
            method="cubic",
        ),
    )
    return grid_x, grid_y, grid_z

femforge_workbench.grid

Grouped rows on polars frames: a composite label per group for the tables to group by.

Type Aliases:

  • Row –

    A table row: each column's value by name.

Functions:

  • group_rows –

    The frame with a group label per by value: the value, the first of each first column, the row count and the maximum of each max_ column.

Attributes:

  • GROUP_COLUMN –

    The column group_rows adds and the tables group by; reserved in every frame a table shows.

GROUP_COLUMN module-attribute

GROUP_COLUMN = 'group'

The column group_rows adds and the tables group by; reserved in every frame a table shows.

Row

Row = dict[str, object]

A table row: each column's value by name.

group_rows

group_rows(frame: DataFrame, by: str, first: Sequence[str] = (), max_: Sequence[str] = ()) -> DataFrame

The frame with a group label per by value: the value, the first of each first column, the row count and the maximum of each max_ column.

Parameters:

  • frame (DataFrame) –

    The rows to label.

  • by (str) –

    The column whose values form the groups.

  • first (Sequence[str], default: () ) –

    Columns whose first value per group joins the label.

  • max_ (Sequence[str], default: () ) –

    Columns whose maximum per group joins the label.

Source code in packages/femforge-workbench/src/femforge_workbench/grid.py
def group_rows(frame: pl.DataFrame, by: str, first: Sequence[str] = (), max_: Sequence[str] = ()) -> pl.DataFrame:
    """The frame with a group label per `by` value: the value, the first of each `first` column, the row count and the maximum of each `max_` column.

    Args:
        frame: The rows to label.
        by: The column whose values form the groups.
        first: Columns whose first value per group joins the label.
        max_: Columns whose maximum per group joins the label.
    """
    firsts = [pl.first(name).alias(f"first {name}") for name in first]
    maxima = [pl.max(name).alias(f"max {name}") for name in max_]
    groups = frame.group_by(by, maintain_order=True).agg(*firsts, *maxima, pl.len().alias("count"))

    parts: list[pl.Expr] = [pl.col(by).cast(pl.String)]
    parts.extend(pl.col(f"first {name}").cast(pl.String) for name in first)
    parts.append(pl.format("{} rows", pl.col("count")))
    parts.extend(pl.format("max " + name + " {}", pl.col(f"max {name}").round(DECIMALS)) for name in max_)
    labelled = groups.select(pl.col(by), pl.concat_str(parts, separator=LABEL_SEPARATOR).alias(GROUP_COLUMN))

    return frame.join(labelled, on=by, how="left", maintain_order="left")