BF: .vti, .vts and .vtr hold a grid, and say so when handed something else - #54
Merged
Merged
Conversation
skoudoro
force-pushed
the
doc/format-status-sync
branch
4 times, most recently
from
September 1, 2026 19:15
f39c309 to
34a6795
Compare
skoudoro
force-pushed
the
doc/format-status-sync
branch
3 times, most recently
from
September 2, 2026 03:20
a57d57d to
08b836f
Compare
… 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
force-pushed
the
doc/format-status-sync
branch
from
September 2, 2026 04:01
08b836f to
e0e47c6
Compare
skoudoro
commented
Sep 2, 2026
skoudoro
left a comment
Collaborator
Author
There was a problem hiding this comment.
ok, all good, merging
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.
.vti,.vtsand.vtrall 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
.vtshold 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
CellDatadropped. Those raise at the point of writing now.The extent a file was read with travels with the mesh, and for
.vtithe origin and step travel with it too..vtrslid its block to the origin, which is the one thing a.pvtrassembling 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
.vtimeasured 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 raisedIndexErrorfrom inside the writer. And the empty.vtsthis 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
.vtrcoordinate array longer than its axis expanded past the extent everything else was sized to; and a.vtswhose 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.0was claimed by the registry and then refused for not starting withMFEM mesh, andMFEM NC-Meshfell 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_idsships 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 carryoriginal_idsis not which ones number densely, a.meshbpath is mapped whateverlazy=says, MFEM INLINE/NURBS/NC all read rather than raising (the reader's own docstring said two of them raised), and.vtr,.vtp,.objand.wktall raise onlazy=Truerather 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.