5.3 Data design
5.3 Data design¶
5.3.1 Case file¶
One declarative YAML document is the unit of reproducibility (IF-03). Everything else is derived.
schema: nanopnp/case/v0.5
name: clya-wt-1M-100mV
inputs: # optional; supplied artefacts, §5.3.2
mesh: {path: clya.msh, format: msh41,
groups: {lumen: electrolyte, upper: cis, lower: trans,
clya: protein, bilayer: membrane,
pore_wall: wall, outer_rim: membrane_outer,
symmetry_axis: axis}}
charge: {path: clya_charge.yaml, format: field1}
eps_r: {path: clya_solid_fraction.yaml, format: field1}
# profile: {path: clya_profile.yaml, format: profile1} # stage 4, nanopnp/profile/v1
# pqr: {path: clya.pqr, format: pqr} # stage 7's PDB2PQR step
structure:
source: {path: 2WCD.pdb, variant: ClyA-AS, chains: all, selection: protein}
ensemble: {trajectory: eq.xtc, frames: {last_ns: 5, count: 50}}
symmetry: {point_group: C12, axis: auto}
geometry:
density: {grid_spacing_nm: 0.05, kernel: gaussian_vdw, sharpness: 0.93} # 0.5 Å, as the paper
contour: {isolevel: 0.25, smoothing: taubin, simplify_tol_nm: 0.02}
membrane: {thickness_nm: 2.8, centre_z_nm: 0.0} # centre in the structure's frame, along the axis
reservoir: {radius_nm: 250}
analyte: {shape: prolate_spheroid, a_nm: 2.0, b_nm: 3.0, z_nm: 6.0, charge_e: -8}
charge:
ph: 7.5
forcefield: CHARMM
titration: propka
smearing: {sharpness: 0.5, grid_spacing_nm: 0.005, axis_cutoff_nm: 0.01}
exclusion_offset_nm: 0.0 # FR-15, PHY-20; 0 is the validated model, no exclusion shell
dielectric_transition_nm: 0.0 # PHY-20 NOTE; 0 is the sharp indicator of the validated model
electrolyte:
species: [{name: Na+, z: +1}, {name: Cl-, z: -1}]
concentration_M: 1.0
temperature_K: 298.15
parameters: willems2020_nacl # reference D_i^0, eta^0, rho^0, eps_r,f^0, a_i, a_0
driver: average # PHY-01; `ionic_strength` is a deviation
corrections: # the pluggable registry, §5.5
diffusivity: {model: willems2020_nacl, wall: true, concentration: true}
mobility: {model: willems2020_nacl, wall: true, concentration: true}
viscosity: {model: willems2020_nacl, wall: true, concentration: true}
permittivity: {model: willems2020_nacl}
density: {model: willems2020_nacl}
steric: {model: borukhov, a_ion_nm: 0.50, a_water_nm: 0.311}
boundary_conditions:
bias_V: 0.100
ground: cis
walls: {ion_flux: no_flux, slip: no_slip}
physics: # the named model of PHY-21
model: epnp-ns
flow: true
variable_density: true
inertia: true # PHY-22; the reference model retained it
dielectric_gradient_forces: false # PHY-23; off in the validated model
solid_permittivities: {protein: 20.0, membrane: 3.2} # PHY-20; the only place they are set
numerics:
elements: {phi: P2, c: P2, u: P2, p: P1}
mesh: {backend: netgen, wall_h_nm: auto, size_scale: 1.0, boundary_layer: false}
# wall_for_concentration_M: 3.0 # optional: size `auto` at this salt's Debye length (§8.2.14 P10)
nonlinear: {strategy: newton, damping: residual, max_iter: 100, rtol: 1e-6} # NUM-16
continuation: default_ladder
stabilisation: none # NUM-11; `supg` is its flag, `reference` matches §6.4
wall_distance: {sources: wall, max_distance_nm: 3.0} # PHY-02
linear: {solver: umfpack} # see §6.6; MUMPS requires a source build
outputs: [current, transport_numbers, rectification, eof_rate, analyte_force, fields]
NOTE (nanopnp/case/v0.5, and schema history; §8.2.2 B3, §8.2.7 G6): the
schema moved from nanopnp/case/v1 to carry every key Phases 2 and 3 needed. Against v1 it adds
inputs.profile, inputs.pqr, structure.source.selection, geometry.membrane.centre_z_nm,
charge.exclusion_offset_nm, charge.dielectric_transition_nm and numerics.mesh.size_scale;
renames structure.source.pdb to structure.source.path, since IF-04 reads mmCIF as well; and
removes charge.eps_protein and geometry.membrane.eps_r, whose values
physics.solid_permittivities already carries. A permittivity set in two places is a calibration
parameter (PHY-20) with two sources of truth, and a default of 20 or 3.2 written in code would be a
fitted parameter hard-coded in Python; the author ruled that the map is the one
place they are set. The added keys default to the validated configuration.
In v0.5, every case document must declare schema nanopnp/case/v0.5 exactly, and any
other schema identifier is refused naming the expected schema and the declared identifier before
any structural validation (§5.4.1). The earlier pre-release identifiers nanopnp/case/v1 and
nanopnp/case/v2 are retired.
NOTE (the added keys that change a number): charge.exclusion_offset_nm
and charge.dielectric_transition_nm are the fitted exclusion offset of FR-15 and the width of
PHY-20's transition to ε_w. Both SHALL be non-negative, and both are switches whose validated
default is 0: the validated model has no exclusion shell and a sharp material permittivity (PHY-20
NOTEs). A non-zero value is therefore a deviation that the FR-25 manifest records. When the
charge: block is absent, each reads as its validated default. Both are built from the stage-4
profile (§5.2.1 NOTE on the ion-exclusion shell; §4.4 NOTE on the derived solid fraction), so
either set away from 0 beside inputs.mesh, or in a case carrying neither structure: nor
inputs.profile, is a knob with no effect and SHALL be refused naming both keys. A non-zero
dielectric_transition_nm beside inputs.eps_r supplies one quantity twice and SHALL be refused
naming both. Each is refused below its resolution limit as its NOTE says, and a model that declares
no solid fraction refuses the transition as it refuses inputs.eps_r (§5.4.3) (WP30). numerics.mesh.size_scale, default
1, SHALL multiply every element-size target of §5.2.2 and NUM-30, the resolved wall_h_nm
included. It exists so that a mesh-convergence study (RSK-09, §6.8) is a sweep over a case-file
field (FR-24) rather than a code edit. It is a discretisation choice recorded with the mesh, not a
deviation. With a supplied inputs.mesh a value other than 1 would be a knob with no effect, so
it SHALL be refused.
NOTE (charge.ph, charge.forcefield, charge.titration; the protonation stage, PHY-16 step 3,
FR-12; WP27): ph is a number in [0, 14], the range PDB2PQR accepts.
titration is propka or none; none runs PDB2PQR without a titration method, so every
residue keeps the force field's standard state and the pH reaches no calculation. A ph set away
from its default beside titration: none is therefore a knob with no effect and SHALL be refused
naming both keys, as numerics.mesh.size_scale is beside inputs.mesh. forcefield is one of
CHARMM, PEOEPB and SWANSON, and is passed to PDB2PQR as both --ff and --ffout. PDB2PQR 3.7
also ships AMBER, PARSE and TYL06, but each gives charged atoms a zero radius (53, 93 and 2269
of them on one chain of 2WCD, measured), and PHY-16 step 4's kernel width
w_i = 0.5 R_i is then zero, so they are refused naming the atom count rather than admitted to
fail at deposition; the stage refuses a charged atom with a radius that is not positive from any
source, inputs.pqr included, naming the frame and the atom. The schema had admitted any string
here, but no case carrying charge: was runnable before this package, so narrowing the value set
breaks no document that ran. titration and forcefield are switches whose validated defaults
are propka and CHARMM (PHY-16 step 3); any other value is a deviation that the FR-25 manifest
records. ph is a condition of the experiment, as concentration_M is, and not a deviation. The
case's structure.source.variant is recorded beside Q_net, which it is the provenance of
(OPN-04). charge.exclusion_offset_nm is read by stage 5 and charge.dielectric_transition_nm
by stage 7 (WP30; see the NOTE on the v2 keys that change a number,
which says when each is refused). charge.smearing is read
by stage 7's deposition (WP28; see the NOTE on charge.smearing).
NOTE (charge.smearing, PHY-16 steps 4–6, PHY-18; WP28): sharpness
is the 0.5 of PHY-16 step 4's w_i = 0.5 R_i. It is a switch whose validated default is 0.5,
and any other value is a deviation that the FR-25 manifest records. grid_spacing_nm is the
spacing of the export lattice, on which the kernel is summed and the deposit's integrals are taken.
It is a discretisation choice recorded with the artefact, as numerics.mesh.size_scale is, and not
a deviation. A spacing above half the smallest kernel width of a charged atom SHALL be refused naming
that atom (PHY-16 NOTE on the deposition). axis_cutoff_nm set away from its default SHALL be
refused naming PHY-18 on every path. The deposited kernel is regular on the axis and takes no
guard, and a supplied field declares its own guard in its header (§5.3.1 NOTE on inputs.charge),
so the key would change nothing. The exported field declares the default 0.01 nm for a later
re-read. As with the protonation keys, charge.smearing set away from its defaults beside
inputs.charge, or in a case carrying neither structure: nor inputs.pqr, configures a step
that does not run and SHALL be refused naming both keys.
NOTE (inputs:, FR-27): the optional top-level inputs: block is hand substitution (FR-27) applied
at stage granularity. Each key names a stage output supplied from outside — a mesh, a charge field,
a dielectric field — by path and format. A stage whose output is supplied does not run, and neither
does anything upstream of it; the substituted file is hashed by content and enters the FR-25
manifest as an input like any other. Releases before v0.3 accept an externally generated mesh
this way, which is what makes the solver core testable ahead of the meshing pipeline (§8.1).
inputs.profile supplies stage 4's conditioned polyline as a nanopnp/profile/v1 document
(format: profile1), which is how a hand-edited contour enters a run (§8.1, GUI increment 2), and
inputs.pqr supplies the per-atom charges and radii of stage 7's PDB2PQR step (format: pqr). The
two chains are structure → density → profile → mesh and structure → PQR → charge field, and the
dielectric field comes from the density (FR-15). Supplying an artefact together with one downstream
of it on the same chain SHALL be refused, naming both. The upstream one would be hashed into the
manifest as an input to a run that never read it, and the same holds of every key: each key of
inputs: SHALL be read by the stage that consumes it, so that no file is hashed into the manifest
as an input to a run that never read it (FR-25, VER-47).
A supplied artefact is read by path. artefact:, a store hash, on inputs.mesh, inputs.charge or
inputs.eps_r SHALL be refused as an unsupported section when the case is resolved, naming the key:
no release schedules the store form of the two fields, and the mesh's is REV-62, deferred to
Phase 5 (§8.2.9 I3) (WP40).
NOTE (a relative path in a case, FR-25, QR-08; §8.2.23 Y3): every file a case names by a relative
path (inputs.*.path, structure.source.path, structure.ensemble.trajectory and a correction file
read by path, below) is taken against the directory of the case file the run read, so a case reads
the same files from any working directory. A case held in memory SHALL carry the directory of the
file it came from, where it came from one, and take its relative paths against that: an edited
copy, a member of a sweep over it and a reproduction of it read the files the case file read, and
only a case that never had a file — one a factory or a caller's text built — reads against the
working directory. A case read in Python keeps that directory, and an edit makes each relative path
absolute against it, the caller's own edits included and after them, so a path the edit names is
read where every other path of that case is read. The FR-25
manifest records each input file, the case file included, by its absolute path and content hash;
the case hash covers the paths as written, so moving a case and its files together moves no key. A
reproduction takes a relative path against the directory the original case was read from (QR-08).
A supplied inputs.charge or inputs.eps_r is in the model frame, beside a moved structure as
anywhere else (the NOTE on inputs.charge, inputs.eps_r; WP41).
inputs.profile is consumed by stage 5 (WP21). It is named by path
with format: profile1, and artefact: or groups beside it is refused. The profile is
hashed by the canonical digest of its validated payload. structure: beside it is refused naming
both, by the upstream rule, and so are geometry.density and geometry.contour where either is
set away from its default; geometry.membrane and geometry.reservoir are read. A section set to
its defaults is not refused, because a case dumped from its resolved document writes every section
out, and FR-26 requires that dump to load back (WP21).
inputs.pqr is consumed by the protonation stage, the first half of stage 7 (WP27, §8.2.4 D4). It is named by path with format: pqr, and artefact: or groups
beside it is refused. The file is a PQR of one frame, or one MODEL per frame; every frame SHALL
hold the same residues, and every atom a charge and a radius. ATOM and HETATM records are read by
the PDB columns through the coordinates and then by the two whitespace-separated numbers after
them, which is the layout PDB2PQR writes, whose coordinate fields may run together; a line those
columns do not read is read as whitespace-separated fields, and a line read both ways with
different values is refused naming its line number. A whitespace field that cannot be an atom
name, a residue name or a chain (more than four, four and one characters) does not read the line:
PDB2PQR writes a four-character residue name from the alternate-location column and runs a
four-character atom field into it (OD2ASPP, measured, WP27), which the columns
read. A record type run into a five-digit serial (HETATM12345, PDB2PQR's own layout) is split
before the fields are read. A residue is identified by chain, number and insertion code, and its
name is the one its atoms carry other than PDB2PQR's CHARMM patch names TER and DISU (a
disulfide-bonded cysteine's CB and SG, written 1CB and 1SG; WP27
review), or, where its atoms carry a parent and one of its
protonation variants, the variant: PDB2PQR writes a patched residue's backbone under the parent and
its side chain under the variant (ASP and ASPP in one residue). Unlike
inputs.profile, inputs.pqr stands beside structure:, because stage 1 still runs for the
geometry chain: there the PQR SHALL hold exactly the ensemble's frames, in order, and each frame is
registered to its stage-1 frame by superposing its Cα atoms on the ensemble's, residue for residue,
after which every heavy atom of the ensemble SHALL lie within 0.01 Å of a distinct heavy atom of the
same residue of the PQR, other than the atoms PDB2PQR's hydrogen-bond optimisation may flip: the
amide OD1 and ND2 of asparagine, OE1 and NE2 of glutamine, and the ring ND1, CD2,
CE1 and NE2 of histidine. A flip turns the group through 180° about the bond to it, and the
group is not symmetric about that bond, so a flipped atom lands 0.13–0.40 Å from every atom it was
given (138 heavy atoms of the prepared 2WCD, measured, WP27; the criterion is not over every heavy atom, because a flip does not exchange positions exactly and
such a criterion would refuse PDB2PQR's own output); they are not compared, their worst distance is recorded,
and the rest of each residue fixes where it is. A flip moves atoms and never removes them or changes their
element, so a residue of these three whose PQR holds fewer heavy atoms of an element than the
ensemble's is refused (WP27 review: without it a PQR missing a flippable atom registered). 0.01 Å is about ten times the rounding of a PQR's coordinate columns
and a hundredth of the shortest bond between heavy atoms, so a frame count that differs, a residue
the two do not share, or a frame whose atoms have moved is refused naming the frame. Heavy atoms the PQR adds, as PDB2PQR
adds a missing terminal oxygen, are counted, not refused; hydrogens are not compared. The
registered coordinates are what the artefact carries, so a PQR written in another frame, as the
reference's MD-frame PQRs are, lands in stage 1's. A frame whose heavy atoms already meet the
criterion where they stand is not moved, so the stage's own export reads back exactly. Without
structure: the coordinates are taken to be in stage 1's frame already, which is the frame the
stage exports. Beside inputs.pqr,
charge.ph, charge.forcefield and charge.titration configure a step that does not run, so any
of them set away from its default SHALL be refused naming both keys, as geometry.density is beside
inputs.profile. The same holds beside inputs.charge, which supplies the field stage 7 would
make of a protonation, and in a case carrying neither structure: nor inputs.pqr, which has
nothing to protonate (WP27).
NOTE (structure:, stage 1, FR-01 to FR-03, IF-04; WP18):
source.path names a PDB or mmCIF file, optionally gzipped. MDAnalysis reads PDB and gemmi reads
mmCIF. source.selection is an MDAnalysis selection applied to the file, and one MDAnalysis cannot
parse SHALL be refused naming the key. The selected atoms are those of the selection in the chains
source.chains lists. Every selected atom SHALL
carry its element in the file, in the PDB element columns or the mmCIF type_symbol, and a file
that leaves one blank SHALL be refused naming the atom: a guessed element is how a Cα becomes
calcium. A selection carrying alternate locations SHALL be refused naming the first, because
choosing between them is structure preparation, which the pipeline does not do. A trajectory that
does not hold the structure's atoms, and an mmCIF model that does not list model 1's atoms in model
1's order, SHALL be refused naming the file (WP18 review).
A chain is identified by its chain identifier, or by its segment identifier where the chain column
is blank. source.chains is all or a comma-separated list of chain identifiers, such as
A,B,C, which SHALL number n (a string, so the key's type is unchanged: WP18). symmetry.point_group is C<n>
with n ≥ 1, and any other group SHALL be refused naming the accepted form. The selected chains
SHALL number exactly n, and a listed chain that is absent SHALL be named (FR-03). Each chain SHALL
carry at least half the Cα atoms of the most complete chain. Chains SHALL agree in residue name at
every residue number they share, with the histidine protonation variants read as one name. A
failure of either SHALL be refused naming the chain.
The ensemble is the frames of ensemble.trajectory when one is given, and otherwise the models of
the source file. frames.last_ns keeps the frames within that many nanoseconds of the last one, by
the times the file records. A value exceeding the recorded span by more than one frame interval
SHALL be refused naming the span and the interval. A file written without a timestep reads as 1 ps
or 0 ps per frame, and without this refusal it would yield the whole trajectory in silence.
frames.count keeps count frames at the uniform stride ⌊N/count⌋, ending on the last frame of
the window of N frames. A count exceeding the window SHALL be refused naming both. The selected
frame indices and their times are recorded as read. Every selected frame is superposed on the Cα
set of the earliest selected frame (FR-01). A trajectory holding no frame SHALL be refused naming
the file, and two Cα atoms at one residue number and insertion code of one chain SHALL be refused
naming the chain and the residue, since one would silently replace the other. A chain split across
the periodic box SHALL be refused naming the chain, the residue pair, the frame and the distance: in
any selected frame, two Cα of one chain at consecutive residue numbers, neither with an insertion
code, lie more than 1.0 nm apart, where a peptide-bonded pair lies 0.38 nm apart and a wrapped one
is a box length apart (QR-12; REV-19, REV-57; WP46).
symmetry.axis: auto takes the Cₙ axis from the ensemble-mean Cα structure by chain-permutation
superposition (FR-02). The chains are ordered by azimuth, and the whole assembly is superposed on
itself with each chain mapped to its neighbour. The axis is that rotation's eigenvector of
eigenvalue 1, through the Cα centroid. It SHALL refuse the point group, naming the measured
quantity, when the chains' azimuths are not spaced 360°/n to within a quarter of that spacing, or
when the rotation angle differs from 360°/n by more than 1°. It SHALL refuse C1, which has no
permutation to superpose.
The axis carries no sign of its own. It is signed to agree with the file's +z, so the structure
file SHALL point +z from the trans side to the cis side. A detected axis more than 10° from the
file's z SHALL be refused, naming the angle, because such a frame does not name an end.
symmetry.axis: z takes the file's z axis through its origin. Where n ≥ 2, it SHALL be refused
when its lateral displacement from the detected axis exceeds 0.01 nm anywhere over the Cα axial
extent, naming the tilt, the offset and the displacement. The detected axis it is measured
against passes the spacing, angle and orientation refusals above first.
The aligned frame puts the axis on z at r = 0 and keeps the file's axial coordinate, z = â · x. So
geometry.membrane.centre_z_nm is read in the frame it is written in, and stage 5 applies that
shift. The four thresholds above are constants of the code and never case keys. Their derivation
and the margins measured on the author's aligned copy of 2WCD, on 6MRT and on the ClyA-AS ensemble
are in the WP18 plan, Design §2–§4. The wwPDB entry 2WCD as deposited is not such a file: its
asymmetric unit holds two dodecamers, chains A–L and M–X, in the crystal frame, with the pore axis
22.9° from z and +z towards trans, so it SHALL be refused by the 10° rule like any other file. Its
chains A–L are the author's copy moved rigidly (RMSD 1e-4 nm), and moving them is structure
preparation (WP18 Outcomes).
Until stage 2 is delivered, a walk that extends past stage 1 on a case carrying structure: SHALL be
refused as an unsupported section naming stage 2. Once stages 2 and 3 are delivered, and until
stage 4 is, a walk that extends past stage 3 SHALL be refused in the same way, naming stage 4
(WP19). Once stage 4 is delivered, and until stage 5 is, a walk that
extends past stage 4 SHALL be refused naming stage 5 (WP20). With
stages 5 and 6 delivered, a walk runs the whole pipeline and no stage is refused on this ground
(WP21). Stage 1
alone runs through the stage command (IF-02). A case carrying structure: and inputs.mesh SHALL be refused naming both: a stage whose
output is supplied does not run, and neither does anything upstream of it (the inputs: NOTE), so
the structure would be recorded as an input to a run that never read it (WP18).
NOTE (geometry.density, stages 2 and 3, FR-04 to FR-06, CON-04, IF-05; WP19): the geometry: block is read on a case carrying structure:. Beside inputs.mesh
it SHALL be refused naming both, by the upstream rule of the inputs: NOTE. grid_spacing_nm
SHALL lie in [0.025, 0.05] nm (FR-04) and sharpness SHALL be finite and positive, and each is refused naming
its value rather than narrowed in the schema.
NOTE (geometry.pore, an idealised pore, IF-02, FR-09, FR-10; §8.2.22 X1): a case without
structure:, inputs.profile or inputs.mesh MAY declare an idealised pore,
geometry.pore: {shape: cylinder, radius_nm}, the membrane's thickness and the reservoir's radius
read from geometry.membrane and geometry.reservoir. Stage 5 builds the region from it: the lumen
surface r = R, |z| < h/2 with h the membrane's thickness, is named wall, PHY-02's distance source set and the surface PHY-09's
σ_s is set on, and the membrane's cis and trans faces are excluded from it, as the bilayer's are in
the reference model. Each reservoir is the quarter disc of radius geometry.reservoir.radius_nm
centred on its mouth, the lumen is the domain electrolyte and the reservoirs are cis and
trans, and the two mouths are interface. Stage 6 meshes it under numerics.mesh and its gates,
with the size field of the §5.2.2 NOTE on an idealised pore; stages 1 to 4 and the charge stages do not run unless inputs.charge
supplies a field. geometry.pore beside structure:, inputs.profile or inputs.mesh SHALL be
refused naming both, and radius_nm not below geometry.reservoir.radius_nm SHALL be refused
naming both values. The solid around the lumen is the membrane domain, so a case declaring
geometry.pore SHALL set physics.solid_permittivities.membrane and is refused naming the key
otherwise, rather than taking ion-free water as an unlisted solid does; geometry.membrane.thickness_nm
defaults to the bilayer's 2.8 nm, so a solid-state pore sets it. Beside geometry.pore, keys that
only a structure's stages read are refused naming each: geometry.membrane.centre_z_nm away from 0,
which is a position in a structure's frame, and geometry.density or geometry.contour away from
their defaults; so is numerics.mesh.backend other than netgen, since the Gmsh backend's size
field is posed on a structure's region (WP57). boundary_conditions.wall_charge_C_m2
(PHY-09) and numerics.mesh.maxh_nm are read only beside geometry.pore, and each is refused
elsewhere naming the key.
kernel: gaussian_vdw takes each atom's width as σ R_i, with σ the sharpness. R_i is the
atom's CHARMM van der Waals radius (Rmin/2) by residue and atom name, from the radius set of
PDB2PQR's CHARMM.DAT, held as data under data/radii/. This is an author ruling: it is the set carried by the per-frame PQR files of the reference ensemble
(knowledge/04-clya-geometry-and-charge/01-geometry-pipeline-as-executed.md#11-the-authors-md-archive-measured). The histidine names HID, HIE and HIP read as CHARMM's HSD, HSE
and HSP. HIS resolves only where every one of those three that names the atom gives the same
radius. ILE CD1 reads as CD, OXT as OT2,
and terminal atoms resolve through the patch residues. An atom the set does not name SHALL be
refused, naming its chain, residue number, residue and atom. There is no fallback by element: a
guessed radius is a plausible wrong geometry. Which atoms are deposited, hydrogens included, is
structure.source.selection's to decide, and the count of each element is recorded.
Each frame's density is the probabilistic union ρ_f = 1 − Π_i (1 − g_i) over that frame's atoms,
with g_i = exp(−d_i²/(σR_i)²). A term is kept where g_i ≥ 10⁻⁶. The ensemble map is the mean
of the per-frame maps, not a union over frames. Grid nodes lie at integer multiples of the spacing
in the stage-1 frame, so the axis is a column of nodes. The grid is square in (x, y) about the
axis and holds every kept term with one cell to spare. The map SHALL be finite and within [0, 1],
or the run aborts naming the voxel (QR-12).
Stage 3 bins the map in (r, z) at r_j = j·h, each bin being the annulus of half-width h/2 about
r_j. Its weights are the exact areas of overlap between each grid cell and each annulus, so an
annulus's weights sum to its exact area, and each nonzero cell's to the cell's area. No bin is
interpolated. FR-05's average over the n rotated copies SHALL be taken in the angular harmonic
basis. There a rotation by α multiplies the m-th coefficient by e^{−imα}, so the average keeps
exactly the harmonics with m ≡ 0 (mod n). The averaged map's azimuthal mean is then the map's own,
and its azimuthal variance is 2Σ_{k≥1}|c_{kn}|². Both are computed from the unrotated map, with
neither rotated deposition nor interpolation. That variance is FR-06's residual azimuthal variance.
It SHALL be computed after the binned mean profile, interpolated at each cell's own radius, is
subtracted, because otherwise the radial gradient across a bin reads as azimuthal variation. It
uses only harmonics below the ring's sampling limit π r_j/h. Below r = n h/π no harmonic is
resolved, and the artefact records that radius and each bin's harmonic count rather than
presenting a measured zero. The variance of the map without the Cₙ average is reported beside it,
and their difference is the part of the azimuthal variation that is not Cₙ-symmetric. Neither is
gated: RSK-07 makes the variance a validity criterion to be documented, and no threshold is
specified. The derivations and measurements are in the WP19 plan, Design §1–§4.
NOTE (geometry.contour, stage 4, FR-07, FR-08; WP20): isolevel
SHALL be finite and lie in (0, 1). simplify_tol_nm SHALL be finite, positive and below the
density grid spacing h, because a larger tolerance discards resolved geometry and breaks the margin
of the §5.2.1 NOTE on the contour's size target. Each is refused naming its value, and the second
names h as well. smoothing is taubin or none. No other contour parameter and no gate
threshold is a case key: they are the constants of §5.2.1, and they key the stage-4 artefact.
Stage 4 emits a nanopnp/profile/v1 document with provenance.source: pipeline, and its sha256
is the digest of the stage-3 payload it was drawn from. The loop runs clockwise from its lowest
vertex and stays in the stage-1 frame, so stage 5 applies geometry.membrane.centre_z_nm to it
exactly as to a supplied profile. The document read back through inputs.profile is a supplied
profile (the §5.2.1 NOTE on a supplied fixture). The conditioning and gate records belong to the
stage-4 artefact, not to the document, so a hand edit cannot carry a stale one.
A hand edit written by the desktop shell (§8.1, GUI increment 2) is a nanopnp/profile/v1
document with provenance.source: hand-edit, which is never a reference source. Its sha256 is the
canonical digest of the profile it was edited from, or, where it was started from a loop stage 4's
gate refused (§8.2.2 B9), the digest of the stage-3 payload that loop was drawn from, as stage 4
records it. Its measurements are re-derived from its own vertices. It enters a run only through
inputs.profile, and is gated as a supplied profile (WP24 D5).
NOTE (inputs.charge, inputs.eps_r, IF-05, IF-03): a supplied field is named by a
pydantic-validated header document, schema: nanopnp/field/v1, which carries the quantity, its
units, the grid descriptor (origin, spacing, shape), the axis cutoff of PHY-18 (default 0.01 nm),
the declared Q_net where the producer knows one, the provenance of the file, and the data file it
refers to. format: field1 names that document; the data format is read from it, .npz on the
default path, OpenDX or CCP4 through GridDataFormats, which SHALL remain optional, and
comsolgrid — the reference model's own %Grid/%Data interpolation table, in metres, read
only, since it is an input to the reference rather than an output of ours and writing it would
invite a round trip that is not one. A grid axis SHALL be uniform to round-off or the file SHALL
be refused naming the axis: the interpolant of PHY-19 takes a box and a shape, so a non-uniform
axis would otherwise be resampled onto a uniform one in silence. A header
document SHALL reject an unknown key naming the key, as every other schema in §5.3 does. quantity
is one of areal_charge_density (the reference's rhoq_pore, C m⁻²), volume_charge_density
(C m⁻³) or solid_fraction (dimensionless), and the 1/(2πr) projection and the axis guard of
PHY-16 step 6 SHALL be applied to areal_charge_density only — applying them twice, or not at
all, is invisible in the conservation check of PHY-19, for the reason given there. The interpolant
SHALL be zero outside the grid box rather than continued by its edge value, and a grid whose
boundary values are not negligible against its interior SHALL be refused rather than truncated
silently. An inputs.eps_r field SHALL supply a solid fraction, not an absolute ε_r: see §4.4.
A supplied field is in the model frame, the frame of the mesh the solve runs on, and SHALL be
applied at that mesh's coordinates without a shift. For a case that generates its mesh, that frame
is stage 1's moved by stage 5's z ← z − geometry.membrane.centre_z_nm, the frame stage 7 deposits
and exports in; beside a supplied mesh it is that mesh's own. The header MAY declare the frame it
was written in as model_frame_centre_z_nm, the centre_z_nm of the case it belongs to, and stage
7's export SHALL declare it. Beside a case that generates its mesh, a declared value other than the
case's geometry.membrane.centre_z_nm SHALL be refused, naming both values and the offset every
feature would be applied at (QR-12). A header that declares none is read in the model frame, as
before the key existed, so no document that ran is refused (WP41 D1, REV-06).
NOTE (inputs.mesh.groups, IF-06, QR-12): the mapping reads file group name → vocabulary name.
The key is the physical-group name the mesh file carries; the value is the name the solver selects
on. Several file groups MAY map to one vocabulary name — a CAD export routinely splits one physical
wall into several curves — and the reverse is not expressible, which is why the direction is this
way round. The vocabulary is fixed and carries no aliases: materials electrolyte, cis, trans,
protein, membrane, analyte, exclusion; boundaries axis, wall, membrane,
membrane_outer, cis, trans, analyte, interface. protein is the pore's dielectric body (§2.2), a solid domain
Poisson is solved on and Nernst–Planck and the flow are not; pore is deliberately not a name,
because it reads as both that body and the lumen fluid, and a mesh that uses it SHALL disambiguate
through the mapping. interface is the interior fluid-to-fluid seam a fragmented region carries —
the pore-mouth interfaces the reservoir-to-lumen split leaves behind — and, in a region stage 5
generates, every solid-to-solid seam: the protein against the membrane and, with an exclusion
shell, the shell against the protein and the membrane (WP30). Nothing
selects on it;
it is in the vocabulary because every group must be claimed by some name, and calling an interior
seam wall would put it in the PHY-02 distance source set and impose no-slip across the middle of
the electrolyte. exclusion is the ion-exclusion region of FR-15 — the shell between the dielectric
contour and the exclusion contour, offset outward by the hydrated-ion radius. It is a solid for
Nernst–Planck and for the flow, so the no-slip surface sits at the outer edge of the shell, which is
the conventional hydrodynamic shear plane; it is water for Poisson, so
physics.solid_permittivities SHALL NOT require an entry for it and SHALL NOT abort on its absence.
Ions do not exist in it, so ⟨c⟩ has no meaning there (author ruling 13, §4.4 NOTE) and the water
is ion-free: the shell SHALL take ε_r,f⁰ exactly, never ε_r,f(⟨c⟩) evaluated where the
concentrations are undefined. The resulting step at the shear plane is the salt correction of the
fluid side, which the shell by construction does not carry.
ePNP-NS carries no explicit Stern layer, so a mesh presenting this material is a deviation from the
validated model and SHALL be recorded as one in the run provenance (FR-25), even though no case-file
switch selects it. Ingestion SHALL abort when any group in the file is left unclaimed by the mapping,
or when any name the resolved run selects on — the boundary-condition names,
numerics.wall_distance.sources and the fluid material set — is supplied by no group, and the
diagnostic SHALL name both lists (QR-12). A mapping whose keys are all vocabulary names while its
values are not is almost certainly written backwards, and the diagnostic SHALL say so rather than
reporting every group unclaimed.
NOTE (a group that claims itself, IF-06): a group whose own name is already a vocabulary name needs
no entry in the mapping, and is claimed by that fact. The failure the gate exists to catch is a name
the solver does not speak reaching a solve unnoticed, and a name it does speak is not one of
those; requiring wall: wall of a mesh this implementation generated would be ceremony that a
reader learns to write without reading. Which names the run selects on is derived from the resolved
case — the model's boundary vocabulary, both electrodes, the species, the fluid material set and
numerics.wall_distance.sources — and never from a constant list, so the diagnostic is exactly
true of the run that produced it: this run will select on this name, and no group supplies it. A
selection pattern that is not a flat a|b|c alternation of literal names SHALL be refused rather
than parsed heuristically, since a requirement derived from a mis-parsed pattern is a gate that can
only pass.
NOTE (a solid without a permittivity, PHY-03, QR-12): physics.solid_permittivities is a map from
material name to relative permittivity, defaulting to empty; PHY-20 gives ε_r = 3.2 for the membrane
and 20 for the protein and the analyte, and it is not defaulted because the mesh decides which
solids exist. It is the one member of physics: that is not a switch, and pb and pb-linear
carry no solids, so a non-empty map beside one of them SHALL be refused as §5.3.1's other
inapplicable switches are; each value SHALL be finite and positive, and each key SHALL name a solid
material of the vocabulary, because a key naming a fluid domain (electrolyte, cis, trans) would
replace ε_r,f(⟨c⟩) in the electrolyte and a name outside the vocabulary would be recorded and never
applied, so case resolution refuses both naming the key (a vocabulary solid the mesh lacks is allowed);
poisson carries them as the coupled models do (WP26: the refusal covered all three electrostatic models, which left none of them runnable
on any mesh with a membrane). On an ingested mesh, a material that is
neither in the fluid set nor named in physics.solid_permittivities SHALL abort the run, naming the
material; for a model that carries no solids the abort SHALL say that the model cannot be posed on
a mesh with a solid domain, rather than ask for an entry the case would then refuse. Poisson is
solved over the whole domain, so the alternative is the electrolyte's ε_r about
24 times too large in a solid — a plausible wrong answer with no solver diagnostic. Meshes built in
process by the benchmark geometries keep the warning they have today; the difference is that an
ingested mesh's material names were not written by this codebase. A mesh stage 6 generates from a
profile is gated as an ingested one is: its protein is a structure's body, not a benchmark's, and
the same wrong answer follows from a missing entry (WP21). Rationale: boundary conditions are selected by name and the natural condition under the
r-weighted forms is the free one (§6.2, NUM-06), so an unmapped wall becomes an open boundary,
the solve converges, and the current is wrong with no residual, no gate and no diagnostic.
NOTE (geometry.membrane, geometry.reservoir, numerics.mesh; stages 5 and 6, FR-09, FR-10,
NUM-30; WP21): membrane.thickness_nm, reservoir.radius_nm and an
explicit wall_h_nm SHALL each be finite and positive, and each is refused naming its value.
Stage 5 applies membrane.centre_z_nm as z ← z − centre_z_nm and gates the junction as the
§5.2.1 NOTE on the membrane junction on any profile says. wall_h_nm: auto resolves to
size_scale × min(0.05 nm, λ_D/5). λ_D is §6.3's √(ε₀ ε_r,f⁰ RT / (2F² I)), with ε_r,f⁰
from electrolyte.parameters, the case's temperature and the ionic strength I of the bulk species.
The 0.05 nm ceiling is §5.2.2's pore-boundary maximum, which the validated reference applied at
every concentration. It governs below 1.474 M, and above that NUM-30's target is finer and governs.
ε_r,f⁰ is used rather than the concentration-corrected permittivity, so the mesh does not move
when the permittivity correction is switched. An explicit value is used as written, times
size_scale. wall_for_concentration_M, default none, takes λ_D at that concentration instead of
the case's own for auto, so every salt at or above it shares one mesh (§8.2.14 P10). It SHALL be
positive and finite, and is refused beside an explicit wall_h_nm, which it would not move. The
manifest records the run's own λ_D and NUM-30 ratio beside the design concentration and the λ_D it
sized the wall at. A resolved wall size coarser than λ_D/5 is logged and recorded in the manifest, not
refused: it is a discretisation choice, and a mesh-convergence study needs coarse meshes. §5.2.2's
other sizes are each multiplied by size_scale. After meshing, the mean length of the wall
boundary segments SHALL NOT exceed 1.15 × the resolved wall size, and the longest SHALL NOT exceed
2.0 ×, or the run aborts naming the statistic, the segment and its midpoint (QR-12). Netgen treats
the size as a target, and 1.045–1.078 and 1.25–1.62 are the measured means and maxima (WP21 plan,
Design §3). backend: gmsh meshes the stage-5 region with the optional backend of ADR-002, under
the §5.2.2 NOTE on its size field, and passes the same three gates. It needs the gmsh extra.
Without it, stage 6 refuses the run, naming the extra and the import error, whether that error is a
missing module, a native library the wheel could not load, or a library that loads and then fails
to initialise (CON-10; REV-37). The resolved case is not refused: backend is checked only against
the registered meshers, netgen and gmsh as shipped (§5.5). A generated mesh's key records the backend and that backend's own settings, and the Gmsh
version is recorded beside the key (WP23 D8, D9). The key's wall entry is the resolved wall size,
its source and size_scale, never λ_D: λ_D moves the mesh only through the wall size, so below
the 1.4741 M at which the ceiling binds a salt sweep meshes once. λ_D stays in the manifest's
sizing record beside the key (REV-16; WP46).
maxh_nm, default none, is the global element size of an idealised pore, finite and positive
and refused naming its value otherwise; none takes §5.2.2's global 10 nm. It is multiplied by
size_scale as the wall size is, and it enters the stage-6 key (the §5.2.2 NOTE on an idealised
pore).
boundary_layer: true is refused naming FR-11. geometry.analyte on a generated mesh is refused naming FR-21.
Beside inputs.mesh, any numerics.mesh key away from its default is refused naming it, because
nothing meshes and the key would change nothing.
NOTE (electrolyte.parameters, electrolyte.driver): parameters names the correction file the
reference properties are read from — D_i^0, η^0, ϱ^0, ε_r,f^0 and the steric diameters
a_i, a_0 — and is separate from the per-property correction models because a classical PNP-NS
run turns every correction to none and still needs those values. Steric diameters given under
corrections.steric SHALL be checked against that file and SHALL NOT override it: they are fitted
parameters of the correction set (FR-16), and a case file that could override them silently would
be a second source of truth for a physical constant. driver selects the argument the
concentration corrections are evaluated at: average, ⟨c⟩ = (1/n)Σc_i (PHY-01), or
ionic_strength, which is a deviation from the validated model and is recorded as one.
electrolyte.temperature_K is checked against that file the same way and SHALL be refused naming
both values when it differs: every reference property, every scale and V_T are taken from the
file, whose fits are temperature-specific, so a case at another temperature would be solved at the
file's while its manifest recorded the case's (code review CR-1).
NOTE (a correction file outside the package, FR-16, FR-25; §8.2.22 X6, REV-122): electrolyte.parameters
and each electrolyte.corrections.*.model MAY name a file by a path, relative to the case file or
absolute, as well as by the name of an installed file. A file read by path is not pinned by the
package's version, so its content hash SHALL enter the stage-8 key and the FR-25 manifest, and the
file SHALL be listed as a deviation from the validated default; an edit of the file then misses the
cache rather than serving a stale result. Its path SHALL NOT enter a key: a key carries the
file's name alone, so one calculation run beside two copies of the same file keys one artefact and
two checkouts of one piece of work share a cache, while an edit of either file still moves the key
through the content hash. A case naming two correction files of one file name by different paths
SHALL be refused, naming the file name, rather than keyed as though it named one (QR-12). A name
ending in .yaml is a path; any other is an installed file's name. A relative path is taken
against the directory of the case file the run read, as the NOTE on a relative path above has it;
the manifest records the file among the run's inputs by its absolute path and hash, and SHALL
refuse rather than record a null hash for a file it cannot find, and a reproduction takes a
relative path against the directory the original case was read from, so that it reads the file the
run read (QR-08).
NOTE (numerics.nonlinear): the values shown are the NUM-16 reference settings — the monolithic
damped Newton of the reference model, 100 iterations, relative tolerance 10⁻⁶, tested on the
residual and on the relative update alike. strategy: hybrid and damping: backtracking select
the NUM-20 fallbacks, which no release schedules, so a case selecting either SHALL be refused as an
unsupported section naming NUM-20 and its fallback (WP40).
NOTE (numerics.elements, NUM-03): phi and c_i carry one element order; u is independent of
them and p is independent of both, so the reference implementation's own discretisation —
quadratic phi and c_i with linear u and p — is expressible. An order for u at or below
the order of p SHALL be refused unless numerics.stabilisation selects a mode supplying the flow
stabilisation of §6.4.2, and the refusal SHALL name both the inf-sup condition and the mode that
would permit the pair. The refusal is made when the case is loaded, so nanopnp validate case
and the desktop shell's commit make it with the same text as a run. All three orders are recorded
in the run provenance record (NUM-03).
NOTE (numerics.stabilisation, and the compatibility rule for the case schema): the value set is
the registered modes, shipped as none | supg | reference; a mode registered at run time widens it
(§5.5; §8.2.8 H7). none is the validated default and the production policy of NUM-11;
supg is NUM-11's flag, the streamline term alone; reference is the mode of NUM-14, streamline
and crosswind on the transport operator together with the flow stabilisation of §6.4.2, and is the
only shipped value permitting an equal-order velocity–pressure pair; a registered mode permits
one by declaring it (permits_equal_order). More generally: widening the accepted
value set of an existing key is compatible and SHALL NOT move the schema version, because every
document that validated before still validates; adding, removing, renaming or narrowing a key SHALL
move it. The mode a run actually solved is in its FR-25 manifest, so no artefact of an earlier
revision becomes ambiguous under a widening. From Phase 4 the identifier that a move takes names
the release that ships it (§8.2.7 G6): the first move inside v0.5 makes the schema
nanopnp/case/v0.5, a move inside v0.6 makes it nanopnp/case/v0.6, and the stable schema of v1.0
is nanopnp/case/v1.0, which is distinct from the retired nanopnp/case/v1. A release whose
packages move nothing keeps the identifier it inherited. Between that release's work packages, the
schema under its identifier may change again without a new identifier, because a pre-release makes
no compatibility promise. The manifest of every run records the package version (FR-25), and that
version settles which revision the run read. The identifier is fixed when vX.Y.0 is
tagged. In v0.5, every case document must declare schema nanopnp/case/v0.5 exactly, and any
other schema identifier is refused naming the expected schema and the declared identifier before
any structural validation. The earlier identifiers nanopnp/case/v1 and nanopnp/case/v2 are retired.
NOTE (numerics.wall_distance): sources is the boundary-name pattern the PHY-02 distance field
d is measured from, and its validated default is the pore wall alone. PHY-02 excludes the
membrane from the source set deliberately, so widening sources is a deviation from the validated
model and SHALL be recorded in the run provenance (FR-25). max_distance_nm is the saturation
distance beyond which the wall functions are 1 to within round-off. A value other than the
validated 3.0 nm SHALL be recorded as a deviation for the same reason: below about 1 nm the ion
wall function no longer reaches 1 inside the cap, which changes the model. A value that is not
finite and positive SHALL be refused by resolve(), naming the key (codebase review CR-4).
NOTE (walls): the wall values name the condition applied, not its absence. ion_flux takes
no_flux | prescribed and slip takes no_slip | navier | free. Under the r-weighted forms of
§6.2 the natural condition is the free one, so a value reading as "none applied" would silently
remove no-slip while appearing to be the validated default. The forms pose no_flux and no_slip
only, so every other value SHALL be refused by name until the condition is implemented, and the
refusal lifted in the same commit that implements it: a condition recorded in the manifest but not
applied would be a plausible wrong answer (code review CR-2).
NOTE (outputs:): the list selects what the run produces, and each word is refused rather than
silently ignored where it cannot be met. current, transport_numbers and eof_rate select scalar
quantities of interest (§6.7). Each model declares the quantities it provides (§5.4.3), and a word
the named model does not declare SHALL be refused when the case is resolved, naming the model and
the quantities it declares, rather than when stage 11 reaches it (WP26); the electrostatic models declare none, so a case naming one of them writes outputs: [] or
outputs: [fields]. fields gates the IF-07 field export, which is off unless asked for:
the export is of order ten megabytes per solve and an envelope sweep of thousands of points would
otherwise write tens of gigabytes nobody requested. analyte_force requires the mesh to carry an
analyte material, and a case asking for it on a mesh without one SHALL abort naming the missing
material (QR-12). rectification is a two-point quantity, I(+V)/I(−V); a case asking for it at a
single operating point SHALL be refused naming it as such, because the second bias can only come
from a sweep (FR-24) and inventing one would report a ratio the run did not measure. A sweep
(§5.3.4) SHALL therefore strip rectification from each member's outputs: and produce it in
collection, from pairs of members whose case-file assignments are equal except at
boundary_conditions.bias_V and whose two biases are exactly opposite. Exactly, not approximately:
a tolerance on the pairing would report the ratio of two unrelated operating points as a
rectification. A sweep asking for rectification whose axes produce no such pair SHALL be refused
when the plan is built, naming the axis, rather than collecting a column of absent values (QR-12).