The external parity sweep#
The conformance suite grades this reader against PyVista’s over
tests/fixtures/, which is a corpus this project wrote. That is the right
instrument for pinning behaviour we already understand and the wrong one for
finding behaviour nobody has thought of: an authored corpus contains the cases
its author knew to author. A green sweep over it says the two readers agree on
the files we wrote.
This document records what happened when the same comparison was pointed at 1,766 FRD files this project did not write, and what it found — including the places where the instrument was wrong rather than the reader.
Result#
Corpus |
Files |
Agree, bit for bit |
Read only here |
Declined by both |
Divergences |
|---|---|---|---|---|---|
CalculiX regression suite |
839 |
832 |
1 |
6 |
0 |
GitHub code search, |
927 |
239 |
0 |
688 |
0 |
Total |
1,766 |
1,071 |
1 |
694 |
0 |
No file in either corpus produced a different answer from PyVista’s reader. Re-run after the writer and the 32-bit mesh storage landed, over the same 1,766 files, with the same result: 832 / 1 / 6 and 239 / 0 / 688.
The writer is swept over the same corpus by tools/sweep_rewrite.py, which
reads each file and emits it again: 1,111 identical, 655 passthrough, 0
differing. “Passthrough” is a file no FRD reader accepts — the loudspeaker
measurements and the rest — which the rewriter hands back unchanged. The two
sweeps are not the same claim: this one says two readers agree about a file,
that one says the file’s bytes survive a round trip. doc/writing.md has it.
“Declined by both” needs care, and an earlier version of this document did not give it any. It means both readers refused the file in the same way — same exception type, same message — which is a real result only when the file genuinely has no mesh in it. It is not a licence to file every refusal as a success, and reading it that way is how a missing half of the format went unnoticed here for a while. The 694 break down as:
688 files that are not CalculiX FRD at all. The extension is shared with loudspeaker measurements and at least one fractal renderer. Declining these is the correct answer and the interesting half of the GitHub corpus.
6 files CalculiX wrote with no mesh in them. Checked individually rather than counted:
spring6.frddeclares0nodes and0elements in its own headers, two.rfn.frdfiles are five bytes containing only the9999trailer, and three more are 86-byte headers with no blocks. A file that states it has no nodes has no mesh, and both readers say so.
“Read only here” is the column that used to be missing. One file —
beampdouble.frd.ref, which CalculiX publishes and reads back as a submodel
input — is binary FRD. PyVista’s reader parses FRD as text, so it answers No nodes found in FRD file; this library reads its 261 points and 32 cells. See
doc/binary.md for how that decode is graded, since the oracle cannot grade
it.
The corpora, and why these two#
CalculiX’s own regression suite#
The strongest available evidence, because these files were written by the
program that defines the format. There is no independent FRD specification;
FRD is what ccx writes and cgx reads.
tools/fetch_corpus.py --source calculix builds it, from three sources rather
than the one an earlier version of this document described:
216 FRD files the suite ships. Mostly
*.frd.ref, the reference output each deck is graded against. They cost nothing to collect and were being ignored entirely, which is how the only binary FRD CalculiX publishes stayed invisible to every sweep.595 written by running the decks,
ccx -i <job>in the directory the deck lives in.28 written by running the decks under a name that is not the deck’s.
<job>.net.frdfor a network analysis,<job>.rfn.frdfor a refined mesh. Collecting only<job>.frddiscarded these, and they are not a random sample: the extra outputs come from different code paths infrd.cthan the main one.
Pinned to http://www.dhondt.de/ccx_2.22.test.tar.bz2 and solved with
CalculiX 2.22, so the suite and the solver are the same release. 839 FRD
files, 125 MB. Of 610 decks, 599 wrote at least one FRD and 11 wrote none
— no FRD requested, or a solver error.
The per-deck bound is 900 s and nothing reached it. The previous run used 180 s, and 23 decks that hit it left truncated FRD files behind that were collected as though they were complete CalculiX output. A partial file is indistinguishable from a small one; the bound is now generous enough that reaching it means the deck really is long-running, and a deck that reaches it has its output deleted rather than kept.
GitHub#
Worth having for a reason that has nothing to do with volume. The .frd
extension is overloaded. Of 927 files retrieved, only 318 are CalculiX FRD.
The other 609 are loudspeaker frequency-response measurements from REW and
OmniMic, satellite laser-ranging data, XML documents, and at least one fractal
renderer’s project files.
That population cannot be authored. A fixture written here to look like “a file that is not FRD” would only ever contain what we already expected a non-FRD file to look like. All 609 are declined by both readers identically, which is the behaviour that matters: a reader that met one of these and invented a mesh would be worse than one that crashed.
Retrieved with gh api search/code, which caps at 1,000 results; 927 unique
blobs remained after de-duplication by SHA. Provenance for every file — the
repository, the path, the blob SHA — is recorded in provenance.tsv by
tools/fetch_corpus.py.
Why neither corpus is in this repository#
CalculiX is GPL, and the GitHub files carry whatever licence each repository
does. Vendoring either into an MIT project would be a licensing problem and a
several-hundred-megabyte one. tools/fetch_corpus.py rebuilds them instead, so
the corpus is reproducible without this repository having to carry it.
The corpus that did come into the repository#
The sweep corpora stay out of tree for licensing reasons, but the reason to
want them applies to the repository’s own fixtures too. Every file under
tests/fixtures/elements/ was written by hand, which means it encodes what
this project believes CalculiX writes — and a hand-written fixture graded
against a hand-written reader can agree perfectly while both are wrong about
the bytes a real solver emits.
tools/generate_fixtures.py closes that loop without touching anyone’s
licence: the input decks are ours, written here and MIT like the rest of
the repository, and the FRD files are what ccx produced from them. Twelve
decks, one element of each supported type, solved with CalculiX 2.22 — 160 KB
in tests/fixtures/generated/, discovered by the same rglob as everything
else, so the entire conformance suite now runs over genuine solver output as
well. All twelve agree bit for bit. The corpus went from 33 files to 45 and the
suite from 298 tests to 358.
tests/test_fixture_bytes.py requires each of them to carry CalculiX’s own
1UPGM/1UVERSION banner, because nothing else in the suite would notice if
one were quietly replaced by an authored file — after which the directory would
still be named generated.
The pyramids have no official fixture, and cannot get one#
Worth stating rather than leaving as an asymmetry someone trips over.
CalculiX 2.22 answers *ERROR reading *ELEMENT: C3D5 is an unknown element type and stops. That is not a quirk of one deck: none of the 610 official
decks mentions C3D5 or C3D13, and a census of the 839 files finds 11
distinct cell types across 188,951 cells and no pyramid among them:
cell type |
cells |
files |
cell type |
cells |
files |
|
|---|---|---|---|---|---|---|
|
93,512 |
228 |
|
289 |
132 |
|
|
64,772 |
429 |
|
64 |
2 |
|
|
18,413 |
23 |
|
42 |
4 |
|
|
7,991 |
21 |
|
8 |
2 |
|
|
1,479 |
38 |
||||
|
1,292 |
3 |
||||
|
1,089 |
122 |
Linear TETRA is absent from this corpus, which the previous run did contain.
That is a difference between the two corpora rather than a change in the
reader, and it is the sort of thing a census restated from memory would have
carried over silently.
So the PY5/PY13 support added by pyvista#8936 is the one corner of the
element table that no file in either corpus corroborates. Whatever writes
those records, it is not this solver, and tests/fixtures/elements/PY5.frd and
PY13.frd — both hand-written — remain the only evidence there is. The sweep
cannot strengthen that claim, and this document should not be read as though
it had.
Method#
tools/sweep_external.py runs both readers over each file and compares
everything: point coordinates, node ids, cell types, offsets, connectivity,
step times, array names and their order, shapes, component counts, and every
value of every array. Two standards, the same two the conformance suite uses:
bit for bit for everything read off the file and for the von Mises arrays;
a stated band for the principal values, which come from an eigensolver — 32 ulp of the tensor’s magnitude, not the eigenvalue’s. See
doc/divergences.mdfor why that distinction is the whole point.
A file gets one of six verdicts. agree and differ are the obvious two.
both-decline means both readers refused it in the same words. both-refuse
means both refused it in different words — the ragged-block case, where the
difference in wording is itself documented in doc/divergences.md; neither
reader stores a short row, which is the part that matters. beyond-oracle
means this library read the file and the oracle could not, which is not an
agreement and must never be counted as one: it is the verdict for binary FRD,
and the file it applies to has to be graded some other way. error is
reserved for a fault in the sweep, so that a defect in the instrument can
never be reported as a property of the reader.
The sweep exits non-zero on differ and on error. It exits zero on the
agreements and on beyond-oracle, because a corpus that legitimately contains
files neither reader should accept — which the GitHub one very much does —
would otherwise make a clean run indistinguishable from a broken one.
What the sweep found#
Four things. Two were defects in the sweep itself, one was a real gap in the test suite, and one was a latent defect in the repository that had nothing to do with FRD.
1. The comparison reported three divergences that were not#
channel8.net.frd, channel9.net.frd and backstepturb.frd came back as
differ: points: 24 of 936 values differ.
They do not. CalculiX writes a literal NaN into the coordinate records of
network nodes — nodes in a fluid deck that have no geometric position:
-1 61 NaN NaN NaN
Both readers produce NaN, in the same places, with the same bits. The
comparison was numpy.array_equal, and NaN != NaN. A false red is not a
harmless one in a sweep of this size — it is the result that teaches you to
skim the output.
2. Thirty-four files were reported as errors when both readers agreed#
ValueError: No nodes found in FRD file. CalculiX writes node-free FRD files
for spring-only and substructure decks, and PyVista refuses to build a grid
from one rather than returning an empty mesh. This library refuses in exactly
the same way, with the same message — but the sweep called the oracle’s
grid-building step outside its comparison logic and let the exception classify
the file as a harness error.
Both of these were the instrument, not the subject. They are recorded here because the first version of this document would otherwise have reported “3 divergences and 34 errors” from a reader that had none.
3. The suite could not have detected a dropped sign on zero#
Fixing (1) required deciding what “equal” means for a parser, and == is
wrong in both directions:
|
bit patterns |
|
|---|---|---|
NaN vs the same NaN |
|
|
|
|
|
The second is the one that matters. -0.0 == 0.0 is true in IEEE 754, so a
reader that lost the sign of a negative zero would pass a value comparison
unnoticed — and numpy.testing.assert_array_equal, which the conformance
suite used, has the same blind spot.
21 files in CalculiX’s regression suite contain -0.00000E+00. This
repository’s fixtures contained none. So the gap was doubled: an assertion
that could not see the bug, over a corpus that could not produce it.
Both readers do handle it correctly — scheibe2f2f.frd has 65 negative zeros
in DISP and they agree bit for bit. What was missing was the ability to
notice if they ever stopped. Closed by:
tests/fixtures/signed_zero.frdandtests/fixtures/nan_coords.frd, written here but modelled on what the sweep found in real files;assert_bitwise_equalin the conformance suite, replacingassert_array_equalfor every array claimed to be bit-exact;tests/test_fixture_bytes.py, so a formatter quietly rewriting-0.00000E+00cannot remove the coverage while leaving the suite green.
Verified by control: against a sign-stripped array, assert_array_equal
passes and assert_bitwise_equal fails, naming the reason. Matching NaNs still
pass, so it does not cry wolf.
4. Git could have destroyed two fixtures, and a test would have gone green#
Not an FRD finding, but found while setting the corpus up. The repository had
no .gitattributes, so git classified fixtures by guesswork:
mock_crlf.frd was stored i/crlf and treated as text. A clone with
core.autocrlf=true — what the Git for Windows installer sets by default — or
input normalises CRLF to LF on the way into the index, turning that fixture
into a byte-for-byte copy of mock.frd.
What makes it worth a document entry is what the suite would have done next.
test_newline_variants_read_identically reads all three encodings and requires
the same arrays from each. Once they are all LF they are identical, and the
test passes because its subject was destroyed. The gate reports green at
the moment it stops testing anything.
Fixed with *.frd binary in .gitattributes, and tests/test_fixture_bytes.py
as the detector for the case where that line is ever removed. Confirmed by
control: normalising mock_crlf.frd fails the new test while
test_newline_variants_read_identically still passes.
Performance#
tools/bench_corpus.py, over the 839-file CalculiX corpus, 125 MB, best of
three per file.
Both readers are driven to the same end state — every array of every step
materialised — because otherwise the comparison is unfair in this library’s
favour: opening a file here indexes the blocks and defers a step’s values until
they are asked for, while PyVista’s reader parses everything during parse().
Timing “open” against “parse” would be measuring work not yet done.
PyVista |
this library |
|
|---|---|---|
total, 839 files, 125 MB |
8.72 s |
1.17 s |
Aggregate 7.42x. The per-file distribution matters more than the aggregate, which is dominated by the largest files:
p0 |
p10 |
p25 |
p50 |
p75 |
p90 |
p100 |
|---|---|---|---|---|---|---|
0.83x |
1.50x |
3.26x |
5.49x |
7.26x |
9.60x |
37.67x |
Slower on 2 of 839 files. Both are tiny files where the whole read is scheduler noise, not a regression.
Files over 1 MB (24 of them): median 5.25x.
Files under 64 KB (658 of them): median 3.51x.
The README’s two-file table is a quick check on a machine; this is the number to quote.
Reproducing it#
Everything below is a command in this repository. The previous version of this
section had a # ... solve each .inp ... in the middle of it, which is the
part that was hardest to get right and the part that was not written down.
# CalculiX corpus: fetch the pinned suite, run every deck, collect every FRD.
# Needs ccx 2.22. Takes a while; nothing else should touch the tree meanwhile.
python tools/fetch_corpus.py --source calculix --ccx ccx
# GitHub corpus: needs an authenticated `gh`.
python tools/fetch_corpus.py --source github
# Parity against PyVista, and the writer's byte-for-byte gate.
python tools/sweep_external.py external-corpus/calculix --json sweep.json
python tools/sweep_external.py external-corpus/github --json gh-sweep.json
python tools/sweep_rewrite.py external-corpus --json rewrite.json
# Timings
python tools/bench_corpus.py external-corpus/calculix --json bench.json
What this does not establish#
Worth stating plainly, because a number like 1,766 invites more weight than it can carry.
It is agreement, not correctness. The oracle is PyVista’s reader. Where both readers are wrong about the format in the same way, this sweep is silent by construction, and no amount of corpus fixes that. The hand-computed values in the gtest tier, the fixtures taken from CalculiX itself, and the byte-for-byte writer gate in
doc/writing.mdare what carry that half.The oracle cannot see binary FRD at all. For the one binary file in the corpus the sweep has no opinion, and for the twelve binary fixtures this project generates it has none either. Those are graded against CalculiX’s own ASCII encoding of the same computation instead.
The two corpora are not independent of each other. Both consist mostly of files written by
ccx, so a CalculiX quirk that both readers mishandle is present in both.11 decks wrote no FRD, so whatever they would have exercised is not here.
The GitHub search caps at 1,000 results. It is a sample of what GitHub will admit to having, not a census of FRD files in the world.
It is one run on one machine. The timings especially are a property of this hardware; the ratios travel better than the absolutes.
Two element types are untouched by it. PY5 and PY13 appear nowhere in either corpus, for the reason above. Every other supported type is covered by both hand-written and solver-written fixtures. Linear
TETRAis covered by fixtures but, in this run of the corpus, by no external file.A green sweep says nothing about the files that are not in it. The corpus grew by 40% between two runs of the same tool, and the growth was not random: it was the shipped reference outputs, the differently-named deck outputs, and the files a 180-second timeout had been truncating. Each of those was invisible, and each was invisible in a way that made the previous result look complete.