FerriteViz.jl v0.3.0 — a ParaView-style pipeline for Ferrite.jl results
I’m happy to announce FerriteViz.jl v0.3.0, a full rewrite of the internals of the visualization package for Ferrite.jl.
The short version: plotting a finite element result is no longer “call a recipe with a pile of keyword arguments”. It follows the model ParaView uses — a source holds your solution, filters transform it, and representations draw the result:
FEData(dh, u) |> filter |> filter |> solutionplot(...)
source transformations representation
using Ferrite, FerriteViz, GLMakie
dh, u = solve_problem()
ds = FEData(dh, u) # the source
solutionplot(ds) # plot it
solutionplot(ds |> WarpByVector(:u) |> Gradient(:u) |> VonMises(); color = :vonMises)
Filters map an FEData to an FEData, so they compose freely, and the whole chain stays reactive: FerriteViz.update! anywhere in a pipeline propagates through every filter into every open plot — which is what makes live plotting during a solve work.
One pipeline, three stages, on a plastified cantilever (J2 plasticity, trilinear hexahedra). (a) the raw source, (b) warped by the displacement field, (c) plastic work density built from two quadrature point quantities. All three panels share one bounding box. In (c) the white lines are the FE element outlines, the coloured patches the quadrature point partition — see below.
Filters compose, and derived quantities are first class
Gradient turns a field into its piecewise discontinuous gradient, and Derive maps that through an arbitrary function — which is where your constitutive law goes. Because the pipeline carries named arrays, a quantity you solved for and one you derived live in the same dataset and can be plotted side by side:
mixed = FEData(dh, u) |>
Gradient(:u; copy_fields = [:p]) |>
Derive(∇u -> vonmises(stress(∇u)); input = :gradient, output = :σvM)
solutionplot(mixed; color = :p) # solved for
solutionplot(mixed; color = :σvM) # derived
Cook’s membrane, mixed displacement/pressure formulation (Q2/Q1). The visible faceting in
(c) is not an artifact to be ashamed of — the gradient is kept element-wise discontinuous instead of being L2-projected onto a nodal field, because those inter-element jumps are one of the better indicators of an under-resolved discretization. Colour limits in (c) are clipped at the 90th percentile; the clamped corner is singular.
Filters currently shipping: WarpByVector, Gradient, CrinkleClip, Refine,
FirstOrderRefinement, ExtractComponent, Magnitude, Norm1, VonMises, Deviator, Threshold, Derive, AddQuadraturePointData.
Internal variables without averaging or smoothing
This is the feature I’d most like people to try. Internal variables — plastic strain, damage, stress in a history-dependent material — are known only at the quadrature points, with no interpolation defining them anywhere else. The two usual ways of drawing them both destroy information: averaging per cell throws away the sub-element variation, and projecting onto a nodal field invents smoothness that smears out exactly the localization you were looking for.
AddQuadraturePointData instead partitions every cell into the Voronoi regions of its quadrature points and fills each region with that point’s value.
A localisation band sampled at 2×2 quadrature points per cell on a 12×12 mesh. (a) cell average, (b) the quadrature point partition, (c) the underlying field. Same mesh, same data, same colour limits.
The result is ordinary point data, so it feeds straight back into the rest of the pipeline. Two such filters sharing a quadrature rule produce the same vertex layout, which is what lets their arrays be combined afterwards — for instance into a plastic work density:
ds |> AddQuadraturePointData(qr, states; extract = s -> s.σ, output = :σ) |>
AddQuadraturePointData(qr, states; extract = s -> s.ϵᵖ, output = :εᵖ) |>
Derive((σ, εᵖ) -> σ ⊡ εᵖ; input = [:σ, :εᵖ], output = :wᵖ) |>
WarpByVector(:u, 2.0)
extract pulls the quantity out of your material state struct, so states can be handed
over exactly as it comes out of your solver.
High order fields
High order solutions are still resolved by refinement rather than by curved rendering, but
Refine now improves the geometry too, not just the solution:
A Q2 heat problem, clipped open with CrinkleClip to show the interior. (a) flattening the quadratic ansatz onto the element corners produces the star-shaped artifact; (b) and (c) progressively resolve it. FirstOrderRefinement is the cheaper alternative that replaces the high order field by a first order one spanned by its nodes.
The viewer is composable now
ferriteviewer(ds) still gives you the familiar single panel with menus and toggles, but it is no longer hard-wired. The whole thing is described declaratively with Makie.SpecApi: a layout(ds, state) -> GridLayoutSpec hook, pluggable Controls that feed the view state, and spec helpers that assemble panels.
function twopanels(ds, state)
disp = solutionplotspec(ds; color = :default, colormap = state.colormap)
pres = solutionplotspec(ds; color = :p, colormap = state.colormap)
return S.GridLayout([
panelspec(disp; colorbar = disp, dim = 2, axis = (; title = "displacement magnitude"))
panelspec(pres; colorbar = pres, dim = 2, axis = (; title = "pressure"))
])
end
ferriteviewer(ds; layout = twopanels, controls = [ColormapMenu(), DeformationToggle()])
Structural state (colormap, which panels exist, anything the layout reads) re-diffs the spec, so Makie updates only the attributes of the plots it reuses. Data streaming — update! and the deformation scale — bypasses the spec entirely and mutates the shared GPU buffers, which is why live plotting stays cheap.
All Makie backends
GLMakie for interactive work, WGLMakie in Pluto/Jupyter, CairoMakie for vector graphics.
The same pipeline code, the same figure:
CairoMakie in particular is working again: its mesh path cannot accept the ShaderAbstractions.Buffers the pipeline shares with the GPU, so the representations now unwrap them to plain arrays when CairoMakie is the active backend. Thanks to PeturBryde for diagnosing and fixing that one (#146, fixes #118).
Timings
Uniform hexahedral grids, vector-valued Q1 field, everything measured after warm-up so no
compilation time is included (Julia 1.12, single machine, GLMakie):
| cells |
dofs |
triangles |
FEData(dh,u) |
Gradient|>Derive |
first solutionplot |
update! with an open plot |
| 1 000 |
3 993 |
24 000 |
0.04 s |
0.13 s |
0.04 s |
3.8 ms |
| 8 000 |
27 783 |
192 000 |
0.34 s |
1.18 s |
0.37 s |
26 ms |
| 27 000 |
89 373 |
648 000 |
1.24 s |
4.26 s |
1.20 s |
74 ms |
| 64 000 |
206 763 |
1 536 000 |
2.79 s |
10.0 s |
3.09 s |
144 ms |
The last column is the one that matters for live plotting: pushing a new solution vector into an open plot costs milliseconds, because it streams through the shared buffers instead of rebuilding anything. Setting up the tessellation is the one-off cost. Gradient |> Derive is the expensive filter — it evaluates your constitutive function at every tessellation vertex — and is worth applying downstream of a CrinkleClip if you only want to look at a cut.
Breaking changes
v0.3.0 is a breaking release. The main renames:
| before |
now |
MakiePlotter(dh, u) |
FEData(dh, u) |
solutionplot(p, field = :p) |
solutionplot(ds, color = :p) |
solutionplot(p, deformation_field = :u) |
solutionplot(ds |> WarpByVector(:u)) |
solutionplot(p, field = :gradient, process = f) |
solutionplot(ds |> Gradient(:u) |> Derive(f, output = :name), color = :name) |
crinkle_clip(!) |
CrinkleClip filter |
uniform_refinement |
Refine |
for_discretization |
FirstOrderRefinement |
arrows |
arrowplot |
wireframe |
meshplot |
FEData(dh, u) copies u, so update! no longer mutates your solution vector. transfer_solution’s process keyword, postprocess, x₁/x₂/x₃ and l1/l2 are gone — use the derivation filters instead.
Adding your own cell type is now a single method, reference_tessellation(::Type{<:AbstractRefShape}); Wedge and Pyramid work out of the box, and there is a worked example for cohesive/interface elements in the docs.
Links
Feedback, bug reports and feature requests very welcome — especially from anyone with history-dependent material models, since the quadrature point machinery is new and I’d like to know how it holds up on real problems.