Date: 2026-04-06 Status: Known limitation. The mathematics is correct; only recovered nodal output is affected. On curved shells, this includes nodal H and B inside the layers (§2.5), not just J/Jc. Module: src/fem/maxwell and src/mesh Related:
In a thin-shell stack with adjacent layers of different materials (e.g. hastelloy / ybco), the H-formulation DOFs are decoupled correctly across the layer interface — edges and faces are duplicated, ghost facets are inserted, and the Nitsche-DG-Ghost penalty couples the two layers only weakly. The solver is fine.
However, the geometric nodes sitting on that layer interface are still shared between the two layer blocks. The Maxwell postprocessor performs nodal SPR recovery and writes its outputs into a global per-node field. As a result, fields that are only meaningful on one layer (e.g. J/Jc on the YBCO layer) appear to "leak" onto the other layer (e.g. Hastelloy) when viewed in ParaView, even though no leakage actually exists in the assembled system.
This document explains the trace, why we are deliberately not fixing it right now, and what a clean fix would look like when we revisit.
Setup (from cmake-build-debug/input.conf):
Two tape thin shells, each with one Hastelloy layer block and one YBCO layer block. After running, the recovered J/Jc field is non-zero on the Hastelloy-layer side of the layer interface — exactly at the row of nodes that sits between Hastelloy and YBCO.
For this flat stack, the H field, B field, and assembled DOF system show no equivalent contamination. This does not extend to curved shells: there, the nodal H and B fields inside the layers are also wrong (§2.5).
ThinShellFactory::create_nodes_on_layers (src/mesh/cl_ThinShellFactory.cpp:980-1134) allocates one Cell<Node*> per layer, in Layer::Nodes. The Layer struct itself (src/mesh/cl_ThinShellFactory.hpp:35-44) only carries Edges, EdgeDuplicates, Faces, FaceDuplicates, and GhostFacets. There is no NodeDuplicates.
When element blocks are built (e.g. create_elements_on_blocks_line2 at src/mesh/cl_ThinShellFactory.cpp:1140-1177, and the analogous line3 / tri3 / tri6 variants), the bottom and top nodes of block b come straight from the layer node arrays:
For [hastelloy, ybco]:
| Block | bottom nodes | top nodes |
|---|---|---|
| 0 (hastelloy) | aLayers(0)->Nodes | aLayers(1)->Nodes |
| 1 (ybco) | aLayers(1)->Nodes | aLayers(2)->Nodes |
The two blocks share the literal Node* objects in aLayers(1)->Nodes.
Layer::hasDuplicates is set in src/mesh/cl_ThinShellFactory.cpp:186-194 whenever the materials of two adjacent layer blocks differ. Following the flag:
The Maxwell H-φ formulation puts conductor DOFs on edges (edge_h, plus face_h for higher-order) — see IWG_Maxwell field setup at src/fem/maxwell/cl_IWG_Maxwell.cpp:51. There are no nodal conductor DOFs in the assembled system, so the shared nodes do not introduce any extra coupling into K. The Nitsche-DG-Ghost penalty acts on the duplicated edges and behaves exactly as designed.
MaxwellPostprocessor::compute_superconductor_ts (src/fem/maxwell/cl_MaxwellPostprocessor.cpp:585-) computes J/Jc per integration point of each YBCO thin-shell element. Recovery happens through the generic Postprocessor SPR pipeline:
The leak does not come from the Hastelloy block being processed. It comes from the YBCO block writing into nodes that the Hastelloy block also points at. For the bottom row of YBCO elements those nodes are literally aLayers(1)->Nodes, which is the same memory the Hastelloy block uses for its top row. So JJCz(node_index) ends up non-zero at indices that ParaView colors on both sides.
The same mechanism applies to any per-node recovered field on a thin shell where the layer materials differ. J/Jc is just the most visually obvious because it is only meaningful on one side of the interface.
The transport current in a superconducting layer appears as a jump in tangential H across the layer thickness. The element preserves this variation: EF_PENTA6TS blends the bottom-face and top-face edge values linearly through the thickness (src/fem/interpolation/nedelec/cl_EF_PENTA6TS.cpp:221-224). Nodal recovery loses the jump on a curved shell. At order 1, recovery fits one polynomial in global Cartesian coordinates, {1, x, y, z} (src/fem/kernel/cl_FEM_Postprocessor.hpp:370-376), to the integration-point values from every selected element that touches the node, then evaluates that polynomial at the node (src/fem/kernel/cl_FEM_Postprocessor.cpp:1148).
The shared interface nodes from §2.4 then move the jump. The superconducting postprocessor is created after the normal-conductor postprocessor (src/fem/maxwell/cl_MaxwellFactory.cpp:2528-2544), and synch_target_fields assigns values rather than combining them. The mean values therefore overwrite the correct side-local values that the normal-conductor pass wrote on neighboring layer faces. In ParaView, the current sheet then appears in the two layers next to the superconductor, such as silver and buffer, while the superconducting layer itself shows almost no jump. The sum of the jumps through the whole stack remains correct because the per-layer jumps telescope to the outer face minus the inner face.
Measured on the five-tape rutherford example (4 mm tapes around a core with 0.9 mm edge radius), the jump on the flat faces sits in the YBCO layer and matches the sheet current (a B jump of μ0 J d) to about 1 %. On the bends, the YBCO layer carries an average of 0.5 % of that jump. Columns on the flat whose node patches reach the bend are affected as well.
The solve does not use these nodal fields. Assembly, the thermal Joule source, and dotQ all evaluate C q and E q at integration points. The affected outputs are the Exodus nodal fields and the J/Jc_max value printed in the step footer, which is taken from the nodal JJC*.
Fix: thin-shell layer blocks now use the shell coordinates (s₁, s₂, η), where η is the offset through the stack computed from each element's thickness coordinate (postprocessor_recovery_theory.md §2.1.2). On a flat patch, this basis spans the same space as the previous basis. On a bend, it preserves the jump. The shared-node overwrite described in §2.4 remains, but it now copies the correct face values.
The clean fix is to introduce Layer::NodeDuplicates as a sibling of EdgeDuplicates / FaceDuplicates, allocate them when hasDuplicates is true, and route the upper block's bottom row through the duplicates. That would also require:
We are deliberately not paying that cost today, because:
When we revisit (likely after the FVM thermal transition is in place), the work is:
src/mesh/cl_ThinShellFactory.hpp — extend the Layer struct:
create_elements_on_blocks_line2 / line3 / tri3 / tri6 — pick the bottom row the same way link_elements_with_edges already picks the bottom edges:
When the duplicate-node path is implemented, confirm:
This document records a joint Claude/Codex investigation on 2026-04-06. Confidence: high that this is a postprocessor visualization artifact and not a solver bug. See devlog/dl20260406_thinshell_postprocessor_trace.md and the corresponding entry in todo/ai_exchange.md for the audit trail.