Read and write GPEC .kin files without touching the radial coordinate - #651
Merged
Conversation
The kinetic-profile container the three file formats share, plus the .kin reader and writer. The Osborne pfile and MARS PROF*.IN follow. C-10 is fixed by there being no rescale path at all. The legacy readers min-max rescaled the psi column to [0,1] on every read and the writer put the stretched values back, so one round trip moved every interior point permanently. That is a physics error, not a cosmetic one: GPEC's own read_kin re-splines onto a uniform 100-point [0,1] grid *with extrapolation*, so it expects a truncated edge and handles it. Making the coordinate span [0,1] is now normalize_psi(), an explicit operation whose result records that it happened. C-27 is fixed in the API rather than in a comment: omega_tor and omega_exb are separate fields, and write_kin refuses a set carrying only the former, because the .kin rotation column is omega_E and a toroidal rotation written there is wrong in a way nothing downstream can detect. An all-zero omega_exb is refused too unless asked for, since GPEC substitutes 1e-9 for every zero to keep its spline finite -- the file would not mean what it says. The reader implements GPEC's actual header/footer contract, "no lines start with a number", rather than assuming a fixed header, so a file carrying a provenance footer round-trips. Units are one set fixed by the container, which is what GPEC consumes, so the .kin path converts nothing. Verified on the real reference file: 201 rows, psi[0] = 0.00494621873, and the round trip is byte-identical to the file it came from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…r that refuses Review of the .kin reader found it disagreeing with GPEC about where a table ends, and a suite that asserted the fixture rather than the code. The reader: - _data_lines collected every numeric line anywhere in the file. GPEC's readtable (pentrc/utilities.f90:428-451) takes the *first contiguous block*: it starts at the first line beginning with a number and stops at the next line that does not, ignoring the rest. A summary row below a footer was therefore a fourth data point here and three there, with no error on either side. It now implements the rule GPEC implements. - The numeric test is GPEC's too -- one character after up to two signs -- so "nan" and "Infinity" are header text, as they are to GPEC, rather than numbers Python happens to accept. - Fortran D exponents (1.0D+00) were classified as header and silently dropped; GPEC's list-directed read accepts them, so a file written by a Fortran tool lost rows without a word. Read now. - A ragged row died inside numpy with an inhomogeneous-shape message naming no line. The row is checked and named first. - Provenance numbered columns from zero, so n_e read "column 2" for the file's third column, and extras got no provenance entry at all. Both fixed; extras are keyed column_7 rather than column_6 to match. normalize_psi had method="min_max" as its default, so a caller passing axis= and edge= got a min-max stretch and a PsiNormalization record certifying it -- the silent rescale this module exists to prevent, now with provenance vouching for it. method is required, and axis/edge with min_max is refused. The container was frozen in name only: == raised on its numpy fields, hash raised, and both the arrays and extras were editable in place behind a provenance record that said "as read". eq=False, and the arrays are sealed as read-only *views* so the caller's own array stays writable. The writer refused an omega_exb that was entirely zero; GPEC substitutes 1e-9 element by element, so a converter that zero-filled the core wrote a file whose inner half GPEC quietly rewrote. Any zero is refused now, as is a non-finite value (GPEC's own warning says one NaN ruins the whole spline) and a coordinate that does not increase (spline_fit assumes it does). Naming: UNITS is now KINETIC_UNITS and is exported, as are KIN_HEADER and PROFILE_FIELDS -- the docstring pointed at a vaft.data.kinetic_units module that does not exist, and none of the three were reachable from vaft.data. kinetic_profiles is in __all__ alongside its peers. Tests. The fixture imported KIN_HEADER from the module under test and re-implemented write_kin's format string, so the byte-level assertions compared the writer to its own twin: a header with the right tokens and the wrong spacing passed every test while matching no real file. The header is now a literal in the test. New cases cover the block rule, the numeric-looking footer, D exponents, ragged rows, the axis/edge-without-method refusal, the partly-zero and non-finite and non-monotonic write refusals, sealing (and that sealing does not reach back into the caller's array), and vaft.data's lazy namespace, which had no walk of its own though vaft.formula has had one. 18 tests to 29. Against the reference tree unchanged: 65 .kin files read, 59 written back byte for byte including the named MAST-U example, 0 errors. The six that differ are GPEC's own DIII-D example (right-aligned columns) and the MARS-input file (no indent) -- the docs now say so rather than claiming byte-identity outright. Corrections: GPEC's grid is 101 points (nkin = 100 intervals), not 100. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second PR of the V4 migration slice (hsyun_GPEC#26): the kinetic-profile container the three file formats share, and GPEC
.kinI/O. The Osborne pfile and MARSPROF*.INfollow on top of it.C-10 — the silent ψ rescale
The legacy readers min–max rescaled the ψ column to [0, 1] on every read, and the writer put the stretched values back. Fixed here by there being no rescale path at all; making the coordinate span [0, 1] is
normalize_psi(), an explicit operation whose result records that it happened.This is a physics error rather than a cosmetic one, and the reason is in GPEC's own source:
read_kin(pentrc/inputs.f90) re-splines a.kinonto a uniform 100-point [0, 1] grid with extrapolation. It expects a truncated edge and handles it. A real MAST-U file spans ψ_N = 0.00495 → 1.0, so stretching moves every interior point — and permanently, once written back.C-27 — the rotation column is ω_E
omega_torandomega_exbare separate fields, andwrite_kinrefuses a profile set carrying only the former. A toroidal rotation written into the.kinrotation column is wrong in a way nothing downstream can detect, and the formats invite exactly that mistake — MARS splits the two acrossPROFROT.INandPROFWE.IN, and a pfile has ten rotation-like sections.write_kinalso refuses an all-zeroomega_exbunless asked: GPEC substitutes 1e-9 for every zero to keep its spline from going NaN, so the file would not mean what it says. The committedPROFROT.INreally is all zeros, so this is not hypothetical.The container
A frozen dataclass with named typed fields, alongside
eqdsk.py/aeqdsk.py/keqdsk.py. One unit set fixed by the container — m⁻³, eV, rad/s, Pa, V/m, which is what GPEC consumes, so the.kinpath converts nothing and the pfile/MARS readers will convert on the way in through a single table. Not a dict: a dict is what letwexbmean two things. Anything a file carries with no field of its own goes toextras, andprovenancerecords where each profile came from.The reader implements GPEC's actual contract — "(nearly) arbitrary header and/or footer, with the exception that no lines start with a number" — rather than assuming a fixed header, so a file with a provenance footer round-trips where the legacy
col_mapparse dropped it.Verification
18 tests on synthetic fixtures shaped from the real file (a coordinate that does not span [0, 1], a negative rotation column, a footer). Locally, against the real restricted reference: 201 rows, ψ[0] = 0.00494621873, and the round trip is byte-identical to the file it came from — which also confirms the writer's format matches GPEC's own examples.
Develop gate: 4897 passed.
🤖 Generated with Claude Code