Skip to content

Add 2D boundary mode problem type and enhance waveport capabilities#657

Open
simlapointe wants to merge 68 commits intomainfrom
simlapointe/boundarymode-2d
Open

Add 2D boundary mode problem type and enhance waveport capabilities#657
simlapointe wants to merge 68 commits intomainfrom
simlapointe/boundarymode-2d

Conversation

@simlapointe
Copy link
Contributor

This PR adds a 2D boundary mode problem type along with multiple enhacements to waveports. Specifically:

Enable Palace to accept 2D meshes and run all five solver modes
(electrostatic, magnetostatic, eigenmode, driven, transient) with
proper finite element discretization.

Key changes:
- Add L2 FE space for scalar B-field (curl E is scalar in 2D) with
  proper ND->L2 discrete curl operator using MFEM native assembly
- Add Cross2 template for 2D cross product, dimension-aware
  coefficient classes for surface current and Poynting vector
- Generalize BoundingBox to support 2D (mfem::Vector/DenseMatrix),
  handle collinear point sets, fix pointmat out-of-bounds for 2D
- Add 2x2 MatrixFunction, truncate material tensors to sdim x sdim,
  add scalar CurlCurlInvPermeability for 2D curl-curl operator
- Use ElementToEdgeTable for 2D meshes, fix GetGeomTypes for 2D
- Add 3D-only guards for wave ports, Stratton-Chu, far-field
- Skip curl flux error estimator for 2D (B on L2, not RT)
- Add disc2d (electrostatic) and cavity2d (eigenmode) test examples
  with Gmsh mesh scripts and regression reference data
New example: a coplanar waveguide in top-down 2D view with two
dual-element lumped ports (trace-to-ground) at each end. Uses the
same dimensions as the 3D CPW (w=30 μm, s=18 μm, L=4000 μm).

The trace is modeled as an internal PEC boundary (cracked mesh) with
the ground planes extending to the domain boundaries. Each port has
two elements connecting the trace to the top and bottom ground planes.

Also fixes GetFaceVertices → GetEdgeVertices for 2D meshes in the
internal boundary cracking code (AddInterfaceBdrElements).
New example mirroring the 3D CPW geometry as an x-y slice. The port
patches are gap_width x gap_width squares in the gap at each end,
matching the 3D CPW lumped port geometry. Port direction is along the
edge (+X/-X).

The trace is boolean-cut from the domain (not meshed) because in 2D a
PEC region fully isolates the two gaps — there is no third dimension
for the field to wrap around the trace. The trace boundary becomes PEC.

Results show good CPW behavior: S11 = -15 dB at 2 GHz, half-wavelength
resonance at ~18 GHz, S21 near 0 dB at low frequency.

Also fixes:
- Collinear bounding box: add perpendicular axis with scaled length
  for direction alignment checks on 1D boundaries in 2D
- Relax projected length cross-check for 2D ports where the direction
  is perpendicular to the edge
Five new cavity2d examples testing each BC type on a rectangular
2D cavity (MFEM inline mesh, per-wall attribute control):

- PMC: explicit magnetic wall on right wall, eigenfrequency shift
  confirms half-integer mode structure
- Impedance: Rs = 1e6 Ω on right wall, finite Q from resistive loss
- Absorbing: 1st-order Silver-Muller ABC, low Q from radiation loss
- Conductivity: σ = 5.8e7 S/m (copper) on right wall, driven solver
  with lumped port excitation
- Periodic: simple periodic BC (left↔right), eigenfrequencies match
  parallel-plate waveguide cutoffs

All tests produce physically correct results and are registered in
runtests.jl with reference data for regression testing.
New example: CPW cross-section (transverse-vertical plane) with
sapphire substrate (ε_r=9.3) below and air above, matching the 3D
CPW geometry. Electrostatic solver computes capacitance, energy
distribution, surface charge, and dielectric participation ratios.

Validates all 2D postprocessing features:
- Domain energy: 90.38% in substrate (theory: 90.29%)
- Surface flux: electric charge on trace (TwoSided=true)
- Dielectric participation: SA, MS, MA interfaces with correct
  ordering (p_MS > p_MA, substrate-side field stronger)
- All p_surf > 0, all Q_surf > 0

The mesh uses MFEM native format with explicit internal boundary
elements at z=0 for the metal traces and substrate-air interface.
A Gmsh-based Julia script handles geometry and meshing, then writes
MFEM format directly (needed because MFEM's Gmsh reader does not
import internal 1D groups for 2D meshes).
The curl flux error estimator was previously skipped for 2D because
B lives on L2 (scalar), not RT (vector). This left the magnetostatic
error indicator at zero and the eigenmode/driven estimator incomplete.

Fix: for 2D, the curl error estimator uses H1 spaces (smooth scalar
flux) and L2 (raw scalar B), with scalar MassIntegrator instead of
VectorFEMassIntegrator, and a new scalar L2/H1 error qfunction.

Changes:
- New qfunction: l2h1_error_qf.h for scalar ||c1*u1 - c2*u2||^2
- FluxProjector: detect scalar FE spaces and use MassIntegrator
- CurlFluxErrorEstimator: use scalar coefficients (1x1 mu^-1) and
  the scalar qfunction for 2D
- TimeDependentFluxErrorEstimator: accept L2 curl space and H1
  hierarchy for 2D curl estimation
- All solver drivers pass L2/H1 spaces for 2D
- MatrixFunction: add 1x1 matrix support for scalar sqrt/pow

Verified: eigenmode indicator norm increased from 8.6e-5 (grad only)
to 1.0e-4 (grad+curl). Magnetostatic indicator is now 6.5e-5 (was 0).
3D tests unchanged.
Critical fixes from code review:
- C1: Fix MPI deadlock in collinear bounding box — broadcasts are now
  outside the dominant_rank block so all ranks participate
- C2: Use z-z (out-of-plane) permeability component for 2D curl-curl,
  not x-x (in-plane). Added GetInvPermeabilityZZ() for coefficients
- C3: Normalize eigenvectors in 2x2 MatrixFunction (divide by ||v||^2)
- C4: Transient solver uses GetCurlSpace() instead of GetRTSpace()

Important concerns:
- I1: Removed debug print from fespace.cpp
- I3: Error message prints all dimension deviations dynamically
- I4: Updated stale comment about 2D curl estimator
- I5: Projected length check skips only when proj_l ≈ 0, not all 2D
- I6: Added magnetostatic and transient regression tests

New examples:
- cpw2d_square_adaptive.json: adaptive PROM frequency sweep that
  matches the uniform sweep with 5 solves instead of 29 (5.8x speedup)
- COMSOL mesh export for cpw2d_postpro cross-section validation

All existing 2D tests (7) and 3D tests (2) pass unchanged.
Two issues fixed in the COMSOL-to-Gmsh mesh converter (meshio.cpp):

1. Edge elements (edg, edg2) were silently skipped because
   ElemTypeComsol() had no mapping for them. Added mappings:
   edg to Gmsh type 1, edg2 to Gmsh type 8.

2. The geom_start offset was applied based on Gmsh type number,
   incorrectly shifting domain elements (tri2, quad2) that are
   already 1-based in COMSOL. Fixed to use element dimension:
   only boundary elements (dim < mesh sdim) get +1, matching
   COMSOL convention of 0-based boundary / 1-based domain indices.
New solver type ModeAnalysis that computes propagation modes of a
2D waveguide cross-section at a fixed frequency. Eigenvalue is the
propagation constant kn, effective index n_eff = kn/k0.

Validated: CPW on silicon (eps_r=11.47) gives n_eff = 2.50, matching
theoretical sqrt(eps_eff) = sqrt((1+11.47)/2) = 2.497.
Phase 2 of mode analysis: add Boundaries.Postprocessing.Impedance
config section for specifying voltage integration path and current
loop boundaries. The solver extracts eigenvectors, computes V and I
line integrals, and derives Z0, L, C.

The V/I computation framework is in place but the integrals need
calibration — the VectorFEBoundaryFluxLFIntegrator gives the normal
flux (E.n) which may not align with the voltage direction for all
gap geometries. TODO: validate against COMSOL impedance results.
…and AMR for mode analysis

- Power-normalize eigenvectors to unit transmitted Poynting power so field
  magnitudes are physically meaningful and comparable to COMSOL.
- Compute full in-plane B field Bt = -(kn/w)(z x Et) + (1/(iw))(grad Ez x z)
  via ProjectCoefficient onto the ND space. Output as Bt_real/Bt_imag in
  Paraview alongside the existing scalar Bz (Hz).
- Extract both Et (ND) and En (H1) eigenvector components; pass En to
  PostOperator for the gradient correction in the Bt formula.
- Add V/I impedance: I = integral of Bt . t dl on current boundary attributes
  using VectorFEBoundaryFluxLFIntegrator; Z_VI = |V|/|I|. Print both Z_PV
  (power-voltage) and Z_VI alongside each mode.
- Add "Target" config option for ModeAnalysis solver to specify target n_eff
  for the eigenvalue shift-and-invert transformation.
- Switch eigenvalue solver from LARGEST_REAL to LARGEST_MAGNITUDE so modes
  closest to the target (in absolute distance) are found on both sides.
- Add TimeDependentFluxErrorEstimator for AMR support using E-field gradient
  flux and Bz curl flux recovery, matching the eigensolver pattern.
- Call MeasureFinalize to save mesh files for GLVis visualization; handle
  empty ErrorIndicator gracefully in the finalization path.
- Add current_marker to PostOperator; update CSV output with Z_PV/Z_VI columns.
…er class

Extract the duplicated 2D boundary mode eigenvalue problem assembly and
solution code from ModeAnalysisSolver and WavePortOperator into a new
shared BoundaryModeSolver class.

The class encapsulates:
- Matrix assembly (Att, Atn, Ant, Ann, Btt) parameterized by material
  attribute mapping, normal projection, and loss tangent support
- Block system construction with BC elimination
- GMRES + sparse direct linear solver setup
- SLEPc/ARPACK eigenvalue solver configuration
- Shift-and-invert spectral transformation

Key design:
- BoundaryModeSolverConfig struct with non-owning pointers to material
  property data, accommodating both volume (ModeAnalysis) and boundary
  (WavePort) attribute mappings
- Constructor assembles frequency-independent matrices on the FE space
  communicator (all MPI processes), configures solvers on an optional
  solver_comm subset (for wave port process splitting)
- Solve()/SolveSplit() methods assemble frequency-dependent Att and run
  the eigenvalue solve, with SolveSplit handling the wave port case where
  only a subset of processes have solvers

Net reduction: ~500 lines removed across the two callers.
…stprocessing

Add GSLIB-based line integral utility (ComputeLineIntegral) for computing
V = integral of E . dl along a straight line between two user-specified
coordinates. This provides an alternative to boundary-attribute-based
voltage integration for both mode analysis and wave port impedance.

Config: "VoltageP1", "VoltageP2" specify endpoint coordinates (in mesh
file units) for the voltage path. "IntegrationOrder" (default 100) controls
the number of Gauss-Legendre quadrature points.

Wave port postprocessing:
- Implement GetVoltage(E), GetExcitationVoltage(), and
  GetCharacteristicImpedance() using the line integral on the 3D field.
- Add port-V.csv columns for wave port voltage (V_wp[port][excitation]).
- Add port-Z.csv with wave port impedance Z = V*conj(V)/(2P), where
  the sign of P is corrected to ensure positive Re{Z}.

NOTE: The wave port voltage and impedance values from the GSLIB-based
line integral have not been fully validated against reference solutions.
The GSLIB point interpolation of H(curl) fields near PEC boundaries may
have reduced accuracy compared to boundary-attribute-based integration.
Users should treat these values as experimental and verify against known
results for their geometry.
…sors

HIGH:
- Revert GetExcitationVoltage/GetCharacteristicImpedance to TODO stubs:
  port_E0t lives on the 2D submesh, GSLIB cannot find 3D points on it.
  GetVoltage(E) using the 3D parent mesh field remains functional.

MEDIUM:
- ComputeLineIntegral: add byVDIM ordering detection and reorder to
  byNODES, matching how ProbeField handles this.
- Replace unused line_length with MFEM_VERIFY for degenerate line check.
- Schema: add minItems/maxItems (2-3) for VoltageP1/VoltageP2 arrays.
- WavePort constructor: MFEM_VERIFY that VoltageP1 and VoltageP2 are
  either both provided or both omitted.
- port-Z.csv: use |V|^2/(2|P|) instead of sign-flipping P, avoiding
  incorrect impedance for receiving ports in multi-port setups.
- Fix comment "Et . n" to "Et . t (tangential)" for boundary attribute
  voltage integral.

LOW:
- Replace public has_voltage_coords with private has_voltage_coords_ and
  HasVoltageCoords() accessor, matching HasExcitation() pattern.
- Initialize port_comm to MPI_COMM_NULL at declaration.
- GetLinearSolver() returns pointer (nullable) instead of reference.
- Expand Bt_inplane Piola scaling comment to explain ND-not-H(div).
- Add evanescent mode normalization fallback using unit et^H Btt et
  when transmitted power P=0 (purely imaginary kn).
The binary (.mphbin) reader used a hardcoded element-type-based formula
for geom_start that incorrectly treated 2D domain elements (triangles,
quads) as boundary elements, shifting their attributes by +1. This caused
"Unknown material attribute" errors when loading 2D COMSOL binary meshes.

Replace with the same elem_dim < sdim logic used by the text (.mphtxt)
reader, which correctly distinguishes boundary elements (dim < sdim,
0-based COMSOL entity indices needing +1) from domain elements (dim ==
sdim, already 1-based).
…ate-based impedance integrals

Refactor voltage and current line integrals to use multi-point paths:

- VoltagePath: list of points [[x1,y1], [x2,y2], ...] defining an open
  path for V = ∫ E · dl. Replaces VoltageP1/VoltageP2. Segments are
  summed: V = Σ ∫ E · dl from p_k to p_{k+1}.

- CurrentPath: list of points defining a closed polygon for the current
  contour integral I = ∮ Bt · dl. The loop is automatically closed
  (last point connects back to first). Uses GSLIB interpolation, avoiding
  mesh edge orientation issues that affected the boundary-attribute method.

Both work via ComputeLineIntegral (GSLIB point interpolation) and are
available for mode analysis (ModeImpedanceData) and wave ports
(WavePortData).

Also filter AMR error indicators to only include propagating modes
(|Im{kn}| < 0.1 * |Re{kn}|), preventing spurious/evanescent modes from
driving refinement to irrelevant regions.
…ate horizontal

New example: cpw2d_cross_section/
- Mode analysis of CPW vertical cross-section with dielectric participation
- cpw2d_thick.json: finite metal thickness (100 nm PEC hole), with
  VoltagePath, CurrentPath, and SA/MS/MA surface loss postprocessing
- cpw2d_thin.json: infinitely thin PEC at substrate surface, with
  VoltagePath, CurrentPath, and SA/MS/MA surface loss postprocessing
- Separate Gmsh mesh scripts: mesh_thick.jl (t_metal > 0 only) and
  mesh_thin.jl (t_metal = 0, region-split approach for boundary control)
- Validated: n_eff ~ 2.477-2.497, Z_PV ~ 38.4-38.7 Ohm, p_sub ~ 92%

Consolidated: cpw2d/ (was cpw2d_square/)
- Renamed cpw2d_square to cpw2d as the canonical horizontal driven example
- Includes both uniform and adaptive frequency sweep configs
- Keeps S-parameter comparison data

Removed: cpw2d_postpro/ (electrostatic dielectric participation now covered
by cpw2d_cross_section), old cpw2d/ (superseded by cpw2d_square)
The existing ModeAnalysis solver (Vardapetyan-Demkowicz formulation) uses the
divergence constraint for the H1 block, which structurally cannot capture
impedance BCs on the normal (axial) E-field. The new QuadModeAnalysis solver
derives the eigenvalue problem from the full 3D curl-curl weak form, giving a
quadratic eigenvalue problem (QEP) in lambda = ikn:

  (M0 + lambda * M1 + lambda^2 * M2) [et; en] = 0

where impedance BCs enter M0 as boundary mass terms on both et and en, with no
kn-dependence. The en boundary term has opposite sign from et (from the double
cross product [n x (n x E)]_z = -En in the impedance BC).

New files:
- palace/models/quadboundarymodesolver.{hpp,cpp}: QEP solver class
- palace/drivers/quadmodeanalysissolver.{hpp,cpp}: driver for QuadModeAnalysis
- examples/cpw2d_cross_section/cpw2d_thick_{,impedance_}qep.json: test configs

Also adds a warning to the existing ModeAnalysis solver that only PEC/PMC BCs
are supported, cleans up the existing boundarymodesolver, and allows farfield
and conductivity operators in mode analysis problem types.
Add volumetric London penetration depth term (1/λ_L²)(E, F) to the QEP
boundary mode solver for superconductor modeling. The London term enters
as a positive mass contribution in both the ND tangential block and the
H1 normal block of the M₀ operator.

Includes a meshed-metal CPW example (mesh_thick_london.jl) with metal
domains and a Palace config for London depth testing.
Implements a linear eigenvalue formulation (Eq 1 + Eq 2 with VD substitution
ẽn = ikn·En, et = Et) that captures both tangential and normal impedance
boundary conditions while maintaining a standard generalized eigenvalue
problem in kn². This avoids the QEP linearization doubling (N vs 2N DOFs).

The formulation uses:
  A = [Att  Atn]  B = [Btt    0 ]
      [0    Ann]      [Btn    0 ]

where Ann includes H1 stiffness + mass + BC-n impedance (from Eq 2 Laplacian
IBP), and Btn = -Atn^T provides the Eq 2 coupling.

Verified against QuadModeAnalysis for PEC and Ls impedance test cases.
Problem type: "NewModeAnalysis" in the JSON config.
MaterialPropertyCoefficient locals declared inside if-blocks were destroyed
before BilinearForm::FullAssemble, which dereferences the raw pointer stored
by the integrator. Move coefficient declarations to the same scope as the
BilinearForm so they outlive the FullAssemble call.

Fixed in all three mode solvers: BoundaryModeSolver (AssembleAtt),
QuadBoundaryModeSolver (M0 tt/nn imaginary), NewBoundaryModeSolver
(AssembleAtt/AssembleAnn imaginary).

Also switch WavePortOperator to use NewBoundaryModeSolver for wave port
boundary mode computation.
Expose eigensolver error estimates (backward and absolute) in all three
mode analysis solvers, matching the existing eigensolver output. Errors
are printed in the log table and written to mode-kn.csv.
…q2 formulation

Remove BoundaryModeSolver (Eqs 1+3, V-D sub) and QuadBoundaryModeSolver
(Eqs 1+2, quadratic EVP) along with their driver classes. The remaining
formulation (Eqs 1+2 with V-D substitution) supports full impedance BCs
while maintaining a standard linear GEP. Rename NewBoundaryModeSolver to
BoundaryModeOperator and NewModeAnalysisSolver to ModeAnalysisSolver.
When Solver.ModeAnalysis.Attributes is specified, extract a 2D boundary
submesh from the 3D mesh and solve the mode analysis on it. The pipeline:

- Extract ParSubMesh, remap domain/boundary attributes from parent
- Collect PEC internal edges (metal traces) via global vertex matching
- Gather serial mesh (PrintAsOne), restore attributes, add PEC edges
- Project to 2D coordinates with material tensor rotation
- Redistribute across all MPI ranks via METIS
- Solve with standard 2D BoundaryModeOperator (no special-casing)

Post-processing: energy density, ParaView output, CSV, GSLIB voltage/
current path integrals all work. Z_PV impedance is parallel-consistent.

Also adds permittivity_imag_scalar for 2D H1 loss tangent assembly and
RotateMaterialTensors for non-axis-aligned cross-sections.
Extract the ~450-line submesh pipeline from modeanalysissolver.cpp into
a single reusable function ExtractStandalone2DSubmesh() in geodata.cpp.
The solver's submesh block is now ~40 lines (extract + repartition +
MaterialOperator). No functional changes.
The standalone 2D mesh has dim=SpaceDim=2, so RT elements and the flux
error estimator work. Remove the use_submesh guard.

AMR is not supported for submesh mode analysis because the 3D mesh
refinement would need to be propagated to the extracted 2D mesh. Add a
warning and skip the AMR loop. Also fix the AMR loop to check use_amr
(previously only the mesh flattening step checked it).
Always construct SurfaceImpedanceOperator, FarfieldBoundaryOperator, and
SurfaceConductivityOperator for mode analysis (remove use_submesh guard).

Fix internal boundary attribute collection to include all BC types
(impedance, absorbing/farfield, conductivity) — not just PEC — so that
internal edges with these BCs get boundary elements in the 2D mesh.

For submesh mode analysis, conductivity boundaries use their operator
rather than PEC elimination (native 2D meshes retain PEC treatment for
backward compatibility).
Conductivity boundaries are now handled by SurfaceConductivityOperator
for both native 2D and submesh mode analysis, rather than being
eliminated as PEC DOFs.
Two bugs in ExtractStandalone2DSubmesh caused segfaults with np>1:

1. PrintAsOne duplicates shared vertices without deduplication, producing
   disconnected mesh components. Replace with GetSerialMesh which uses H1
   global TDof numbering to properly merge shared vertices and preserves
   real element/boundary attributes (eliminating the GatherAttrs workaround).

2. PEC edge vertex indices used per-rank local submesh numbering. After
   combining data from multiple ranks, local indices are meaningless. Use
   GetGlobalVertexIndices for globally unique submesh vertex indices that
   match GetSerialMesh's vertex numbering. This also fixes false dedup
   collisions that dropped PEC edges (e.g. 27 found instead of 30).
Implement block lower-triangular p-multigrid preconditioning for the coupled
ND+H1 boundary mode eigenvalue problem. Each diagonal block uses geometric
multigrid with sparse direct coarse solve: Hiptmair distributive relaxation
for the ND block and Chebyshev smoothing for the H1 block. The off-diagonal
shift-and-invert coupling (-sigma*Btn) is captured in the block-triangular
preconditioner application.

New BlockDiagonalPreconditioner class supports block-diagonal and block
lower-triangular preconditioning with independent sub-solvers per block.

Multigrid is disabled by default for BoundaryMode (MGMaxLevels defaults to 1);
users enable it by setting MGMaxLevels > 1. PCMatShifted defaults to true when
BoundaryMode multigrid is active. The coarse solver type follows the user's
Type config (sparse direct, AMS, or BoomerAMG), with AMS automatically mapped
to BoomerAMG for the H1 block. Wave port solves remain on sparse direct.

Enable KSP timing for BoundaryMode linear solves (Preconditioner and Coarse
Solve lines in the timing table).
The default is true, matching the example configs that write Paraview output.
Use the MeshPartitioner-based distribution pipeline (DistributeMesh) for
the 2D submesh repartitioning instead of the direct ParMesh(comm, Mesh&,
int*) constructor. The old path produced incorrect ND DOF orientations at
partition boundaries, causing eigenvalues to drift as rank count increased.
RemapSubMeshBdrAttributes built the edge-to-attribute map from rank-local
parent boundary elements only. An edge at the intersection of the analysis
surface and an adjacent boundary could have the non-surface face on a
different rank, causing the wrong attribute to be assigned. The resulting
serial mesh differed depending on the 3D partitioning (np), corrupting the
eigenvalue problem.

Fix: use global vertex pairs and MPI_Allgatherv so every rank sees the
complete edge-to-attribute picture before resolving conflicts.
Clamp the METIS partition count to min(nranks, num_elements). Ranks
beyond the element count receive empty mesh partitions. This avoids a
hard abort when the extracted 2D submesh has fewer elements than the
total MPI rank count.
Enable device memory for all temporary vectors in the eigenvector
extraction and error estimation loop. Use MakeRef views instead of
std::copy_n host iterators to extract ND/H1 subvectors from the block
eigenvector, avoiding the unimplemented device-to-host path in
ComplexVector::Set.
Replace std::copy_n (host-only) with mfem::forall_switch for
device-aware vector sub-copies. When multigrid is enabled for
boundary mode analysis, BlockDiagonalPreconditioner::Mult allocates
temporary vectors with UseDevice(true), making Read()/Write() return
GPU device pointers that std::copy_n cannot dereference.
blockprecond.cpp was missing from the device source list, so it was
compiled by the host compiler instead of nvcc. The mfem::forall_switch
lambda was not device-callable, causing the kernel to fall back to CPU
execution while Read(true)/Write(true) returned device pointers.
@simlapointe simlapointe marked this pull request as ready for review March 5, 2026 20:46
@simlapointe simlapointe requested review from hughcars and laylagi March 5, 2026 20:46
HypreParMatrixFromBlocks requires at least one non-null block per row
and column to determine sizes. When domain conductivity is the only
imaginary contribution, only Atti is non-null, leaving the H1 block row
and column entirely null. This produces a matrix with wrong dimensions,
causing a segfault. Add zero diagonal placeholders for null block rows
and columns, matching the existing Dnn pattern in BuildSystemMatrixB.
@hughcars hughcars added the no-long-tests This PR does not require the long tests to be merged label Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no-long-tests This PR does not require the long tests to be merged

Projects

None yet

2 participants