Skip to content

The COMSOL export contract (VAL-03, VAL-04, RSK-14)

Kept, but not required

Tier 3 compares the paper's published current–voltage relationships and in-pore averages (VAL-16, VAL-17; SPECIFICATION.md §8.2.4 D6), and that comparison gates v0.7 (§8.2.6 F1). The field exports described here are not asked for. The harness stays, and ingests and refuses exactly as below if an export ever arrives.

What the author produces from the reference model, and what nanopnp validate ingest-golden will and will not accept. SPECIFICATION.md §7.4 governs; where this document and the specification disagree, this document is the thing that is wrong.

Nothing here asks for a judgement call. Every number below either comes out of nanopnp validate export-grid, or is a fact about the model only you can state — and the ingest refuses a golden that leaves one of the latter unstated, rather than guessing. That is the point: a guess made once is a guess repeated silently on every nightly run thereafter.

0. What you need

uv run nanopnp validate export-grid docs/validation/probes/clya-reference.probe.yaml

prints the probe grid's hash and, per patch, the two %Grid axis lines in metres. Paste those into COMSOL's Grid evaluation; do not retype them. A hand-entered extent is a silently moved sample point, and the probe_hash cannot catch it — the document did not change.

The five frozen cases are docs/validation/cases/*.case.yaml. They are frozen: editing one changes its identity and invalidates every golden that declares it.

1. What to export

Five cases × two refinements × six fields × five patches.

Cases (§7.4 NOTE, author ruling):

Case Salt Bias
clya-0.05M-minus200mV 0.05 M −200 mV
clya-0.05M-plus200mV 0.05 M +200 mV
clya-0.5M-plus50mV 0.5 M +50 mV
clya-3M-minus200mV 3 M −200 mV
clya-3M-plus200mV 3 M +200 mV

Refinements. published is the mesh the model ships with. refined_1 is one uniform refinement of it, and is needed for clya-0.5M-plus50mV only — VAL-04 wants a bound on the reference's own discretisation error (RSK-09), not a field of them. If licence access allows only one refined case, that is the one.

Fields. Six per case, each a separate Grid evaluation:

Our name COMSOL expression Unit — exactly this string in the manifest
potential V V
c_Na+ cpos mol/m^3
c_Cl- cneg mol/m^3
velocity_r u m/s
velocity_z w m/s
pressure p Pa

mol/L is refused, not converted. A silent factor of 1000 presents as a 99.9 % discrepancy, which looks like a physics failure and is a units failure. Set COMSOL's unit to SI before exporting, or say so and re-export.

If the model does not export p at all, leave pressure out of the manifest entirely. The report then records it as unavailable and says so. Do not export zeros.

Patches. Five per field — pore, mouth_cis, mouth_trans, reservoir, axis_line — because a COMSOL Grid evaluation takes one tensor-product grid and the probe grid is a union of five. One file each.

2. File layout

One directory per case per refinement, anywhere under $NANOPNP_REFERENCE_DATA:

$NANOPNP_REFERENCE_DATA/comsol/clya-0.5M-plus50mV/published/
    manifest.yaml                  # section 3 below
    npgrid_clya_v8_NaCl_report.mph # the generating model, archived alongside (§7.4)
    potential__pore.txt
    potential__mouth_cis.txt
    potential__mouth_trans.txt
    potential__reservoir.txt
    potential__axis_line.txt
    c_Na+__pore.txt
    ...

Each .txt is one COMSOL interpolation table exactly as the Grid export writes it:

%Grid
<n_r values, the r axis, in metres>
<n_z values, the z axis, in metres>
%Data
<n_r values>      <- one row per z sample
...               <- n_z rows

Nothing else in the file. No extra %Grid block, no second table, no comment header: nanopnp.density.grid.read_grid reads one table per file, and that reader is the one VAL-15 verified against the delivered 77 MB prod5_clya_charge table to 4.7 × 10⁻¹². Adopting a second parser would put unverified code between the reference and every number this phase reports.

Row order is [i_z, i_r] — one row per z, one value per r. No patch is square, so a transposed export is a shape error at ingest rather than a field that is wrong everywhere except on r = z.

3. manifest.yaml

schema: nanopnp/golden/v0.5

case: clya-0.5M-plus50mV
# From: uv run nanopnp validate case-hash docs/validation/cases/clya-0.5M-plus50mV.case.yaml
case_hash: e266057d941a1893c57e86f00612d464f966a659641492fe17790074471b6b70

probe: clya-reference
# From: uv run nanopnp validate export-grid <the probe document>
probe_hash: 0d347ffcb1eb782eb87a1802ef4521b304ee2b72cdb2e2ce400689a3cbf4a9d4

refinement: published          # or refined_1
source: comsol

comsol_version: "COMSOL Multiphysics 5.4 (build 5.4.0.388)"
model_file: npgrid_clya_v8_NaCl_report.mph
export_date: "2026-09-18"

fields:
  potential:   {expression: V,    unit: V}
  c_Na+:       {expression: cpos, unit: mol/m^3}
  c_Cl-:       {expression: cneg, unit: mol/m^3}
  velocity_r:  {expression: u,    unit: m/s}
  velocity_z:  {expression: w,    unit: m/s}
  pressure:    {expression: p,    unit: Pa}

# NOT IN REPORT (knowledge/09-comsol-reference-settings/07-differences-that-cannot-be-removed.md). Only you know these two, and the
# ingest refuses a manifest that leaves either blank.
current_boundary: "the cis reservoir cap, boundary 196"
current_sign_reference: cis     # or trans -- see below

quantities:
  bias_V: 0.05
  current_A: 1.2345e-9
  currents_A: {Na+: 8.0e-10, Cl-: 4.3e-10}
  transport_number: 0.65
  conductance_S: 2.469e-8
  eof_m3_s: 3.1e-18

Take case_hash from the command, not from this page. The schema string is not part of the key (SPECIFICATION.md §5.3.2 NOTE), so a golden declaring a nanopnp/case/v0.5 value, such as 7e9ca188…, is refused when it is compared, naming both hashes.

A case that supplies a charge or ε_r field under inputs: hashes that field's contents into its case_hash: the header's physical declarations and the grid's digest, as stage 7 keys them. Two cases differing only in their charge table are then two cases, and the same table in another format or at another path is one. None of the five frozen cases supplies a field, so none of their hashes moved.

A case whose charge stage 7 deposits, from structure: or inputs.pqr, hashes that charge's recipe instead: the structure's or the PQR's contents, the pH, the force field, the titration and the kernel's settings. The export lattice's spacing, the element order and the tolerances of the protonation's gates leave it unchanged. A case deriving χ from charge.dielectric_transition_nm hashes the width and the structure, and a case with a non-zero charge.exclusion_offset_nm hashes the offset (SPECIFICATION.md VAL-03, §8.2.5 E3). None of the five frozen cases does any of these, so none of their hashes moved.

For such a case, run validate case-hash from the directory its paths resolve against, with the structure or PQR the run will use: the command hashes those files. validate compare and validate export-golden read the structure and protonation the run recorded instead, so they work from any directory and describe what was solved even if the file has changed since.

The two declarations only you can make

current_boundary — which boundary tds.ntflux_i was evaluated on. The model report does not carry it. Write the boundary number and a sentence a reader can act on.

current_sign_reference — cis or trans. §6.7's NOTE fixes our convention: positive current flows trans → cis, referenced to the grounded cis electrode, which is the sign for which an uncharged ohmic pore has G = I/V > 0 at either sign of the bias. If the exported current references the trans electrode instead, declare trans: the loader negates the current, the per-species currents, the conductance and the EOF rate — and prints that it did in every report. The transport number is a ratio of two quantities that flip together and is left alone.

If you are unsure which it is: solve the uncharged-pore case, or check the sign of I at positive bias against a pore you know is cation-selective. Guessing is the one thing that makes a rectification ratio come out reciprocal with nothing to say so.

What is optional

Everything under quantities: except bias_V and current_A. Omit a key you do not have; the comparison records it as unavailable rather than silently covering one quantity fewer.

rectification is deliberately absent — it is a two-point ratio (§5.3.1 NOTE) formed from the matched opposite-bias pair, not a property of either case.

4. Ingest

uv run nanopnp validate ingest-golden \
    "$NANOPNP_REFERENCE_DATA/comsol/clya-0.5M-plus50mV/published" \
    --probe docs/validation/probes/clya-reference.probe.yaml

writes golden.npz and golden.manifest.json beside the tables. It refuses, naming what is wrong, if:

  • a required declaration is missing or blank;
  • a unit is not the one this build reads;
  • probe_hash is not the probe document's;
  • a declared field has no table, or a table's axes are not the patch's to 10⁻⁶ nm;
  • a table's shape is not (n_z, n_r).

Run it once per directory as the exports land. Everything it checks is checked there, once, rather than on every nightly run.

5. What happens to it

Both nanopnp validate compare and nanopnp validate report re-check the probe_hash against the --probe document they were given, and load_golden re-checks golden.npz and golden.manifest.json against the golden_hash written with them. A golden read back is therefore refused on the same three grounds it was ingested on, not only on the day it landed.

nanopnp validate compare samples our solution on the same probe grid and reports, per field, the r-weighted relative L² (the VAL-01 quantity), the unweighted relative L² (which is what sees the axis, where the 1/r forms are fragile) and the located maximum. The norms are taken over the probe points whose four neighbours at margin_nm also lie in the field's domain. Your export must carry a value at every one of those and NaN at every point outside the domain; points within margin_nm of an interface may carry either, and are left out of both the check and the norms, so you need do nothing special for them. nanopnp validate report runs the four-rung attribution ladder and decomposes the discrepancy into the parts we chose and the part that could be a defect.

Tier 3 is recorded, not gated (§7.1, §7.6). Nothing you export here can fail a build.