Changelog#
0.4.1 (2026-09-09)#
0.4.0 reached PyPI only after its release workflow was repaired and re-run by hand, and its documentation never reached the site at all. Every one of the bugs was in the machinery rather than the library, so 0.4.1 is the same code, released the way a release is meant to run.
Bug fixes#
The step that creates the GitHub Release no longer interpolates the changelog into the shell script it runs. The notes are reStructuredText and 0.4.0’s carry 2848 backticks, so every
literalin them opened a command substitution and the step died beforeghwas reached. Every release so far built its wheels and then failed there, which is why the tags before this one carry no assets. The notes travel through the environment now, which is never parsed as shell.A release can be re-run for a tag that already exists. The jobs check out the tag being released rather than the branch the run was dispatched from, which after a release is the next development version - so a release that failed halfway is finished by re-running it, rather than by moving the tag away from the artifacts built under it.
The section a release opens for the next cycle is written above the release just cut, not below it. It was inserted before the second per-version anchor, a rule that dates from a marker matching the file’s own label, and cutting 0.4.0 left 0.5.0 sitting between 0.4.0 and 0.3.0.
A pull request title is escaped before the release quotes it into the changelog. One title spelling an extension as
*.datis an unterminated emphasis marker to the documentation build, which treats a warning as an error: the 0.4.0 stats block broke the build for the tag and for every branch off it. The check the release runs cannot catch this on its own, since it builds the documentation before the stats are appended.A version’s documentation can be published from a ref that is not its own tag. A tag is immutable, so a tag whose documentation cannot build had no way to reach the site at all; a manual run now chooses the tree to build separately from the version directory it publishes as.
GitHub stats for 2026/09/09 - 2026/09/09 (tag: v0.4.0)
These lists are automatically generated and may be incomplete or contain duplicates.
The following 1 authors contributed 10 commits.
Serge Koudoro
We closed a total of 7 issues, 6 pull requests and 1 regular issues.
Pull Requests (6):
PR #64: DOC: the 0.4.1 notes say what the release machinery got wrong
PR #63: BF: a version can be published from a ref that is not its tag
PR #62: BF: the release notes reach gh through the environment
PR #61: BF: the new upcoming section opens the changelog, not the middle
PR #60: DOC: the README and the guides say what 0.4.0 added
PR #59: NF: OBJ keeps the texture coordinates a seam gives two of
Issues (1):
GH#58: OBJ codec: texture coordinates (vt) parsed but not stored in vertex_attrs
0.4.0 (2026-09-09)#
Meshes read and written over file objects and gzip alike, one new format, entity numbering and whole-mesh metadata that survive a round trip, and an extension shared by several formats resolved by what the file holds.
New features#
read()andwrite()now take an open binary file object wherever they take a path, so a mesh round-trips throughio.BytesIO, a socket or a file inside an archive without touching disk. A handle polyxios was given is read or written where it stands and is never closed. A buffer with no file name cannot have its format inferred, sofmt=is required there; a handle fromopen()carries its own extension and does not need it. TetGen is the exception: a.node/.elepair is two files and still needs a path.Lazy reads over a file object work when the handle is backed by a real file and stands at its start - mmap needs a descriptor and addresses a file from byte zero - and raise
LazyReadErrornaming the reason for an in-memory buffer or a handle part-way into a file. Only the formats whose lazy read hands back arrays viewing the mapping ask for that. Binary STL’s lazy mode copies what it reads - it skips vertex deduplication and nothing else - so it takes a buffer like any other read rather than sending the caller to an eager read that would merge the vertices it was asked to keep.gzip is now transparent for every format: a file opening with the gzip magic is decompressed on the way in, whatever it is named, and a destination named
.gzis compressed on the way out..vol.gzis read as Netgen rather than refused,.gznames the compression rather than the format when a codec is chosen - infmt=as well as in a file name - and the compressed output is byte-reproducible (no timestamp, no embedded name).fmt=".obj.gz"is how a nameless buffer asks for compression, since it has no name to end in.gz. A compressed member need not run to the end of what it sits in, so a mesh gzipped into the middle of an archive reads without the bytes after it becoming an error, and a file holding several members back to back - whatcat a.gz b.gzleaves - is read and measured as the whole it decompresses to. A path and a file object go through the same reader, so one compressed file reads the same way and fails the same way whichever it was handed over as. A destination that compresses on its own, such as a handle fromgzip.open(), is written to as it is rather than compressed a second time. Lazy reads still need an uncompressed file and say so, and TetGen - which opens its own sibling files rather than going through this layer - refuses a compressed file rather than parsing it as text, by its content on the way in and by its name on the way out, and for whichever half of the pair carries it rather than only for the one the caller named. Whether a source is compressed is decided by the whole four-byte gzip header rather than by the two magic bytes alone: those open one file in every 65536 by chance, and in a headerless binary format they are an ordinary coordinate’s low mantissa bytes - a.splatwhose first x is 10.658965 opens with exactly them and is a mesh, not an archive.The formats that check a header against the size of the file it came from now take that size from the read they were already making rather than measuring the source separately. Measuring a compressed one cost a whole decompression pass that the read then repeated, and a stream that cannot seek could not be measured at all, so legacy VTK, the VTK XML formats and
.splatread one pass faster and from more kinds of source than before.Extensions several unrelated formats share are now resolved by looking inside the file. A codec declares
SNIFF_EXTENSIONS, asniff(head) -> booltest and aSNIFF_PRIORITY; the contested extension resolves to a dispatcher that delegates to the first codec recognising the opening bytes, and names the candidates when none does. Writing to such an extension raises, an output file having no content to inspect..datis the first user of this: a Tecplot header resolves to the Tecplot codec, a bulk data card to the Nastran one. It no longer needsfmt=, which still works for the cases sniffing cannot settle..pltis registered to the Tecplot codec so a binary Tecplot file is told what is wrong instead of resolving nowhere. Reading it is not supported.PolyData.topological_dimensionreports the highest dimension the mesh actually holds - 0 for points, 1 for lines, 2 for surfaces, 3 for volumes - taking the maximum over the element types present, so a tetrahedral mesh that also carries its boundary triangles still reads as 3-D. It is the dimension of the elements, not of the space they sit in: a triangle mesh embedded in 3-D is 2-D. An empty mesh is 0.transforms.merge_duplicate_verticeswelds coincident vertices into one, the equivalent of ParaView’s “Clean to Grid”. Formats that write a corner per element - STL above all - hand back a soup of unconnected vertices, and welding is what turns it back into a surface.tol=snaps coordinates to a grid of that step before comparing; the default welds only exactly equal ones, and atolso small that snapping overflows to infinity is refused rather than welding every point it overflowed. The survivor of each group is its lowest original index and keeps its own coordinates and attributes, so the result does not depend on which duplicate the file listed first and a tolerance never moves a point. Welding is not culling: a vertex no element references is kept - compose withremove_orphan_verticesto drop those too.Kratos MDPA
.mdpais read and written: nodes, elements named by Kratos element class, conditions,Propertiesids,NodalDataandElementalDatavariables,ModelPartDataand nestedSubModelPartgroups. The class name and the geometry do not identify each other -Element3D4Nis a tetrahedron, and a quadrilateral in space would want the same name - so the writer spells the class with the element’s topological dimension, which makes the pair unique and keeps a round trip exact. Reading is wider: the<n>D<m>Nsuffix is read off whatever application element carries it (SmallDisplacementElement3D4N,VMS3D4N), and the explicit geometry names (Tetrahedra3D4,Prism3D6) are recognised too.Conditionsare read as elements, since they are cells of the same mesh; they are written back underElements: which cells a solver should treat as boundary is a modelling choice the mesh does not carry.The numbers a file gave its nodes and elements now survive a round trip. An Abaqus deck, a Nastran bulk data file, a Gmsh
.msh, a FLAC3D grid and an MDPA file all number entities freely, and renumbering them 1..n leaves the author’s other files - a load case namingGRID 7000001, a report keyed on element4001- pointing at the wrong thing. The reader records what the file said invertex_attrs["original_ids"]/element_attrs["original_ids"]and the writer puts it back. The key is not format-prefixed, so a mesh read from a.bdfand written as.mshkeeps its numbering. Only a numbering the index does not already say is stored: a file numbering1..nin order records nothing, since the writer’s own renumbering reproduces it. Ids that a transform has since invalidated -mergecollides two meshes that each numbered from one, splitting a cell leaves two entities carrying one id - are checked at the point of writing rather than trusted, and a mesh that lost its numbering gets dense ids and a warning rather than a file no solver loads. A format that numbers densely by construction has no id of its own to record and ignores the key on write; whether it carries one comes down to whether it has a general attribute channel, so a mesh read from a.bdf, welded and written as.vtukeeps the ids of the vertices that survived, while OFF or STL drop the key with every other attribute.Two-dimensional files now follow one rule across every format that can spell one. A mesh always carries three coordinate columns, a file that declared two is padded with
z=0, and the fact is recorded inglobal_attrs["was_2d"]so a writer can drop the column again. It says something about the mesh rather than about the file it came from, so a plane read as a 2-D Netgen.vol- which has no two-dimensional spelling of its own to write back - still lands as anNDIME= 2SU2 case. Medit, Medit binary, SU2, Netgen, TetGen, MFEM, DOLFIN, Tecplot, Abaqus and WKT all record it on the way in, and every one of those but Netgen restores it on the way out. Coordinates outrank the flag: a mesh flagged two-dimensional whose vertices have since left the plane is written in three, with a warning, rather than being flattened in silence.Whole-mesh metadata now travels through the VTK family.
.vtu,.vtp,.vti,.vtrand.vtsread and write a<FieldData>block, and legacy.vtkreads and writes aFIELD FieldDatablock between theDATASETline and the geometry - so a time value, a material constant or a solver tolerance inglobal_attrssurvives a write instead of being dropped. A legacySTRUCTURED_POINTS,RECTILINEAR_GRIDorSTRUCTURED_GRIDfile reads the block too, which is where VTK’s own writer puts a time value. The block holds arrays: a scalar comes back as a one-element array, a field array keeps the type it was held in, and a value no numeric array holds is dropped with a warning naming the key. A structured codec spells its own grid entries -vti_extent,vtr_extents,vts_extentand their neighbours - from the mesh on the way out, so those never travel as field data in the format that records them; every other key does, and a structured read takes its own grid over anything a field block names.Text in
global_attrstravels through the VTK XML family. A<FieldData>block holds aStringarray beside its numeric ones, so a name, a title or a solver’s own label - an Abaqus*HEADINGamong them - reaches a.vtu,.vtp,.vti,.vtror.vtsand comes home a string. One VTK itself wrote is read the same way, where such an array used to be skipped with a warning about holding no numbers. A list of strings is one array of several tuples and comes back the list it was. The legacyFIELDblock spells no text at all, so a string handed to a.vtkis still dropped with a warning naming the key.Abaqus
.inpreads a*HEADINGcard intoglobal_attrs["abaqus_heading"]and writes it back, so the deck’s own title survives a round trip. polyxios’s own banner stays a comment, so a deck that never had a heading does not gain one.helper.read_blocksandhelper.read_multiblockread every VTK index file -.vtm,.pvtu,.pvtp,.pvtr,.pvts,.pvti, and a.vtpholding a<vtkMultiBlockDataSet>- whereread_multiblock_vtpread only the last of those. The first hands back onePolyDataper sub-file and the second merges them, so blocks that mean different things can stay apart. An index naming another index is followed and read flat, a pair naming each other is read once, a missing or unreadable sub-file is skipped with a warning - the one the operating system refuses among them - and a reference resolving outside the index file’s directory still raisesPermissionError, nowhelper.Traversal, which is one.read()itself keeps handing back exactly one mesh, and the meta-file refusals now name these two functions.Tag groups travel through the VTK family.
.vtk,.vtu,.vtp,.vti,.vtrand.vtswrite one point or cell column of ones and zeros per group, namedpolyxios_tag_<group>, and read it back as the group. None of those formats has a set of its own, and one column per group is what keeps an element in two groups in both - which the single reference a Medit or Netgen record carries cannot say. A column of that name holding anything but whole numbers stays an attribute, since a member rounded into place names the wrong element.An Abaqus
*SURFACEis read and written. A surface names a side of an element, which the deck holds no element for, so polyxios reads it as the triangle or quadrilateral that side describes, tagged with the surface’s name - the way the UGRID, SU2 and Netgen readers already hand back their boundary faces. Two element attributes carry the link back:face_parent, the element it is a face of, andface_index, which of that element’s faces. Writing puts the*Surfacecard back, with its parent*Elsetmarkedinternalthe way Abaqus marks its own, and an internal set a surface named is dropped on the way in, so a round trip is stable; one no surface names is a group the deck’s author wrote and is kept. A*Surface, type=NODEbecomes a vertex tag - one member per data line, since the field after it is a weight factor and a whole number is as good a weight as a node id - and goes back out as the*Nsetit now is, a node set and a node surface being the same members under two cards.SPOSorSNEGon a shell tags the element itself rather than duplicating it. The two columns are read back wherever they hold whole numbers, so a deck that went out through a format carrying every attribute as a double, legacy.vtkamong them, still writes its surfaces back.transforms.mergeshiftsface_parentonto the mesh it is building, the way it already shifted the tag groups, so the surfaces of two meshes joined into one - the blocks of an index file among them - survive the join instead of naming the first mesh’s elements.read()now forwards the options it does not recognise to the codec it chose, the waywrite()already did, and so does the dispatcher a contested extension resolves through. An option a reader does not take raisesTypeErrornaming it. STL’smerge_verticeswas written as a read option and had never been reachable without importing the codec itself; it is nowread(path, merge_vertices=False).OBJ reads
split_seams=True, which keeps the texture coordinates and normals a per-vertex array otherwise drops. OBJ indexesvtandvnper face corner, so a vertex on a seam is given two texture coordinates and one on a hard edge two normals, and folding them one per vertex kept the last. Splitting copies the vertex once per distinct pairing its corners name: the first corner keeps the index it had and each later one takes a fresh index off the end, so a vertex no face names stays where it was, the faces keep their count and their order, and a file whose corners agree reads exactly as it does without the option. Records nothing indexes are counted against thevrecords the file wrote rather than the vertices the split left, and a copy takes what the vertex it came from takes. The warning about the values it drops now names the option that keeps them.
Behaviour changes#
Medit
.meshrecords a two-dimensional file inglobal_attrs["was_2d"]rather than keeping the file’s own number inglobal_attrs["medit_dimension"], which is now the shared spelling every format uses. A three-dimensional file sets nothing, where it used to setmedit_dimension: 3.Formats that had always written three coordinate columns now write two for a mesh that came from a two-dimensional file and has stayed flat: TetGen’s
.nodeheader declares 2, a Tecplot zone declaresXandYalone, an Abaqus node card carries two coordinates, and a.meshbheader declaresDimension 2. Two of those constrain the rest of the file, and the writer keeps them consistent rather than emitting one no reader loads: an Abaqus deck of two-column node cards is written under the planar element cards -CPS3,CPS4,T2D2- since Abaqus takes a node’s dimensionality from the element referencing it, and a Tecplot zone keeps its third coordinate variable when the mesh carries a variable of its own namedZ, which the positional naming would otherwise read back as the z column. A mesh holding an element type with no planar Abaqus card - a solid, a biquadratic quad - or anelement_type=override naming a three-dimensional card stays three-dimensional too. A flat mesh of solid cells - a tetrahedron is one however flat it lies - keeps its third column everywhere the node count per element is declared separately from the coordinate count: an MFEMverticesblock, a TecplotET=TETRAHEDRONzone, a TetGen.elefile, a MeditTetrahedrasection in either spelling and a DOLFINcelltype="tetrahedron"each need three, whatever the flag says. Adim=given to the DOLFIN writer by name is the caller’s own word and is left alone.Text formats are written with
\nline endings on every platform. Writing used to go throughPath.write_text, which turned them into\r\non Windows; every write now goes through the same binary path a buffer does, so the bytes a path receives and the bytes aBytesIOreceives are the same ones. Every reader already accepted either ending.
Bug fixes#
A legacy
.vtkSCALARSheader names between one and four components and no more. The writer spelled every tuple it had noVECTORSorTENSORSbranch for asSCALARSwhatever its width, so a nine-component per-element array went out asSCALARS name double 9- a count outside the range the format gives that field, which leaves what the file means to how forgiving each reader happens to be. Such a tuple now travels as aFIELDarray, whose own header carries the component count and so has no ceiling; four components and under keep theSCALARSspelling and its lookup table. An array of no components at all is outside the same range from the other end, and is dropped with a warning before its section header is written rather than spelledSCALARS name double 0.A binary block in a legacy
.vtkfile is closed by a newline, and the keyword line after it is found by reading to the next one. Only the dataset’sFIELDblock wrote that newline: the points, the cells, the cell types and every attribute array ran straight into the keyword after them, so the terminator a reader found was the first0x0ainside the following payload and the keyword line it read began in the middle of the numbers - a file whose sections a reader could only find by hunting for the keyword, as polyxios itself did. Every binary block now closes the way the format says, and a file written before this still reads: the newline is stepped over where it is there rather than required.A symmetric tensor’s six components are
XX,YY,ZZ,XY,YZ,XZ- the order VTK itself hands back for the cell at(i, j). The writer mirrored a six-component array into the full 3x3 aTENSORSsection holds using the classical Voigt order instead, whose last three run the other way, soXZandYZcame out in each other’s places. Nothing downstream can tell that from a tensor that was always that way, which is why it went unseen.The two numbers on a legacy
.vtkv4.2CELLSline count the cells and the values that follow them. The second was taken from the length of the whole connectivity array rather than from the run the offsets name. The two are the same for a mesh built here, but a mesh holding a connectivity its last offset stops short of - a slice kept at its original length - declared values the section never went on to write, which leaves the next reader inside the numbers. Both spellings now declare what they write, and the spelled one no longer says one thing where the binary one says another.The first number on a legacy
.vtkv5.1CELLSline counts the offsets that follow it, and every offset past the first names a cell. The whole offsets array was written whatever the mesh’s cell count, so a mesh holding one kept at a buffer’s original length declared cells theCELL_TYPESafter it never named - and a reader that believes the offsets builds them anyway, taking each one’s type from past the end of the array that holds it. Both sections now stop at the cells the types name. An offsets array that begins partway into the connectivity is rebased onto the run written after it, so the two versions answer such a mesh with the same cells rather than only v4.2 doing so.An attribute array whose row count is not the one its section covers is dropped, with a warning, before that section is declared. Only the section header says where one of its arrays ends, so an array of more rows ran past that end and one of fewer stopped short of it, and either way the reader took the values on the wrong side of the boundary for the keyword line that should have been there - costing every array after it in the section as well. VTK’s own reader stopped on such a file with
Unsupported cell attribute type.A legacy
.vtkTENSORS6section is read as the six components it holds. VTK 9 writes a symmetric tensor with that keyword, and every reader here matched it as aTENSORSand took its tuples for nine: an ASCII file failed with aCodecErrorabout a row that is not numbers, and a binary one read six values plus whatever followed them, then went on from the middle of the next array and dropped every array after it. The six are mirrored into the full 3x3, so a mesh holds one tensor shape whichever spelling the file used.An array whose name holds XML markup - an ampersand, a quote, an angle bracket, as a group named in another format may - is written escaped by the VTK XML formats. It used to close the
Nameattribute early and leave a file no reader could parse, polyxios’s own included. A name holding a newline, a carriage return or a tab is escaped as a numeric reference for the same reason: written as it stands, an XML parser normalises it to a space and the array comes back under a name it never had. Legacy.vtkhas no escaping to fall back on, so an array whose name holds whitespace is dropped with a warning there rather than written as a name and a stray token. A name holding a character XML has no spelling for at all - a control character, which no numeric reference reaches either - is dropped with a warning by the XML formats for the same reason.A legacy
.vtkFIELDheader that leaves its type field out is read as thefloatthe ASCII scan has always defaulted it to, where the binary scan skipped the array and carried on framing the block from the wrong offset. The format makes the field mandatory, so this only ever answers a malformed header - but the two scans have to answer it the same way, or one file reads back as two different meshes depending on how it was written. A header short of the name, component count and tuple count that frame its payload is refused by both for the same reason, as is a block that declares more arrays than the file holds, and as is one whose component or tuple count is negative - the two are multiplied into the length of a payload, and a negative one walked the binary scan backwards off the front of the file and reshaped to a dimension numpy infers rather than the one the header claimed.Every legacy
.vtkreader looks for its keywords below theDATASETline. The second line of the file is a free-text title, and the two readers that scanned from the top of the file read one beginning with a keyword as that keyword’s header: aPOLYDATAmesh titledVertices of a coworPoints of interestwas refused, and theFIELDblock this release adds widened that to any title beginningField. The line is found rather than counted, so a header carrying a blank line between its four is read too, where anUNSTRUCTURED_GRIDreader counting to four missed the first keyword after it.A legacy
.vtkcell array is read against what its block actually holds. ACELLSrow, or aPOLYGONScell, declaring more vertices than it goes on to list used to be trusted twice over: the row kept the indices it had while the offsets advanced by the width it claimed, so every cell after it was cut out of the wrong place - and the binary spelling of the same fault reached numpy as a bareIndexErrornaming neither the file nor the cell. Both are now aCodecErrorthat names them.Every remaining legacy
.vtksection that reads numbers off ASCII lines says which file and which section a malformed one came out of.POINTS,CELL_TYPES, aX_COORDINATESarray and aSTRUCTURED_GRIDPOINTSblock used to hand a bareValueErrorabout one token, or a reshape, to a caller that had asked to read a file; a section that runs into the keyword after it, or that the file ends inside, is refused by name. ADATASET FIELDarray the file runs out inside is dropped with a warning instead of being reshaped into a shape it cannot fill. Thirty thousand mutations of six corrupt files now leave the codec raising onlyCodecError.transforms.mergekeeps the surfaces of a mesh whoseface_parentandface_indexcolumns are doubles - which is every mesh that went out through legacy.vtk, where a cell array is a double whatever it held. Merging one with a mesh carrying no such column fills that mesh’s rows with the blank a float dtype spells, NaN, and a NaN was read as a column that could not be trusted at all rather than as the-1that says “not a face” - so every surface the first mesh did carry was dropped without a word, which is the loss shifting the column exists to prevent.A
<FieldData>name that more than onePieceof a.vtuor.vtpspells keeps the first piece’s value and is reported. The mesh’s metadata is one mapping over the joined mesh, so folding the pieces with a plain update kept the last - a value a reader of a single-piece file would never have seen, and a loss no warning named. The dataset’s own block still wins over every piece.A legacy
.vtkPOINT_DATAorCELL_DATAheader is written only once its arrays are known to be nameable. A section whose names all hold whitespace - which the format’s whitespace-separated header field cannot spell - used to declare itself over nothing, the way theFIELDblock already refused to. The names are now checked once per section rather than once per array, so the warning names them together.A legacy
.vtkASCIIPOINTSorCELLSblock wrapped over several lines is read. Both are a run of numbers the header counts, and VTK’s own reader takes them as one however they are broken up; read a row at a time, a wrappedPOINTSblock silently kept three numbers of each line and dropped the rest, and a wrappedCELLSblock was refused for a row that was never short. One row to a cell is still the path every file VTK writes takes, and the run is gathered only once a row turns out not to hold its own cell whole - so a well-formed file pays nothing for it, and a file that is malformed either way is still named by its row.The Cython fast paths for those two blocks are bounds-checked. They are compiled with
boundscheck=False, which takes the check off their list indexing as well as off their arrays, so a header declaring more rows than the file holds, or aCELLSrow holding no tokens at all - a blank line inside the block, which the wrapped-block reader above takes in its stride - read past the end of a list: a segmentation fault on a file the reader was handed, not a diagnosis. The counts are numbers out of the file and are now checked once per block, the row is checked before its first token is indexed, and its own width against the tokens it lists. A width is held in a machine word rather than a Cintbesides, since one past two billion raised out of the conversion itself, before the check that names the row - so both paths refuse the same files with the same sentence whether or not the extension was built.A tag group of a shape numpy builds no array of - a ragged list of lists, a mapping, a column of names - is reported by every writer rather than raising out of the middle of one.
vertex_tagsandelement_tagsare not checked on the way in, and such a group used to reach a bareValueErrorabout an inhomogeneous shape in whichever codec looked at it first. It names no entity, which is the loss the.inp,.msh,.su2,.vol,.ugrid,.f3grid,.mdpa,.node,.objand VTK writers already report for a group whose members index nothing.A
<FieldData>block naming one array twice keeps the first and says so, the way one whose name twoPieceelements spell already did. A mesh’s metadata holds one value per key, and taking the last meant one file read back two ways depending on the order its blocks were walked in. A legacy.vtkFIELDblock answers a repeated name the same way, where all three of its parsers took the last without a word: one mesh has to read back the same whichever spelling of the format it was written in.A
DATASET FIELDfile holding astringarray is read rather than hung on. Its lines were counted from the array’s own header, so one declaring no strings never reached a line to step over and the reader looped on the header for ever - a file of five lines that never returned. Counting them after the header ends that, and ends an off-by-one with it: an array of one string used to leave its text behind to be read as the header of an array of its own. A component or tuple count that is negative is skipped there too, the way theFIELDblock inside a mesh already refuses one: the two are multiplied into a length, and a negative one sliced the values read from the wrong end and stored an empty array under a name the file never spelled.Every face in the table of a volume element’s sides is wound so its normal points out of the element. One face of each - a hexahedron’s and a pyramid’s base, a wedge’s and either prism’s bottom, and three of a voxel’s six - was wound the other way, so
transforms.extract_surfacehanded back a skin lit from inside along those faces, which is the shape that reads as a hole in the mesh. The numbering is unchanged, so an AbaqusS<n>and aface_indexstill name the face they always did.A chain of VTK index files each naming the next is followed to a fixed depth rather than as far as it is long. An index naming a parent of its own was already refused, but a chain is no cycle: a deep enough one recursed past the interpreter’s own limit, and the
RecursionErrorsurfaced as a block that could not be read rather than as the file it was.An
abaqus_headingwhose text would end the*Headingcard is dropped with a warning rather than written into the deck. A heading is free text and travels through every format with a metadata slot, so it comes back holding whatever was put in it; written unchanged, a line beginning*is the next card, and a title of*Nodefollowed by a row spelled the deck a vertex it never had. A line holding**goes for the same reason: the marker opens a comment anywhere on a line, so such a title came back truncated at it, or as nothing at all.An Abaqus
*Headingline ending in a comma keeps the line below it. A trailing comma continues a data line onto the next one everywhere else in a deck, and a heading holds no data: the rule glued a title ofturbine blade,to therev 3under it, and the deck came back one line where its author had written two.An Abaqus
*Surface, type=NODErow that a trailing comma joined onto the next reads a whole number after a set name as that set’s weight factor rather than as a node of the surface. The join takes the line break with it, and the line break is what told the one from the other; a row opening with a node number is still a row of node numbers, which is the spelling the join exists to read.An element is written back as a
*Surfacerow only when its own type is the one a face of that width names. A tetrahedron’s four nodes can be exactly a hexahedron’s face, and columns saying so passed the vertex check: the solid was written as a side of the hexahedron instead of as an element card, so the deck lost a volume element and handed back a quadrilateral.A
*Surfacerow that would take the mesh past the connectivity safety cap is refused before its face is built rather than after, where a check that only counted what was already there let the mesh past the cap by a face’s width.A binary PLY face declaring more vertices than the file holds - 2**31-1 of them - is refused with a
CodecErrornaming the truncation, where the whole-block read used to build a record dtype for it first and hand back numpy’sValueErrorabout a tuple shape, which named neither the file nor the face. Every other format’s declared counts were swept for the same fault and are guarded;tests/test_declared_counts.pynow holds one corrupt header per format as a matrix..vti,.vtsand.vtrhold a grid, and each now says so when handed something else. All three inferred 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 was written as a seven by seven by seven grid of 343 points, with seven rows of point data under a header claiming 343. The file was unreadable - its own reader refused it - but nothing said so at the point of writing. A mesh whose vertices are a grid in some other order was worse, since the count agreed: the file was read back happily with every point attribute sitting on the point mirrored through the diagonal. The extent is now read off the cells instead, which are a grid whatever the coordinates do, and a mesh whose cells are not one raisesCodecErrornaming the shape that was found and pointing at.vtu, which holds an arbitrary mesh.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
CellDatadropped 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 raiseCodecErrorat the point of writing..vtsholds a curvilinear grid: it writes its points, so they need not lie on a lattice at all, and a warped block, a cylindrical shell and an aerofoil O-grid are all StructuredGrids. Taking the extent from the distinct coordinate on each axis meant none of them could be written - a warped 3x3x3 grid counted 27 distinct values on every axis and went out declaring a 27x27x27 one - and reading the extent off the cells is what lets the format hold what it is for..vtrwrites its three coordinate arrays out in full, so nothing says an axis has to ascend. They were taken as the sorted distinct value on each column, which turned a descending axis round underneath its own point data; they are read off the vertices a stride at a time now, and an axis keeps the direction it was given..vtiand.vtskeep the extent of the file they were read from, and.vtithe origin and step as well, so a grid that did not begin at zero - or that steps down an axis - goes back exactly where it stood. All three were trusted after a transform had moved the mesh out from under them, and each fails differently: a grid pruned to one of its cells was written under the extent of the grid it used to be and read back as twenty-seven vertices and eight cells it no longer held, a mesh moved five units went out under the origin it had left, and 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
.vtitook the step on the x axis for the step on all three. A lattice whose planes are unevenly spaced is a RectilinearGrid, 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 axis that is not evenly spaced now raisesCodecErrorpointing at.vtr, which spells its coordinates out.Reading back the empty
.vtsthis codec writes raisedValueError: cannot reshape array of size 0 into shape (0,newaxis). An empty extent gives no column count to infer one from; a file with no points now reads as a mesh with none.Writing
.vtimeasures the grid step on each axis separately. Only the x axis was asked whether it had a second plane to measure against, and y and z were then indexed regardless, so any mesh flat in one of them - a sheet of quads, an image one voxel deep - raisedIndexErrorfrom inside the writer instead of being written, and a mesh flat in x quietly took the default spacing on all three axes rather than the steps it did have. A degenerate axis now keeps the default spacing, which is what VTK reads back for an extent of zero anyway, and a mesh with no vertices at all writes the0 -1extent VTK spells an empty image with - the one the.vtsand.vtrwriters here already spell - rather than the0 0that reads back as a point the mesh never held.Reading a
.vtiwhoseOriginorSpacingspells two numbers where three belong raisedIndexErrorfrom inside the parse. The axes it does give are taken and the rest defaulted, and a bare number given asvti_spacingis taken for every axis rather than subscripted. Any number of them but three warns rather than passing in silence, and a short one is the worse of the two: a dropped fourth number describes no axis, where a missing third is an axis of the mesh given a value the file never spelled. This codec writes no coordinates, so those six numbers are the whole geometry..vtrkeeps the extent of the file it was read from, the way.vtiand.vtsdo. A block that did not begin at zero was slid to the origin on the way out, which is the one thing a.pvtrassembling it next to its neighbours reads. It is checked against the mesh rather than trusted: one a transform has moved the mesh out from under is re-derived from the cells.All three keep the
WholeExtentof the file they were read from as well as their ownExtent. 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,.pvtsor.pvtrassembling it read one neighbour where it should have read several. 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
WholeExtentthat is not six whole numbers now raisesCodecErrornaming the file and the attribute. Unpacked straight into ints, it failed with a bareValueErrorabout a literal instead.A
.vtior.vtrholding a bare grid is readable 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, so a plain 4x4x4 grid went out as a valid 231-byte file and came back asValidationError: declared_n_verts=64 implies 1536 bytes of vertex data. The heuristic is asked only of a format that spells what it declares.A format excused that heuristic is held to a tighter cap in its place. The heuristic is what made the loose one safe to leave loose: a format that writes its points has to spend bytes on each, so a header can only ask for as much memory as the file it sits in is long. An ImageData spends six indices on any number of points, so a 250-byte file could declare a hundred million planes on one axis and be expanded into the 2.4 GB of vertices they come to. The cap clears any grid that expands into a mesh a machine can work with and refuses the rest by name.
Writing
.vtispells itsOriginandSpacingat the width a double reads back at. These six numbers are the whole geometry of an ImageData, and a.10gfield 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
ImageDatademands of an axis is measured against the step, not against the coordinate. A relative tolerance is a fraction of where the axis sits rather than 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 the check and came back regularised, with every plane past the second moved. A drift below what a double holds at that magnitude is still allowed, since no file could record it either way.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, so
0 -1 0 2 0 2came 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.vtsthen handed toreshapeas a second unknown dimension. Such an extent now reads as the empty mesh it describes. Legacy.vtksays the same thing withDIMENSIONSand had the same hole: aSTRUCTURED_POINTSorRECTILINEAR_GRIDof0 3 3came back as four quadrilaterals over no vertices, built from strides that were themselves zero, and a negativeDIMENSIONSwas reported to the caller as a negative count of points.The extent a mesh carries in
global_attrsmay 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 reachedlenand failed with aTypeErrorfrom inside the.vti,.vtsor.vtrwriter. So could avti_originorvti_spacingholding 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..vtrbuilds its vertices from the coordinate arrays and everything else from the extent, and nothing in the file made the two agree. An array longer than its axis expanded into more vertices than the extent declared, while the cells, the offsets and everyPointDataarray stayed sized to the extent - a mesh whose connectivity covered part of itself and whose attributes covered none of it, past every check the reader made. The lengths are compared now, and a mismatch raisesCodecErrornaming the axis. Every axis is asked, including one the extent gives no plane at all: the point count is a product and goes to zero there, but the vertices are the coordinate arrays’ own outer product, so an axis left unchecked expanded0 -1 0 2 0 2into 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..vtsreads the width of its points from the extent and the<Points>array together, so a file whose two disagreed came back as a mesh of the wrong width - one point of no coordinates at all, where the extent declared a point the array did not carry - and only failed later, on a shape nothing in the file explained. Both counts are named in aCodecErrornow. An array wider than three components still keeps its first three.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
stripto take off, so a.meshthe registry had already claimed as MFEM failed for not starting withMFEM mesh.Every spelling of an MFEM header the sniffer accepts is one the reader dispatches on. The registry claims a
.meshfile by its upper-cased header and the reader matched case-sensitively, soMFEM NC MESH v1.0was claimed as MFEM and then refused for not starting withMFEM mesh. The two NC spellings both reach the non-conforming reader as well:MFEM NC-Meshwas falling through to the NURBS one, which read a refinement forest as B-spline control points and warned under the wrong variant’s name..meshbhands back vertices with three columns. A file declaringDimension 2was read into an(n, 2)array, which is not what aPolyDataholds: every consumer indexingvertices[:, 2]- the transforms, the other writers,faces- raised on it. The coordinates are padded withz=0like every other codec’s, and the write side takes itsDimensionfrom the mesh rather than from the width of the array.A tag group naming a vertex or an element that this mesh does not have no longer moves the label onto one that it does.
remove_orphan_vertices,merge_duplicate_verticesandfilter_element_typecarried a group through their index remap after dropping only the members past the end, so a negative index reached the array from the far end and landed on a real item, and a group holding floats - which nothing stops a reader from building - raisedIndexErrorfrom inside numpy instead. All three now go through the same member check the writers use: a member that indexes nothing in this mesh is dropped, and what is left is remapped.The documented
pipeline(filter_element_type(keep="triangle"), ...)raisedTypeError:filter_element_typetakes the mesh first and does not curry. The example now composes it withfunctools.partial.A broken binary file now reports what is wrong with it rather than
BufferError: cannot close exported pointers exist. The formats that parse over a mapping -.ply, legacy.vtk,.meshb- build their arrays as views of it, and a parse that gives up half-way leaves one of those alive in the traceback carrying the failure; unmapping underneath it then raised, and that error replaced the codec’s own. The mapping is left to the last view of it instead, so a corrupt file says the same thing read from a path as it does read from a buffer, where there was never a mapping to unmap.A source that answers a read with less than it was asked for is now read to the end of the request. A bare
readis allowed to come up short - a socket hands back what has arrived, not what was wanted - and the wrapper put in front of a duck-typed handle passed that straight through, so a codec asking for n bytes could silently get fewer and parse the gap as data. A handle fromopen()never came up short, so no codec guarded against it. Nothing is read past what was asked for, so a handle the caller shares still ends up where the codec’s reading left it.OBJ face indices are resolved the way the format defines them: a negative index counts back from what has been declared so far, and an index naming a record the file does not have raises
CodecErrornaming the line instead of wrapping around into a different vertex.OBJ
vtrecords are read. They are indexed per face corner, so a file may hold more of them than it holds vertices; each corner assigns to its vertex, a vertex given two different values keeps the last and warns, and records nothing indexes are kept only when there is one per vertex.vtandvnare written back, so texture coordinates and normals survive a round trip.OBJ writes a bare
gbefore a face that belongs to no group, so it no longer inherits the group of the face above it, and a baregon read clears the active groups rather than inventing adefaulttag.An OBJ file whose
vnorvtrecords cannot be lined up with its vertices now leaves the attribute out. The fold that gives up returned None, and only thevtpath checked for it, sovertex_attrscould hand back a None where an array belongs and the writer raisedTypeErroron it.OBJ writes a number where a vertex has no record. A vertex no face names carries NaN out of the reader, and
vt nan nanis not a record another reader takes; the row nothing indexes is written as zero. Avtarray narrower than two columns is padded rather than indexed past its end.OBJ leaves out a
vnorvtattribute that does not hold one row per vertex, with a warning naming its shape. Only the column count was checked, so an attribute with fewer rows than the mesh wrote faces indexingvtrecords that were not in the file - which no reader can take, this one included: a three-vertex mesh carrying one texture coordinate wrote a file that read back asCodecError. A one-dimensional attribute is now read as one value per vertex rather than as a single row.Legacy
.vtkfiles now readCOLOR_SCALARSandNORMALS, in both the ASCII and the binary flavour. Both used to stop the attribute scan, dropping the array and everything after it without a word. A binaryCOLOR_SCALARScomponent is an unsigned char standing for the 0..1 float an ASCII file writes, and is scaled onto that range, so the same colour reads back the same from either flavour.Legacy
.vtkreadsTEXTURE_COORDINATES, and steps over aLOOKUP_TABLEdefinition rather than stopping at it. Those were the last two attribute keywords the format defines that the scan did not know, and in a binary file an unknown keyword ends the scan: a file carrying either lost every array after it. A palette is not a value per point, so a lookup table becomes no attribute - it is only counted past.A legacy
.vtkattribute keyword the reader does not know now warns that it and everything after it in that section are being dropped. Its payload is binary of unknown length, so the scan still cannot go on, but a short read is no longer a silent one.NORMALSandCOLOR_SCALARSare read fromSTRUCTURED_POINTS,STRUCTURED_GRIDandRECTILINEAR_GRIDfiles too. Those three datasets scan their attributes themselves rather than through the shared parser, and knew onlySCALARS,VECTORSandFIELD.A binary legacy
.vtkattribute that runs past the end of the file now raisesCodecErrornaming the array and the bytes that are missing. The slice came up short in silence and the reshape after it failed with aValueErrornaming neither the array nor the file.A legacy
.vtkattribute section that declares more values than the file holds raisesCodecErrornaming the array and the count, rather than running off the end of the line list with anIndexErrorthat names nothing. This coversSCALARS,COLOR_SCALARS,VECTORS,NORMALS,TENSORSandFIELD.A Nastran real field is now written in whatever spelling fits it. Bulk data allows the exponent’s
Eto be dropped when its sign is there (1.234-10), and nothing reads+07differently from+7, so the writer offers both - three columns back on the explicit form, which is three more significant digits in an eight-column field. A value at the top of the double range is stepped one digit toward zero rather than refused, since rounding it to nearest overflows to infinity. No finite coordinate is refused by a field any more.A Nastran real field prefers a spelling that reads back as the value it was given. The search spent precision one digit at a time and took the first form that fit, so a mantissa stepped toward zero could win a field an exact form two digits shorter would also have fit:
1e7went out as9999999.where1.E+07was available, and1e15as999999999999999.in a large field. Exact spellings are now swept for first, at every precision, and only a value no field can hold exactly - the top of the double range - falls through to the closest one.The XML writers declare
version="1.0"in the<VTKFile>header rather thanversion="0.1", which no VTK release ever defined. Reading a file that declares0.1is unchanged.A
<DataArray>polyxios cannot decode - atype="String"label array, or any type it does not know - is skipped with a warning naming it, and the arrays around it are still read. It used to vanish without a word.A legacy
STRUCTURED_POINTSfile keeps itsDIMENSIONS,ORIGINandSPACINGinglobal_attrsasvtk_dimensions/vtk_origin/vtk_spacing, andSTRUCTURED_GRID/RECTILINEAR_GRIDkeep theirDIMENSIONS. Expanding the header into a point array used to throw the grid away.A
.vtuor.vtpPiecethat declares points and does not deliver them raisesCodecError, whether its<Points>element is short or missing altogether. It used to be skipped, leaving the piece’s cells indexing points that are not there and every later piece shifted by the count that never arrived.A point or cell array that covers only some of a multi-piece
.vtuor.vtpfile is dropped with a warning naming it. Joining the pieces that carried it gave an array shorter than the mesh, whose rows then sat against the wrong points from the second piece on. An array the pieces shape differently - one calling it scalar and the next a vector - is dropped the same way, with its shapes named, rather than raising a bareValueErrorfromnumpy.concatenate.A
CELL_DATAsection is read fromSTRUCTURED_POINTS,STRUCTURED_GRIDandRECTILINEAR_GRIDfiles. Those three walk their own attributes, and the chain that did it asked only about points, so every cell array fell past it in silence - the cell scalars in VTK’s ownSampleStructGrid.vtkamong them. The three now share one scanner, which also gives themTEXTURE_COORDINATESandTENSORS. An array whose declared length matches neither the points nor the cells is dropped with a warning rather than reachingPolyDataas a validation error about lengths.A structured legacy
.vtkgrid extends along whichever axes it declares. ADIMENSIONS 3 1 3sheet was read as two lines over the first two points instead of four quads over all nine, and a column alongyorzwas read with the stride of a row alongx. Only grids flat inzand rows alongxcame out right.An attribute keyword one of the structured readers does not handle now warns that its array is being dropped, the way the shared parser’s binary scan already did. Skipping the line does not step over the payload, so in a binary file the scan carries on inside it.
An unknown attribute keyword in an ASCII legacy
.vtkfile warns as well. Only the binary scan said anything; ASCII skipped the keyword and its value lines without a word.A legacy
.vtkattribute section whose values run into the next header raisesCodecErrornaming the array, the line and what it holds, rather thanfloat()answering with a bareValueErrorthat names neither the array nor the file.A binary attribute in a structured legacy
.vtkfile is checked against the end of the file before it is sliced, which the shared parser already did. A truncated file gave a nameless reshapeValueError.A
.vtuor.vtpPointsarray whose size is not a whole number of tuples per point raisesCodecErrornaming the Piece. Only a short array was caught, so ten values for three points reachedreshapeand came back as aValueErrornaming neither the file nor the Piece.A
v,vnorvtrecord in an OBJ file that does not carry the components its directive needs, or carries something that is not a number, raisesCodecErrornaming the line. Both used to reach the caller as a bareIndexErrororValueError. Avtcarrying a third component - the depth of a volumetric texture - keeps the two a surface uses rather than making the records ragged.Writing an OBJ
vertex_attrsentry that holds no numbers - a label per vertex, say - drops it with a warning instead of raising out ofnumpy.asarray.Legacy
.vtkfiles written by VTK 5.1 - the default since VTK 9.0 - are read. The two numbers on a v5.1CELLSline are the length of theOFFSETSarray and the length ofCONNECTIVITY, not the cell count; reading the first as a cell count ran the offsets into theCONNECTIVITYkeyword and answered with a bareValueError. The offsets are now counted up to that keyword, so files spelling the line either way are read.POLYDATAgained the layout altogether: itsPOLYGONS,LINES,VERTICESandTRIANGLE_STRIPSsections knew only the v4.2 form, so no polydata file VTK 9 writes could be read at all.Writing a v5.1 legacy
.vtkfile declares the length of itsOFFSETSarray on theCELLSline. It declared the cell count, which VTK’s own reader takes literally: it stopped with “Error reading cell array connectivity header” and returned a mesh with no cells. Files polyxios wrote before this still read.A
METADATAblock is stepped over. Every VTK writer since 4.2 puts one after each array, and it is text even in a binary file. In ASCII it warned twice about keywords it named as dropped; in binary it ended the attribute scan, so a file with two arrays came back with one. Inside aFIELDblock it was read as an array header, which took the array after it with it.A binary
STRUCTURED_GRIDkeeps the section that follows its points. The line cursor was stepped past the payload and then once more over the newline that ended it, so whichever section came next - aCELL_DATAwritten beforePOINT_DATA, as VTK writes it - was skipped.A
RECTILINEAR_GRIDfollows its coordinate arrays rather than itsDIMENSIONSheader. The points are the outer product of the three arrays, so a header that disagreed with them generated cells indexing points that do not exist and dropped attributes that covered every point there is. The header is now checked against them, warned about when it differs, and the arrays win..vti,.vtsand.vtrhonourNumberOfComponentswhen reading an attribute. A three-component array on 27 points came back as 81 rows, which belongs to no mesh and failsvalidate;.vtuand.vtpof the same family already cut it into tuples..vtrdeclares the component count of the attributes it writes, so a vector survives a round trip, and writes each array in the type its<DataArray>declares. Everything was cast to float64 under whatever header the original dtype produced, so anInt32attribute read back as the bit pattern of a double.A
.vtuor.vtpPointsarray of a type that holds no numbers -type="String", or any type this reader does not know - raisesCodecErrornaming the type. It warned that the array was skipped and then blamed the point count for the zero values that left.A
.vtrattribute of a dtype no VTK type names - a boolean mask, a float16 - is written as theFloat64its header declares. Only the header fell back; the bytes stayed the dtype’s own, so eight booleans went out as eight bytes under aFloat64header and read back as one garbage double.Every XML writer declares the whole width of a tuple, not the second dimension of the array holding it. A
(n, 3, 3)tensor attribute - what a legacyTENSORSsection reads back as - was declared as one component in.vti,.vts,.vtpand.vtuand as three in.vtr, so nine times the rows came back and belonged to no mesh.The XML writers cast to little endian before taking the bytes, which is what the
byte_orderthey all declare says those bytes are. On a big-endian machine every binary array went out reversed..vti,.vtsand.vtrdrop an attribute whose rows cannot be matched to the mesh, with a warning naming it, as.vtuand.vtpalready did. It used to reachPolyDataand failvalidatewith a message about lengths rather than about the file.A binary legacy
.vtkfile holds itsTENSORSas binary. Both tensor branches of the writer - the 3x3 one and the Voigt 6-component one - spelled their numbers whatever was asked for, so a binary file carrying either had a run of ASCII in the middle of it and came back as aCodecErrorabout a short block.The ASCII writers spell a value as the shortest decimal that reads back as it, rather than to ten significant digits. A legacy
.vtk,.vti,.vts,.vtpor.vtrfile declaringdoubleorFloat64held seven digits fewer than that, so coordinates came back about 1e-10 off what was written; they are now exact.A
METADATAblock written without its blank terminator ends at the geometry keyword after it as well as at an attribute one. Left open at the end of aPOINT_DATAsection it swallowed theCELLSthat followed and every line to the end of the file - the failure the terminator search was added to prevent.A malformed attribute section in a structured legacy
.vtkfile costs the section rather than the mesh. A count running past the end of the file raisedCodecErrorout of a read whose geometry was already whole, and those sections were skipped entirely before they were read at all, so files that used to load stopped loading.A
.vtuor.vtpPiecethat cannot be read is named by its index. A file of forty pieces gave no way to find the one at fault.An OBJ vertex named twice with different values is warned about whichever component they differ in. The check asked whether the first component had been written yet, so a record whose first component is NaN hid every conflict on that vertex.
Writing an OBJ
vnorvtattribute wider than the record says how many components are being left out, rather than truncating in silence.A legacy
STRUCTURED_GRIDwhoseDIMENSIONSdoes not cover itsPOINTSarray hands the points back without cells, warning about the two counts. The cells are strides through the layout the header describes, so a header naming more points than the file delivered generated connectivity indexing points that are not there: the read returned aPolyDatathat failsvalidate.RECTILINEAR_GRIDalready reconciled the two.An attribute section is read by the count its own header declares rather than by the count of the mesh. The two agree in a well-formed file, and where they do not the section’s is the only number that says where one array ends and the next begins - reading by the mesh’s walked an array straight into the header after it. An array that then covers no point or cell of the mesh is dropped with a warning naming it, as the structured readers already did.
A binary legacy
.vtkSCALARSsection without aLOOKUP_TABLEline - which the format leaves optional - reads its own values. The line was consumed unconditionally, so the payload up to its first0x0abyte was swallowed, and a payload holding none rewound the scan to the top of the file.A binary legacy
.vtkPOINTS,CELLSorCELL_TYPESblock is checked against the end of the file before it is sliced, the way the attribute blocks already were. The whole-file bound the header check applies clears a block that still runs off the end - a file with a long comment header, say - and the reshape after the short slice failed with aValueErrornaming neither the array nor the file.A legacy
.vtkattribute header missing a field, or spelling a count as something that is not a number, names the file and the line it is on.SCALARSwith no array name reached the caller asIndexError: list index out of rangeandSCALARS s float xas a bareValueError, in the ASCII scan, the binary scan and the structured one alike. A binary file has no line to name, so the byte offset stands in for one..vti,.vts,.vtpand.vtuwrite each attribute in the type the array is held in, as.vtrdoes. Everything was cast to a double under aFloat64header, so anint64identifier past 2**53 came back a different number.The ASCII body of a
<DataArray>is parsed into the type the element declares rather than throughfloat()first. AnInt64array was rounded to a double before the declared type ever saw it, which the top of the integer range does not survive.A binary legacy
.vtkblock is read as the type its header names. APOINTS n intwas read at four bytes a float, so an integer point array came back as coordinates the file never held, and a type name the reader has no numpy equivalent for -bit, or a misspelling - was guessed at rather than refused. Every binary header now resolves its type the same way and raisesCodecErrornaming it when it cannot; the names VTK writes for 64-bit and signed-char arrays were missing from the table and are there now. An ASCII payload is unaffected: its values are text whatever the header calls them.A legacy
.vtkgeometry or section header spelling a count as something that is not a number, or leaving it out, names the file and the line it is on.POINTS,CELLS,CELL_TYPES,POINT_DATA,CELL_DATA,DIMENSIONS,ORIGIN,SPACINGand the coordinate arrays reached the caller as a bareValueErrororIndexError, which the attribute headers had already stopped doing.A
CELL_DATAarray in a legacySTRUCTURED_GRIDis measured against the cells the mesh ends up with rather than the cellsDIMENSIONSdescribes. A header itsPOINTSarray does not cover leaves the mesh with no cells, and the array was kept against the header’s count, so the read returned aPolyDatathat failsvalidate.An OBJ face index spelled with a superscript digit raises
CodecErrornaming the line.str.isdigitadmits²andint()then refuses it, so the fast path let a bareValueErrorout; the test is nowstr.isdecimal, which still admits the non-Latin digitsint()reads.A bare
mtlliborodirective in an OBJ file names nothing rather than the empty string, which used to be written back as a directive with nothing after it.Writing an OBJ
element_attrs['material']that does not cover the faces drops it with a warning naming its length, the way the vertex attributes already were. Indexed per face it ran off the end partway through, leaving a half-written file and anIndexErrornaming an axis.A
.vtuor.vtpPiecewhoseNumberOfPointsis not a count, and a.vti,.vtsor.vtrExtentthat is not six whole numbers, raiseCodecErrornaming the file. They reached the caller as a bareValueErroraboutint()or about unpacking.A
<DataArray>whoseNumberOfComponentsis not a count is read flat with a warning naming it, rather than raising a bareValueErrorfromint().A legacy
.vtkfile whose header declaresDATASET FIELDis read. The dispatch asked what the line starts with while the line still carried itsDATASETkeyword, so the branch never ran and every field data file was refused by the one below it.A
METADATAblock inside a v5.1CELLSsection is stepped over. VTK follows a cell array with one, so a block sat between the offsets and the connectivity of files every release since 9.0 writes; read as offsets it raisedCodecErrorabout a line of words where numbers belong.A v5.1
CELLSsection is found by what follows the header rather than by the version in the first line. Versions were compared as strings, so a file declaring10.0sorted below5.1and its offsets would have been read as a v4.2 cell stream. The binary scan already asked this way.A
.vti,.vtsor.vtrextent flat along an axis - an image one voxel deep - is a sheet of quads, or a run of lines when it is flat along two. All three read it as a grid of no cells, which left everyCellDataarray belonging to nothing.An ASCII
<DataArray>holding a value its declared type is too narrow for wraps with a warning naming the array, the way a C reader wraps it, rather than escaping as a bareOverflowErrorfrom numpy. A token that names no number at all raisesCodecErrornaming the array.Writing a point or cell attribute of a kind no
<DataArray>can hold - a label per vertex, say - raisesCodecErrornaming the array and its dtype, rather than aValueErrorabout one element.The warnings the
.vtk, XML and OBJ codecs raise are blamed on the code that asked for the file. Every one of them pointed a frame short, atpolyxios.readorpolyxios.writeitself, which tells a caller nothing about which of their own calls found the file.
Optimizations#
A legacy
.vtkwrite no longer builds the whole of a mesh as one string or one array before writing it. Every block is written in runs, so the bytes are the ones a single join produced while the memory a write holds above the mesh is a fixed run whatever the mesh’s size. A run of cells that share a node count - which is every mesh of one element type - is laid out as rows of an array rather than a cell at a time, and both spellings share that layout.CELL_TYPESis translated by indexing a table built once at import rather than by a dictionary hop per cell. Writing 400k triangles: spelled, 923 ms and 54 MB above the mesh became 431 ms and 6 MB; binary, 564 ms and 128 MB became 15 ms and 10 MB.Reading an OBJ file resolves a face corner inline when it names a plain index inside what has been declared, which is what nearly every corner does; the rest still go the long way round, where the message naming the line lives. A mesh of any size has millions of corners, and the call this saves is most of what checking them cost. A
v,vnorvtrecord carrying exactly the components its directive spells skips the padding and the slice that feeds it.A
float32attribute is spelled at its own width in an ASCII<DataArray>. Widened to a double first,0.1went out as0.10000000149011612- seventeen digits of a value carrying seven - which is nearly twice the file for the same numbers.Writing a Nastran large-field deck is roughly ten times faster. Sweeping for an exact spelling asked every precision from seventeen digits down, spelling and parsing candidates at each; rounding to fewer significant digits than
reprcarries cannot be exact whatever form it takes, so the sweep now stops there. Neither sweep spells a candidate at a precision the field could not hold in the first place, and the stepped mantissas - the expensive half - are built only when the rounded ones have all missed. Twenty thousandGRID*cards went from 10.3 s to 1.0 s, and every value is written exactly as before.Reading an OBJ file no longer names its source once per line. The name costs a path walk and was only ever used to spell an error message.
A Nastran real field below one drops its leading zero when the column it costs is a significant digit. Bulk data reads
.5as0.5, so a third in an eight-column field goes out as.3333333rather than0.333333.Writing an OBJ file looks its material attribute up once rather than once per face.
Stepping past a binary payload in a structured legacy
.vtkfile is a binary search over the line offsets rather than a walk. A binary payload carries a newline every few values, so the lines it is cut into number in the thousands for a grid of any size; a 5 MBSTRUCTURED_POINTSfile reads about a fifth faster..vti,.vtsand.vtrbuild their hexahedra with array arithmetic instead of a Python loop per cell. The corners of a cell are strides from its own origin, so the whole connectivity is eight adds over the origins; a 40x40x40 grid builds about nine times faster.The structured legacy
.vtkreaders cut their file into lines in one pass rather than afindper line, and they all do it in the same place. The lines are byte-identical to what the walk produced.Folding OBJ
vtandvnrecords onto their vertices is one pass over the corners rather than a numpy row assignment per corner.A legacy
.vtkASCII payload is spelled and written in one pass instead of a formatted write per point, which was a syscall per line.
Tests#
Regression tests for the dtype a
.vtrheader declares against the bytes under it, the component count of a tensor attribute in every XML writer, a binary legacyTENSORSsection, an unterminatedMETADATAblock followed by geometry, an attribute section declaring more than its file holds, an OBJ conflict hiding behind a NaN first component, and thePieceindex in a.vtuerror.test_a_double_section_holds_every_digit_of_a_doublewrites a coordinate no ten-digit spelling can hold and asks for it back unchanged.test_a_power_of_ten_is_written_exactlyspells its powers as literals rather than computing them with**.powis not correctly rounded everywhere - glibc and MSVC answer10.0 ** 23with the double one unit above1e23, macOS with1e23itself - and that neighbour needs seventeen significant digits, which no eight or sixteen character field can hold. The test asked the writer for a field wider than the format has, and failed on Linux and Windows only.A cross-codec round-trip matrix (
tests/test_roundtrip.py) writes and re-reads five canonical meshes through every writable codec, checked against a table declaring exactly what each format keeps. A new codec cannot join the registry without an entry.
GitHub stats for 2026/08/18 - 2026/09/09 (tag: v0.3.0)
These lists are automatically generated and may be incomplete or contain duplicates.
The following 1 authors contributed 89 commits.
Serge Koudoro
We closed a total of 18 issues, 17 pull requests and 1 regular issues.
Pull Requests (17):
PR #60: DOC: the README and the guides say what 0.4.0 added
PR #59: NF: OBJ keeps the texture coordinates a seam gives two of
PR #56: TEST: the regression guards are named for what they guard
PR #57: MNT: update pre-commit hooks
PR #55: NF: whole-mesh metadata, tag groups and face sets across the codecs
PR #54: BF: .vti, .vts and .vtr hold a grid, and say so when handed something else
PR #53: NF: add a Kratos MDPA codec
PR #51: NF: meshes keep the numbers their file gave (P2.4)
PR #52: MNT: update pre-commit hooks
PR #49: NF: one rule for two-dimensional meshes (P2.3)
PR #50: MNT: update pre-commit hooks
PR #48: NF: topological dimension and duplicate-vertex welding
PR #47: BF: P1 format correctness - Nastran, Medit, Abaqus, Gmsh, PLY/STL, Tecplot
PR #46: BF: OBJ, legacy VTK, VTK XML and Nastran correctness
PR #45: NF: buffer/file-handle IO and transparent gzip
PR #44: NF: Handle *.dat via a sniffer to redirect to the correct codec
PR #43: CI: publish versioned docs from the tag push
Issues (1):
GH#58: OBJ codec: texture coordinates (vt) parsed but not stored in vertex_attrs
0.3.0 (2026-08-18)#
Fifteen new mesh formats, a real command line interface and a versioned documentation site.
New features#
Fifteen new codecs, taking the registry from 16 to 34 recognised extensions:
Abaqus
.inp(C3D4 / C3D8 / S3 / S4 and friends).AVS-UCD
.avs, preservingmat_id.DOLFIN / FEniCS XML
.xml.FLAC3D
.f3grid(zones and faces).Gmsh
.msh- ASCII v2 read/write, v4.1 read, physical groups kept.Medit binary
.meshb(GmfLib v1 and v2, zero-copy mmap).Nastran
.bdf, also registered for.nasand.fem, reading free, small and large field formats.Netgen
.vol(points, edges, faces, cells, tags).OFF
.off(ASCII and binary, colours, normals, texture coordinates).STL
.stl(binary and ASCII, lazy loading for binary).SU2
.su2(VTK element codes, boundary markers).Tecplot
.tec(ASCII FE zone, POINT and BLOCK ordering).TetGen
.node+.elepairs (markers, regions).UGRID
.ugrid(AFLR ASCII, boundary tags).WKT
.wkt(Well-Known Text).
pxioscommand line interface withfetch,list,convertandvizsubcommands. Visualization requires the optionalvizextra (pip install polyxios[viz]).polyxios.read_polydataandpolyxios.visualize_meshhelpers, pluspolyxios.supported_extensionsto introspect the codec registry.Versioned documentation at https://polyxios.org, with a version switcher, a credits page generated from the git history, and a page per format.
Changes#
The fetcher now resolves assets through a remote
models.jsoncatalog and downloads files individually, instead of pinned per-format release zips. Downloads are checksum-verified and the result is cached across runs; setPOLYXIOS_MODELS_URLto pin the catalog to an immutable URL.pxios fetchgained--verbose, requires asha256for every asset and resolves destination paths defensively.ASCII writing is vectorised, and the docs build now treats Sphinx warnings as errors.
Bug fixes#
Fetcher: rejected zip-slip paths, non-HTTPS URLs and stale cache entries, added timeouts, retried dropped transfers, and stopped
example --listfrom downloading the whole pack.STL: fixed a file-descriptor leak, ASCII decoding, lazy-format detection, trailing-data handling and zero normals on degenerate triangles.
WKT: fixed ring grouping, decoding and degenerate rings, and hardened parsing and hole encoding.
Gmsh: corrected the element type codes.
Nastran: fixed marker parsing, exponent handling and write precision.
resolve()now accepts a dotlessfmtoverride.Fixed multi-channel vertex attributes in
mergeandvertex_colors, offset validation and ASCII newlines in the VTK XML path, and restored multiblock partial loading and visualization colours.
GitHub stats for 2026/06/25 - 2026/08/18 (tag: v0.2.0)
These lists are automatically generated and may be incomplete or contain duplicates.
The following 3 authors contributed 97 commits.
Praneeth Shetty
Serge Koudoro
dependabot[bot]
We closed a total of 29 issues, 29 pull requests and 0 regular issues.
Pull Requests (29):
PR #43: CI: publish versioned docs from the tag push
PR #42: NF: warnings as error
PR #41: MNT: update pre-commit hooks
PR #40: DOC: SEO metadata, em dash removal, copy icon, mobile fixes
PR #39: NF: new polyxios website
PR #38: NF: add Netgen .vol codec
PR #37: NF: add UGRID ASCII .ugrid codec (AFLR format, boundary tags)
PR #36: NF: add TetGen codec
PR #35: NF: add SU2 codec
PR #34: NF: add Tecplot ASCII .tec codec (FE zone, POINT + BLOCK)
PR #33: NF: add OFF codec (ASCII + binary, colours/normals/texcoords)
PR #32: NF: add Nastran .bdf codec (free/small/large field read)
PR #23: NF: Add WKT (Well-Known Text) codec
PR #31: NF: Adding gmsh codec
PR #21: NF: add FLAC3D .f3grid codec (T4/P5/W6/B8 zone types)
PR #20: NF: Adding CLI support pxios
PR #29: MNT: update pre-commit hooks
PR #28: MNT: Bump pypa/cibuildwheel from 4.1.1 to 4.2.0 in the actions group
PR #26: MNT: update pre-commit hooks
PR #27: MNT: Bump pypa/cibuildwheel from 4.1.0 to 4.1.1 in the actions group
PR #25: MNT: Bump actions/setup-python from 6 to 7 in the actions group
PR #24: MNT: update pre-commit hooks
PR #22: MNT: update pre-commit hooks
PR #19: Feat/dolfin codec
PR #18: NF: add Medit .meshb codec (GmFlib binary, v1/v2, lazy mmap support)
PR #17: NF: add AVS-UCD .avs codec (ASCII, mat_id preserved)
PR #16: NF: add Abaqus .inp codec
PR #15: MNT: update pre-commit hooks
PR #14: NF: add STL codec with binary/ASCII read-write and lazy binary support
Issues (0):
0.2.0 (2026-06-25)#
First public release of polyxios.
New features#
Plugin-based codec registry via Python entry points - third-party packages can register mesh formats without patching polyxios.
VTK legacy (
.vtk) and XML (.vtu,.vtp) reader/writer with ASCII and binary (raw + appended) encoding.VTR appended format support.
MFEM mesh codec (
.mesh).MEDIT mesh codec (
.mesh).polyxios convertandpolyxios visualize-meshCLI commands.Lazy / memory-mapped loading for binary formats (
read(..., lazy=True)).polyxios.__version__exposes the full version string including the git commit hash for development builds (e.g.0.1.0.dev0+git20260623.101006a).
GitHub stats for 2026/05/26 - 2026/06/25 (tag: None)
These lists are automatically generated and may be incomplete or contain duplicates.
The following 4 authors contributed 47 commits.
Maharshi Gor
Praneeth Shetty
Serge Koudoro
skoudoro
We closed a total of 12 issues, 12 pull requests and 0 regular issues.
Pull Requests (12):
PR #12: NF: add MFEM mesh codec (.mesh)
PR #9: NF: Handle the other vtk formats
PR #11: MNT: update pre-commit hooks
PR #10: BF/NF: fix PLY binary reader + add SPLAT codec and compressed 3DGS support
PR #8: BF: handling VTK files improvements
PR #4: Fix: vtk codec to read ascii polydata and support v1.0
PR #7: CI: Avoid cron job on fork
PR #6: MNT: update pre-commit hooks
PR #3: NF: Adding Data Fetcher
PR #5: MNT: update pre-commit hooks
PR #2: DOC: Replace arrow and em-dashes with dash
PR #1: NF: initial framework from polyxios
Issues (0):