Skip to content

BF: .vti, .vts and .vtr hold a grid, and say so when handed something else - #54

Merged
skoudoro merged 2 commits into
fury-gl:masterfrom
skoudoro:doc/format-status-sync
Sep 2, 2026
Merged

BF: .vti, .vts and .vtr hold a grid, and say so when handed something else#54
skoudoro merged 2 commits into
fury-gl:masterfrom
skoudoro:doc/format-status-sync

Conversation

@skoudoro

@skoudoro skoudoro commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

.vti, .vts and .vtr all hold a grid, and none of them said so when handed something else.

All three took their extent from the distinct coordinate on each axis and never checked that those multiply back out to the mesh. A scattered mesh of seven vertices went out as a 7x7x7 grid of 343 points with seven rows of point data under it - a file its own reader refuses, written without a word. A mesh holding the right points in some other order was worse: the count agreed, so the file read back happily with every point attribute on the point mirrored through the diagonal. The extent is read off the cells now, which are a grid whatever the coordinates do. Reading it off the cells is also what lets .vts hold what the format exists for - a warped block, a cylindrical shell, an aerofoil O-grid - none of which could be written before.

None of the three writes its connectivity, and none checked that the cells it was handed were the ones the reader rebuilds. Two tetrahedra over grid points went out as the grid's eight hexahedra with their CellData dropped. Those raise at the point of writing now.

The extent a file was read with travels with the mesh, and for .vti the origin and step travel with it too. .vtr slid its block to the origin, which is the one thing a .pvtr assembling it next to its neighbours reads; the other two trusted what they carried after a transform had moved the mesh out from under it. Each of the three fails differently - a pruned grid keeps the extent of the grid it used to be, a moved mesh keeps its old origin, a scaled one its old step - so each is checked against the mesh at the point of writing rather than trusted.

Writing .vti measured each axis by its first gap alone, so an unevenly spaced lattice came back with every plane past the second moved, in a well-formed file of the right size. It also measured only x and then indexed y and z regardless, so a mesh flat in either raised IndexError from inside the writer. And the empty .vts this codec wrote was unreadable by the codec that wrote it.

Three more ways a well-formed-looking file came back as a mesh nothing in it described: an extent ending before it starts on one axis read as four quads over no vertices at all; a .vtr coordinate array longer than its axis expanded past the extent everything else was sized to; and a .vts whose extent and <Points> array disagreed came back one point wide with no coordinates on it. Each is compared and named now.

Every spelling of an MFEM header the sniffer accepts is one the reader dispatches on. MFEM NC MESH v1.0 was claimed by the registry and then refused for not starting with MFEM mesh, and MFEM NC-Mesh fell through to the NURBS reader, which read a refinement forest as B-spline control points.

Checking a mesh against a grid now walks the cells a corner at a time rather than building the whole connectivity to compare against, so nothing wider than one column is held and a mismatch usually stops on the first corner.


On the documentation side, three surfaces named the supported formats and all three had drifted apart: the README table stopped at 21, the landing page said 25, and the format index said twenty-six across thirty-six extensions. None was right. Every list is now generated from the same 27 codecs, in one order: 27 formats across the 31 extensions the table spells plus .plt, which is recognised and not read, with the six parallel and multi-block meta extensions called out apart. The README was missing seven formats outright, and the landing page had never listed Medit ASCII at all.

MDPA gets the format page every other codec has, and the entity numbering rule gets a usage section - original_ids ships in five codecs and was documented nowhere but the module docstring. Several pages said things the codecs do not do, and those are corrected against the code: which formats carry original_ids is not which ones number densely, a .meshb path is mapped whatever lazy= says, MFEM INLINE/NURBS/NC all read rather than raising (the reader's own docstring said two of them raised), and .vtr, .vtp, .obj and .wkt all raise on lazy=True rather than ignoring it.

Verified against build_default_registry(): every extension in the README table is registered, and the landing page rows, format cards and toctree agree on each name and extension. 2275 tests pass; two fuzz sweeps over 14,811 structured writes - degenerate and flat extents, descending axes, warped grids, permuted vertices, negative extent starts, and translate/scale/prune transforms on top of a stored extent - found no case where a writer accepted a mesh and the file read back as a different one. Docs rebuild clean under -W.

@skoudoro
skoudoro force-pushed the doc/format-status-sync branch 4 times, most recently from f39c309 to 34a6795 Compare September 1, 2026 19:15
@skoudoro skoudoro changed the title DOC: the README and the site list the same formats again BF: .vti, .vts and .vtr hold a grid, and say so when handed something else Sep 1, 2026
@skoudoro
skoudoro force-pushed the doc/format-status-sync branch 3 times, most recently from a57d57d to 08b836f Compare September 2, 2026 03:20
… else

All three structured writers took their extent from the distinct coordinate
on each axis, and none of them checked that those multiply back out to the
mesh. A scattered mesh of seven vertices went out as a seven by seven by
seven grid of 343 points with seven rows of point data under it - a file its
own reader refuses, written without a word. A mesh holding the right points
in some other order was worse, since the count agreed and the file read back
happily with every point attribute on the point mirrored through the
diagonal. The extent is read off the cells now, which are a grid whatever
the coordinates do, and a mesh whose cells are not one raises CodecError
naming the shape that was found and pointing at .vtu, which holds an
arbitrary mesh. The repository already carried an xfail saying exactly this;
it is a passing test now.

None of the three writes its connectivity - the reader rebuilds it from the
extent - and none of them checked that the cells it was handed were the ones
it would rebuild. Two tetrahedra over the points of a grid were written as
the grid's eight hexahedra, their CellData dropped on the way out for
covering the wrong number of cells, and the mesh that came back was not the
one that went in. Cells the extent does not read back now raise at the point
of writing.

The extent a file was read with travels with the mesh, and for .vti the
origin and step travel with it too, so a grid that did not begin at zero -
or that steps down an axis - goes back exactly where it stood. .vtr slid its
block to the origin, which is the one thing a .pvtr assembling it next to
its neighbours reads. The other two trusted what they carried after a
transform had moved the mesh out from under it, and each of the three fails
differently. A grid pruned to one of its cells went out under the extent of
the grid it used to be and read back as 27 vertices and 8 cells it no longer
held; a mesh moved five units went out under the origin it had left behind;
one scaled by ten went out under the step. What no longer describes the mesh
is re-derived from it, the way the entity ids are re-checked at the point of
writing rather than trusted.

Writing .vti measured each axis by its first gap alone. A lattice whose
planes are unevenly spaced is a RectilinearGrid and not an ImageData, and
one written as the latter came back with every plane past the second moved:
x coordinates of 0, 1 and 5 read back as 0, 1 and 2, in a well-formed file
of the right size. An unevenly spaced axis now raises and points at .vtr,
which spells its coordinates out. It also measured the step on the x axis
alone and then indexed y and z regardless, so a mesh flat in either raised
IndexError from inside the writer, and a mesh flat in x quietly took the
default spacing on every axis rather than the steps it did have. Each axis
is now asked on its own, and an axis with no vertex at all ends at -1 - how
VTK spells an empty extent, and what the .vts and .vtr writers already
spelled. Ending it at 0 declared a grid of one point, so a mesh with no
vertices was written as a file holding a vertex it never had. That empty
.vts was then unreadable by the codec that wrote it: an extent with no
points gives reshape no column count to infer one from, and the read came
back as a ValueError about newaxis rather than a mesh with no vertices.

Reading a .vti whose Origin or Spacing spells two numbers where three belong
raised IndexError from inside the parse. The axes it does give are taken and
the rest defaulted, a bare number given as vti_spacing is taken for every
axis rather than subscripted, and more than three warns rather than dropping
the extras in silence.

Three more ways a well-formed-looking file came back as a mesh nothing in it
described. An extent that ends before it starts on an axis holds no points,
so it holds no cells either; all three readers counted the other two axes'
cells anyway, and 0 -1 0 2 0 2 came back as four quadrilaterals over no
vertices at all, every corner naming point zero of an empty array. An end
two or more before its start turned the point count negative on top of that,
which .vts handed to reshape as a second unknown dimension. .vtr builds its
vertices from the coordinate arrays and everything else from the extent, and
nothing made the two agree: an array longer than its axis expanded into more
vertices than the extent declared, while the cells, the offsets and every
PointData array stayed sized to the extent. That comparison is asked of
every axis, including one the extent gives no plane at all: the point count
is a product and goes to zero there, but the vertices are still the
coordinate arrays' own outer product, so an axis left unasked expanded
0 -1 0 2 0 2 into nine vertices under an extent declaring none. An axis the
file leaves no array for takes the one plane at zero the extent gives it,
which is how a two-dimensional grid writes its third. And .vts read the
width of its points from the extent and the array together, so a file whose
two disagreed came back one point wide with no coordinates on it, failing
later on a shape nothing in the file explained. Each of the three is now
compared and named.

Legacy .vtk says the same thing with DIMENSIONS and had the same hole. A
STRUCTURED_POINTS or RECTILINEAR_GRID of 0 3 3 came back as four
quadrilaterals over no vertices, built from strides that were themselves
zero, and a negative DIMENSIONS was handed to the caller as a negative count
of points - compared against a real array length it could never match, and
named in the warning that followed. An axis with no point on it empties the
grid there too.

The extent a mesh carries in global_attrs may be no extent at all. A bare
number has no length and a string that looks like one has characters rather
than numbers, and either reached len and failed with a TypeError from inside
the writer; so could a vti_origin or vti_spacing holding something no float
reads. None of them is trusted now. What cannot be read is named in a
warning and the writer reads the mesh's own extent, origin and step off it
instead - the same answer a stale one gets, since a value nothing can spell
describes the mesh no better than one a transform has invalidated.

Every spelling of an MFEM header the sniffer accepts is one the reader
dispatches on. The registry claims a .mesh file by its upper-cased header
and the reader matched case-sensitively, so MFEM NC MESH v1.0 was claimed as
MFEM and then refused for not starting with MFEM mesh. The two NC spellings
both reach the non-conforming reader as well: MFEM NC-Mesh was falling
through to the NURBS one, which read a refinement forest as B-spline control
points and warned under the wrong variant's name.

Checking a mesh against a grid walks the cells a corner at a time instead of
building the whole connectivity to compare against, so nothing wider than
one column of it is ever held and a mesh that is not the grid parts from it
on the first corner. The .vti writer asks its cheapest question first.

Three surfaces name the supported formats and all three had drifted apart:
the README table stopped at 21, the landing page said 25, and the format
index said twenty-six across thirty-six extensions. None of them was right.

Every list is now generated from the same 27 codecs, in one order, and the
counts are the ones the registry actually holds: 27 formats across the 31
extensions the table spells plus .plt, which is recognised and not read.
The six parallel and multi-block meta extensions are called out apart,
since reading one of those raises. The landing page's mocked `pxios list
--codecs` output was a fourth surface and the stalest of them, naming 21
extensions in a shape the command has not printed for some time.

The README was missing seven formats outright - Medit ASCII, MFEM, VTU,
VTS, VTI, Gaussian splat and Kratos MDPA - and the landing page had never
listed Medit ASCII at all, nor said that .mesh is shared.

MDPA gets the format page every other codec has, and the entity numbering
rule gets a usage section: original_ids ships in five codecs and was
documented nowhere but the module docstring. Both, and the MDPA codec, are
entered in the changelog for the upcoming release.

Several pages said things the codecs do not do, and those are corrected
against the code rather than against each other. Which formats carry
original_ids is not which ones number densely: OFF, STL and Medit have no
attribute channel and drop the key, while PLY, the VTK family and Tecplot
write it back out as an ordinary data array. A .meshb path is mapped
whatever lazy= says, so telling a reader to pass the flag was telling them
to take a warning - lazy=True warns and lazy=False is silent. MFEM INLINE is
materialised into its grid, NURBS reads back control points, and an NC mesh
is read down to its leaves with the vertices rebuilt from its midpoint
rules; none of the three raises, and the reader's own docstring said two of
them did. A .vts extent flat in one axis is a sheet of quads, not
hexahedra. And .vtr, .vtp, .obj and .wkt all raise on lazy=True rather than
ignoring it, which only the pages of the first two admitted.

A .vti or .vtr holding a bare grid was unreadable by the codec that wrote it.
Neither format spells a coordinate per vertex - an ImageData writes an origin
and a step, a RectilinearGrid three axes - but the header check weighed the
declared point count against the bytes on disk as though they did. A plain
four by four by four grid went out as a valid 231-byte file and came back as
ValidationError: declared_n_verts=64 implies 1536 bytes of vertex data. The
heuristic is asked only of a format that spells what it declares; the caps
that bound the allocation still apply to every one of them.

Writing .vti spells its Origin and Spacing at the width a double reads back
at. These six numbers are the whole geometry of an ImageData, and a .10g field
kept ten of the seventeen a double carries: an origin of 0.12345678901234 came
back 2.6e-10 away and a step of 1.0000000001234 came back as a flat 1, moving
every plane by one step more than the last. The writer had just checked the
vertices against that origin and step to prove the file describes this mesh,
and then wrote one that did not.

The evenness an ImageData demands of an axis is measured against the step
rather than against the coordinate. A relative tolerance is a fraction of
where the axis sits and not of how far apart its planes are, so an axis at
x = 1e6 was allowed half a millimetre of drift per plane: a visibly uneven
lattice passed and came back regularised, with every plane past the second
moved. Drift below what a double holds at that magnitude is still allowed,
since no file could record it either way.

All three keep the WholeExtent of the file they were read from as well as
their own Extent. The two differ exactly when the file is one piece of a
parallel set, which is the case the piece indices are kept for, and writing
the piece extent into both narrowed the grid to the piece: a block that went
back out at the indices it stood on still claimed to be the whole domain, so
the .pvti, .pvts or .pvtr assembling it read one neighbour where it should
have read several. It is kept only while the piece extent is, since an extent
re-derived from the cells is zero-based and says nothing about the grid the
mesh used to stand in. A WholeExtent that is not six whole numbers is named
the way a piece Extent already was, rather than failing with a bare ValueError
about a literal.

An MFEM file that opens with a byte order mark is read rather than refused.
The sniffer decodes the mark away and the reader did not, and the mark is not
whitespace for strip to take off, so a .mesh the registry had already claimed
as MFEM failed for not starting with MFEM mesh.

Which formats carry original_ids is corrected once more: Tecplot has the
channel on one side only, since its variables are per-vertex, so vertex ids
travel and element ids go with the rest of the element attributes.
An ImageData spells its whole geometry in an origin, a step and six
indices, and writes no coordinates. Excusing it the file-size heuristic -
which it has to be, since a 4x4x4 grid really is 64 points in a 231-byte
file - left MAX_SAFE_VERTICES as the only bound on the expansion, and
that cap is loose because the heuristic is what makes it safe: every
other format has to spend bytes on a point before the reader allocates
one, so a header can only ask for as much memory as the file it sits in
is long. Here nothing stands between the number and the allocation, and
a 250-byte .vti declaring a hundred million planes on the x axis was
expanded into the 2.4 GB of vertices they come to.

Held to MAX_IMPLIED_VERTICES instead, which clears any grid that expands
into a mesh a machine can work with - a 4000 x 4000 image still reads -
and refuses the rest by name. .vtr is bounded already: its coordinate
arrays have to be as long as the extent says, and a file that omits them
is refused for it.

A .vti Origin or Spacing of any length but three now warns either way.
Only a long one did; a short one was filled from the default in silence,
which is an axis of the mesh given a value the file never spelled - and
with no coordinates written, those six numbers are the geometry.
@skoudoro
skoudoro force-pushed the doc/format-status-sync branch from 08b836f to e0e47c6 Compare September 2, 2026 04:01

@skoudoro skoudoro left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, all good, merging

@skoudoro
skoudoro merged commit 274a2a9 into fury-gl:master Sep 2, 2026
13 checks passed
@skoudoro
skoudoro deleted the doc/format-status-sync branch September 2, 2026 18:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant