Changelog#
0.5.1 (2026-10-15)#
Bug fixes#
A binary legacy
.vtkPOLYDATA file whose cell sections carry noOFFSETSblock no longer reads with an empty first cell ahead of its own, which shifted every cell one slot: a file of 13 streamlines read as 14 cells. The ASCII and binary readers now agree on such a file.
Packaging#
A release publishes to PyPI only from
fury-gl/polyxios. A tag pushed to a fork still builds its wheels and its GitHub release, where the PyPI job used to run and fail.
GitHub stats for 2026/10/11 - 2026/10/15 (tag: v0.5.0)
These lists are automatically generated and may be incomplete or contain duplicates.
The following 1 authors contributed 7 commits.
Serge Koudoro
We closed a total of 3 issues, 3 pull requests and 0 regular issues.
Pull Requests (3):
PR #111: BF: binary VTK POLYDATA reads no empty cell ahead of its cells
PR #110: MNT: update pre-commit hooks
PR #108: BF: spell XML-forbidden regex ranges as escapes in a raw string
Issues (0):
0.5.0 (2026-10-11)#
New features#
PCD (
.pcd), the Point Cloud Library’s format that PCL, ROS and Open3D exchange clouds in, is read and written in all three of its bodies:ascii,binaryandbinary_compressed, the last packed with LZF, which polyxios now ships as a compiled module with a pure-Python twin behind it.x y zare the vertices,normal_x/y/zfold intonormals, a packedrgborrgbaintocolorsin 0..1 (in the integer spelling an ascii body uses and the float-bits one a binary body does), and every other field is a vertex attribute of its own name and dtype, a vector when itsCOUNTis above one. An organised cloud keepspcd_widthandpcd_height, a sensor posepcd_viewpoint; padding fields are skipped and NaN points kept. Write picks the body withdata_format=andF 8coordinates withdouble=True.lazy=Truemaps a binary file: coordinates, normals and every field are views of it in the file’s own dtypes.LAS (
.las), the ASPRS LiDAR exchange format, is read and written in versions 1.0 to 1.4 and point formats 0 to 10, and.laz, its LASzip-compressed twin, through lazrs behind the newlazextra. The scaled integer coordinates become float64 vertices;intensity, the return and classification bit fields, the scan angle (scan_angle_rankin degrees before format 6,scan_anglein 0.006-degree steps from it),user_data,point_source_id,gps_time,nirand the wave packet fields are vertex attributes in the file’s own dtypes,red green bluefold intocolorsin 0..1, and extra bytes read by the name and type their VLR gives them, scaled and offset when it says so. An OGC WKT VLR iscrs_wkt, the GeoTIFF key VLRslas_geokeys, every other VLR or EVLR raw inlas_vlrs; the scale, offset, version, point format and header strings arelas_*globals. Write picks the point format from the attributes present or takespoint_format=, the version from the format orversion=,scale=andoffset=from the globals or the cloud, and sends every other numeric attribute out as an extra bytes field.lazy=Truemaps an uncompressed file: the vertices are the int32 the file holds, the colours itsuint16and every field a view of its column.VTKHDF (
.vtkhdf), VTK’s own HDF5 layout and the file ParaView is moving to, is read and written behind thehdf5extra. AnUnstructuredGridreads as its cells are, aPolyDatawith its vertices, lines, polygons and strips typed by point count, anImageDataexpanded into the lattice its extent spans with thevti_*globals a.vticarries; every partition is merged and taggedpart_<k>,PointData,CellDataandFieldDataare the attributes and globals, and a temporal file reads one step -step=on read, every step throughhelper.read_time_series. Write emits anUnstructuredGrid, or aPolyDatawithpolydata=Trueor when the mesh was read as one,time=aStepsgroup of one, andhelper.write_time_seriesappends each step’s arrays to one file, the cells written once and the points again only when they moved. Verified both ways against VTK’s own reader and writer, in the interop CI job as well as locally.COLLADA (
.dae), the Khronos XML scene format every modelling tool of its decade exports, is read and written as aSceneData. Each<geometry>is a mesh: triangles, polylists, polygons, lines, line and triangle strips and fans, with a corner split into its own vertex where its normal, texture coordinate or colour differs,NORMAL,TEXCOORDandCOLORinputs asnormals,texcoordsandcolors, and each block’s material symbol resolved through the instancing node’s<bind_material>intoelement_attrs["material"]; a geometry two instances bind to different materials gets a mesh per binding. Effects read as materials (diffuse, emission, transparency and its mode, textures through sampler and surface, an FCOLLADA bump as the normal texture, the phong pieces inextras), images by uri,<hex>ordata:URI. Nodes keep the transform elements they were spelled with. Every count and reference is checked by name before anything is allocated, an XML entity is refused in any encoding, and nested<instance_node>copies are capped by the document’s size. Write spells 1.4.1 with fixed timestamps, so a scene always writes the same bytes, refuses connectivity a reader could not read back, and writes a node shared by two parents once in<library_nodes>, instanced from there.read()flattens with a warning,lazy=Trueraises.COLLADA animations are read and written.
<animation>channels land underglobal_attrs["animations"]in the shape glTF uses, each target holding the node, the path, the transformsidand its member; a channel addressing an instanced node gets a target per copy, capped by the document’s size. A write targets the transform element holding the channel’ssid. Channels without one on a node written as a single<matrix>- a glTF animation’s translation, rotation and scale - are baked into matrix keys, with extra keys where a matrix blend would drift from the rotation, so a glTF animation goes to COLLADA and comes back as translation, rotation and scale channels holding its values at its key times.COLLADA skins are read and written. An
<instance_controller>gives its meshjointsandweights(the four heaviest influences per vertex) and its node askinindex intoglobal_attrs["skins"], each entry holding the joints bound to node indices, the inverse bind matrices and the bind shape matrix in the shape a glTF read gives. Joint names bind by sid under the instance’s<skeleton>roots, else to the nearest node holding them, so two rigs sharing bone names and each copy of an instanced character bind their own. A geometry skinned by one instance and not another gets a mesh per use. Write builds a<controller>per skinned mesh, names each joint by sid where a reader cannot mistake it and by id elsewhere, and spells the rig’s top joints as<skeleton>roots, so a rigged glTF scene keeps its skin through COLLADA.A glTF read decodes each skin’s
inverseBindMatricesaccessor intoinverse_bind_matrices(row-major, one per joint), so a format that stores the bind pose by matrix can write a skinned glTF scene. A skin with no joints, or whose accessor is missing, is not float MAT4, has nobufferView(all zeros), cannot be read or holds fewer matrices than joints, is kept undecoded with a warning rather than failing the read.glTF
write_scenewrites skins, so a skinned scene keeps its rig on its way back to glTF: each entry ofglobal_attrs["skins"]becomes a glTF skin with itsname,joints,skeleton,extrasand itsinverse_bind_matricesas a float MAT4 accessor, and a node’sextras["skin"]itsskin. Abind_shape_matrix, which glTF lacks, is folded into the inverse bind matrices. A skin glTF cannot hold (joints that are not distinct nodes under one root, matrices that are not one finite affine 4x4 per joint, an accessor never decoded) is left out with a warning, as is a node’s skin when it has no mesh or its mesh’sjointsandweightsdo not fit it. Joints that are not whole numbers in 0..65535 are no longer cast into wrapped indices, nor influences that are not four per vertex written as another accessor type: they and their weights are left out with a warning. uint8 and uint16 weights are written normalised rather than as raw counts, and a second influence set (joints_1,weights_1, …) is written asJOINTS_1andWEIGHTS_1rather than dropped, so a vertex with eight influences keeps weights summing to one. A mesh’smesh_nameand the scene’sextrasare written; vertex and element attributes, tags and globals glTF has no place for, once dropped silently, now warn. A value that is not JSON raisesCodecErrorrather than a bareTypeError, and a NaN in one no longer blames the mesh data.A glTF material written to COLLADA keeps its PBR terms. Metallic, roughness, the alpha mask and its cutoff, the metallic-roughness and occlusion textures and a tint beside a texture, which the common profile has no slot for and a write lost, some without a warning, go into an
<extra><technique profile="polyxios">the reader restores them from; other readers skip it and see the phong approximation. A term that is not a finite number is refused on write, and one in the extra that is not finite numbers of the right count is ignored on read with a warning.Tangents cross between glTF and COLLADA. A COLLADA write spells a glTF tangent as a
TEXTANGENTof its xyz and, when the mesh has normals, aTEXBINORMALon the side itswgives; a glTF write rebuildsTANGENTfrom the first complete frame of set 0, or a lone tangent, where every tangent set used to be dropped. A frame glTF cannot hold - no normals, a tangent of zero or non-finite length, a shape other than four numbers per vertex - is left out with a warning, so a glb -> dae -> glb round trip keeps its tangents.glTF write rebuilds COLLADA transform animations as translation, rotation and scale. A channel on a
<rotate>angle, a<translate>member or a<matrix>beside other elements was skipped, so rigs from Blender, Maya or 3ds Max lost their animation; each node’s channels are now applied to the elements it was read with, composed at the union of their key times and split. STEP keys keep their steps, BEZIER keys are sampled along their curve in each of the three tangent layouts exporters write, and spans are split until glTF’s straight blend and slerp stray from COLLADA’s by at most a thousandth of the range or 15 degrees, capped per write. Animations that animate the same values become separate clips. The element matrices and the TRS split live inpolyxios._trs, shared by both codecs.PVD (
.pvd), ParaView’s collection of datasets over time, is read and written. Every dataset at onetimestepis read through its own codec and merged, tagged bygrouporpart_<n>, with the step’s time underglobal_attrs["time"];step=picks the step andhelper.read_time_seriesreads them all. Write puts one.vtu- or the extensionformat=names - per step beside the index.ASCII point clouds (
.xyz, Leica.ptsand.ptx) are read and written. Columns past the coordinates are named by count and kind - an intensity, three whole numbers in 0..255 ascolors, three others asnormals, anything else kept whole asextra- with comment lines and a column-label line skipped and the body parsed by numpy in one pass..ptshonours and checks its count line..ptxreads every scan, applies each one’s transform, drops the0 0 0 0.5invalid cells with a warning, tags the scans invertex_tagswhen there are several and keeps the matrices and grid sizes inglobal_attrs. A nameless buffer takesvariant=to say which of the three to write.read(..., lazy=True)hands back arrays that view the file’s own mapping, read-only and in the dtype and byte order the file holds, for every array a format stores as one run of bytes in the shape the mesh needs. VTU and VTP files with a raw appended section map their points, connectivity and attributes; the writers gainappended=Trueto emit that layout, a third smaller than base64, and declareheader_type="UInt64"when a block passes four gigabytes. XDMF maps a binary sidecar, and an HDF5 dataset stored contiguously without a filter through the offset h5py reports..splatmaps everything, a binary legacy VTK v5.1 file every block and a v4.2 file its points and attributes, and binary PLY and Medit.meshbtheir vertex records. What cannot be a view is built in memory and the guide says which; what cannot be mapped at all raisesLazyReadErrornaming why. Before this, every lazy read copied its arrays like an eager one and the guide said otherwise.Reading a VTK XML file no longer walks its cells in Python nor re-slices the appended section for every array, and a binary legacy VTK file reads its blocks in place: a ten-million-point
.vtureads in half a second instead of fourteen, a v5.1.vtkin half a second instead of nine. Cell offsets that run backwards or past the connectivity raiseCodecErrornaming the piece instead of silently dropping cells.XDMF (
.xdmf,.xmf) is read and written: XML light data over heavy data held inline, in an HDF5 sidecar or in a raw binary one, everyDataItemflavour read - aHyperSlaband aReferenceby XPath among them - and a sidecar found beside the file, refused outside its directory. A uniform grid reads with itsNodeandCellattributes in the shape the file gives them,Gridattributes andInformationintoglobal_attrs,Setelements into the tags andTimeunderglobal_attrs["time"]; aMixedtopology counts the nodes of its free-size cells the way the Xdmf library does, so a line beside a triangle reads and writes; a lattice topology expands into explicit cells. Several grids at one time are merged, each grid’s elements tagged with its name; a temporal collection is read at one step,step=choosing which, and the whole series byhelper.read_time_series. A grid that borrows its mesh through anxi:includepointer is followed, and the grid it borrows from, lying beside the series, is not read as a second mesh. The writer names the topology for a single-type mesh and spellsMixedotherwise, sends the arrays toout.h5besideout.xdmfby default -data_format="xml"inline,"binary"one raw sidecar with a seek per array - andhelper.write_time_seriesstreams(time, mesh)pairs into one collection, the mesh written once and included by pointer after, a moved mesh with its own geometry per step. The HDF5 flavour needs h5py, now thepolyxios[hdf5]extra; without it the other two work and an HDF5 reference raisesUnsupportedFormatErrorspelling the install line.MED (
.med), the HDF5 mesh file of Salome and code_aster, is read and written. Every mesh in the file is read, several merged with each mesh’s elements tagged by its name andmesh=picking one; MED groups become tag groups through the families, a cell in two groups landing in both, and a family belonging to no group keeps its number. Fields on the nodes and on the cells are attributes, a field over some geometries only NaN on the rest, Gauss-point and per-node values keeping their axis, a profiled field spread over its support;step=picks the fields’ step and its time lands inglobal_attrs["time"]. The writer spells MED 3.0 with every attribute the MED library’s mesh-info call reads, one cell group per geometry with itsGEOcode, one family per distinct combination of groups, and per-node values asELNO; a write to a path lands under a temporary name and is moved into place, so a failed write leaves no half file.CGNS (
.cgns), in its HDF5 form, is read and written. Bases, zones and sections are found by their label whatever their names; every zone is read, several merged with each zone’s elements tagged by its name andzone=andbase=picking one; sections split by type,MIXEDby the code ahead of each cell with or without start offsets,NGON_nas polygons in either layout, a structured zone expanded into cells with its solutions and itsPointRangeboundary conditions laid over the lattice; boundary conditions become tag groups over the vertices or the elements their location says, flow solutions attributes, one over aPointListorPointRangelanding on the listed entities and a cell-centred one over the volume cells alone NaN on the boundary elements; the quadratic hexahedron and wedge are permuted from the SIDS order to VTK’s. The writer spells every node attribute, root dataset and version node the CGNS library probes, and splits the element values by dimension so the cell-centred solution counts what the zone says, faces and edges in their own solutions over aPointList.H5M (
.h5m), MOAB’s native file, is read and written: handle ids resolved through each group’sstart_id, dense tags as attributes laid over every element, meshsets as tag groups named by theirNAMEtag or their material, Neumann or Dirichlet number, range compression expanded,GLOBAL_IDasoriginal_ids; the wedges and the quadratic hexahedra are permuted from MOAB’s node order to VTK’s. The writer spells one group per type, dense tags declared with their type, and a meshset per tag group with aNAMEand a set number.HMF (
.hmf), XDMF’s model in one HDF5 file, is read and written, the topologies in the order they are numbered, withNodeSets,CellSetsandAttributesadded for the tags and the mesh-wide values.The four share
polyxios.codecs._hdf5: h5py imported on first use, a file opened from a path, a buffer or a gzip member alike, and a count larger than its dataset refused naming both.Exodus II (
.e,.exo,.ex2), Sandia’s netCDF finite element database, is read and written. Every element block, node set and element set is a tag group named by its name or its id; a side set is read as the faces it names, linked to their parents byface_parentandface_index, and a group of such faces goes back out as one. The nodal, element and global variables at one time step -step=picks it - are the attributes,_x/_y/_zand_0/_1/… families folded into one array and split again on write; the step’s time and the file’s title travel inglobal_attrs, the number maps inoriginal_ids. AHEX20,HEX27,WEDGE15orWEDGE18is permuted between Exodus’s node order and VTK’s both ways, and the side numbering is checked against the manual’s table. Blocks are chosen on write from the tag groups that partition the mesh by type, the rest per type. The codec needs netCDF4, the newpolyxios[netcdf]extra, and refuses with the install line without it.3MF (
.3mf), the 3D-printing interchange format, is read and written. The build is read as a printer sees it: every placed object, a mesh outright or an assembly through its components, each transform composed and applied, a mirroring one reversing the winding; each object’s triangles are an element tag group of its name. A triangle’s base material or colour iselement_attrs["colors"], RGBA in 0..1; the model’sunitand itsmetadataareglobal_attrs. On write a quad, polygon or strip is split into triangles and anything with no triangle dropped with a warning; every tag group goes out as an object of its name, the colours as onebasematerials, the unit fromunit=or the mesh. The package is written with fixed timestamps, so two writes of one mesh are byte-identical. Nothing beyond the standard library.Optional packages have one rule,
polyxios._optpkg.optional_package: a module when it imports, and otherwise a stand-in that raisesMissingPackageError- anUnsupportedFormatErrorand anAttributeErrorboth - the moment anything is asked of it, naming the package, why the import failed and the extra that installs it. A minimum version can be asked for and is read without a version-parsing dependency.SVG pictures are written:
.svg, gzip included, is a drawing of the mesh projected onto a plane -plane="xy"by default,"xz"or"yz"on request - in the mesh’s own units with the upward axis mirrored, theviewBoxits extent padded by half a stroke. Lines and faces are outlined, the higher-order kinds through their corner nodes and a triangle strip as one ring per triangle, one<path>per element type carrying the type’s name as itsclass; a vertex, a solid cell and an element with too few nodes to outline are each skipped with a warning per type.width=sets the rendered size,stroke_width=the line weight andfloat_fmt=the number format. A mesh that is not flat in the picture’s plane is drawn as its projection with a warning. The format is write-only: reading an.svgraisesUnsupportedFormatErrornaming the file, since a picture keeps no third coordinate, element type or attribute to read a mesh back from.glTF 2.0 (
.gltf,.glb) is read and written.read_scenereturns aSceneDatawith the full node hierarchy, PBR materials, textures, and animations;readflattens to a singlePolyDatawith a warning.write_sceneround-trips fromSceneData;writeserialises a flat mesh as a single-node scene.ANSYS Fluent meshes are read and written:
.mshwhen the file opens with a parenthesised section, and.fluentto name the codec on a write, gzip included. A Fluent mesh is its faces, each naming the cell on either side, so a cell comes back assembled from them as the tetrahedron, hexahedron, wedge or pyramid - in the plane the triangle, quadrilateral or polygon - they close, right-handed whatever the faces’ own orientation; one whose faces close none of those, a polyhedron or a hanging-node refinement among them, is dropped with a warning. Every face of a zone that is not interior comes back as an element of its own, tagged with the zone’s name and linked to its cell throughface_parent/face_index, a non-conformal interface’s faces by the type under their 1000 offset; each zone is a tag, named by its(45 ...)record or<type>-<id>without one, and the zone types travel underglobal_attrs["fluent_zone_types"]. The ASCII and binary flavours of the node, cell and face sections are both read - a double-precision section’s integers in either width - as are the explicit node lists another reader writes under a typed cell zone; a binary section of another kind is stepped over by its end marker. The writer spells the standard face-based ASCII file: one cell zone per element tag group, the faces of every cell with the cell on either side, outward of the cell whatever its handedness in the mesh, and the boundary in a zone per group naming the matching face elements,wallfor the rest. A mesh of triangles and quadrilaterals is a two-dimensional file, which Fluent holds in the plane only, so a surface off the plane is refused. A face element that is no side of a written cell, an element of any other type and a member of a second group are each dropped with a warning..mshis now shared by Gmsh and Fluent and resolved by content; a bare write there is still Gmsh.PERMAS decks are read and written:
.dato,.post, and.datwhen the file opens with a PERMAS record, gzip included.$COORnodes and$ELEMENTcells come back renumbered from zero with the deck’s own numbering kept underoriginal_ids; every$NSETand$ESETis a tag, as is theNSET =a$COORrecord names and theESET =an$ELEMENTrecord names; a&continuation line joins the record before it; and the component name is kept underglobal_attrs["permas_component"]when it is not the default. The solver element class is read as its geometry -SHELL4,LOADA4andQUAD4are all a quad - and written as the plain structural class of that geometry unlesselement_type=names another, the way the Abaqus writer takes it. A block of a class with no geometry here is skipped with a warning, an id spelled twice, not positive or wider than 64 bits, a node the deck never declares or a file with no$record is refused, as is a$COORblock flaggedCYLorSPH, a coordinate may carry a FortranDexponent, and the writer keeps the mesh’s element order by opening a new$ELEMENT TYPE =block wherever the geometry changes; a planar mesh is lifted with a zero z. Nothing in a structure section carries per-entity data, so attributes are dropped on write..datnow has three candidates, and the PERMAS test - an$ENTERsection or one of a handful of$keywords spelled the way the format spells them, so a bulk data banner sitting flush against its$is not claimed - is tried ahead of Tecplot’s and Nastran’s.
Behaviour changes#
Python 3.12 is the lowest version supported. 3.11 is dropped from the test matrix, from the wheels a release builds, and from
requires-python, so pip resolves an older polyxios for a 3.11 interpreter rather than installing a package that will not run. The floor is what the language level is now written against: the type-parameter syntax 3.12 introduced is in use where a generic helper needed a type variable declared beside it.STL write warns when it skips elements that are not triangles, with their count and types. A quad, a line or a tetrahedron has no STL spelling and was dropped without a word, so a mesh could lose most of itself on the way out. A suite that runs with warnings as errors sees the skip now.
Bug fixes#
Two
SceneData(orSceneNode, orSceneMaterial) compared with==no longer raise when theirglobal_attrsorextrashold arrays, as a glTF animation does: nested dicts, lists and arrays are compared by value, NaN equal to NaN. An array and a list holding the same values now compare unequal, where a one-element pair compared equal before. The meshes of twoSceneDataare compared the same way, array by array, where any two scenes holding a mesh raised unless they shared the very samePolyDataobjects.glTF write skips, with a warning, an animation channel glTF cannot hold: a target carrying fields glTF has none for, a path other than
translation,rotation,scale,weightsorpointer, a node that is not one of the scene’s, an interpolation other thanLINEAR,STEPorCUBICSPLINE, a sampler that is not an object, holds no key, NaN, Inf or a value float32 cannot hold, times that are not strictly increasing, a rotation of zero length, or values that are not three per key for a translation or scale and four for a rotation, or a node path an earlier channel of the same animation already animates. Such a channel was written as is, a valid-looking file animating the wrong thing, or raisedTypeErrororKeyError, and an animation left with no channel is no longer written empty, which glTF forbids. An animation that is not an object is skipped with a warning as well. A node that only such a skipped channel, or one naming no sampler, targets keeps itsmatrixinstead of being spelled as translation, rotation and scale. Amatrixchannel animating a node’s whole transform is split into translation, rotation and scale channels.glTF write stores flat animation values at the width of their path, six values of a translation as two
VEC3keys, and morph weights given one row per key as that many scalars rather than one per row. A sampler several channels share is written once, as is a times array several samplers share. A node, mesh or child index held as a numpy integer, or an array in a channel target’sextras, is written rather than raisingTypeError. The values of apointerchannel are written as the accessor type as wide as one key - 9 asMAT3, 16 asMAT4- where any width but 1, 2 and 4 was labelledVEC3over the wrong byte count; a shape glTF has no type for is skipped with a warning. A target whosenodeis None is written without the key rather than asnull, and an animation name that is not a string is left out.glTF read raises
CodecErrornaming the animation and sampler, rather than a bareKeyError,IndexErrororAttributeError, when an animation is not an object holding a list of channels and a list of samplers, a channel or sampler is not an object, or a sampler names no readable accessor. A negative or boolean accessor index, which read the accessor that many from the end or number 0 or 1, raises as well, as do times taken from an accessor that is notSCALAR.The glTF warnings for skipped volume, pixel and quadratic elements, and for a scene flattened by
read, point at the caller’s line whether the codec was called throughpolyxiosor directly.glTF write of an animated node whose matrix mirrors (a negative determinant) no longer raises
ValueErrorwhen SciPy is installed, nor writes a wrong rotation when it is not: the rest pose splits into translation, rotation and scale the way the animation keys do, the reflection on the x scale. A matrix holding NaN or Inf raisesCodecError. Key times of every channel, not only a split matrix one, stay strictly increasing once stored as float32, and the first never turns negative.glTF read keeps one vertex set for primitives sharing their accessors, as the one-primitive-per-material meshes
write_sceneemits do. Each primitive was read as a mesh of its own and merged, so every round trip multiplied the vertex count by the number of materials and cut connectivity across them. Element decoding is vectorised: a million lines or fan triangles read in 0.03 s instead of 0.5 to 1.6 s. A primitive with no attributes object, an index naming nothing in the file (a negative one read the last accessor), indices that are signed, float or past the vertex set (a uint32 above 2**31 wrapped to -1), or an attribute whose length differs fromPOSITIONraisesCodecError.The COLLADA animation reader no longer refuses valid documents with many instanced nodes: a channel missing its transform counts once per
<node>, not per copy, and the target cap is four times the node cap. A BEZIER or HERMITE sampler missing a tangent is dropped with a warning naming it instead of written for strict readers to reject, and a channel dropped on a copy alone no longer claims to reach every instance.A 3MF whose model part inflates past a gigabyte - ten million vertices - is read. The part was inflated whole and handed to the XML parser in one buffer, which expat refuses past that size with
out of memory, and the tree it built held an Element per vertex and per triangle, thirty times the memory of the numbers. The part is now fed to the parser as it inflates, and each<vertex>and<triangle>goes straight into a numeric buffer instead of the tree.A VTK XML file declaring
header_type="UInt64"has its inline binary arrays read past an eight-byte block header; the reader assumed four there and handed back values shifted by half a word.A count a file spells as negative - a PLY
elementline, a legacy VTKPOINTSorCELLSheader, an XDMFSeek- raisesCodecErrornaming it. numpy reads a negative count as “everything left”, so the block swallowed what followed it and the parser walked backwards from there.A legacy VTK v5.1 binary file whose
OFFSETSrun backwards or end past theCELLStotal, a VTU whosetypesarray miscounts its cells, a VTK XML block whose bytes are not a whole number of values, that declares more bytes than its section holds, or whoseoffsetis not one, a raw appended section with no_marker, a VTK XML head that is not well-formed, and a Medit.meshbshorter than its header are each refused withCodecErrorwhere they read a broken mesh or raised a bareValueErrororParseError.A VTU or VTP file of several pieces shifts each piece’s indices by the points before it in an integer type that holds the sum. Added in the file’s own dtype, a one- or two-byte connectivity wrapped, or numpy refused the shift outright. The offsets of a lazy read are native int32, or int64 when they need it, whatever unsigned or big-endian dtype the file’s connectivity keeps.
A 3MF object or metadata name holding a tab, newline or return is written as character references and reads back unchanged; XML normalises the literal characters to spaces inside an attribute.
A TetGen pair in a directory the process cannot search is reported as the permission error it is on Python 3.14 too. The codec asked
Path.exists, which answered False there rather than propagating the refusal - 3.14 changed it to - so a file that was on disk was reported missing and the caller was sent looking for a path already there. The question is asked withstatnow, which refuses on every version and says by errno which case it is.STL write refuses, with
CodecErrornaming the element, a triangle that references a vertex outside the mesh. A negative index wrapped to the last vertex and wrote wrong geometry; one past the end raised a bareIndexError.
Tests#
Python 3.14 is in the test matrix and among the wheels built for a release. It was the one supported version nothing exercised, and the
Path.existschange above reached a packaged build before any test saw it.Python 3.15 is in the test matrix. Its rows install
netcdfandpytestonly, as h5py, lazrs and PyYAML publish no cp315 wheels yet, so the HDF5 and LAZ tests skip there.
Packaging#
Python 3.15 wheels are built for a release, for Linux, macOS and Windows, and the distribution declares 3.15 among its classifiers.
The PyPI page and the GitHub repository link to GitHub Sponsors.
SECURITY.mdsays how to report a vulnerability privately and what counts as one in a file parser;CITATION.cffgives the citation GitHub shows on the repository page.The PyPI page now carries the project links (homepage, documentation, changelog, issue tracker, source), trove classifiers, keywords and an author, none of which the distribution declared before. The README links to polyxios.org from its head and from each section that has a fuller page on the site, and the two links that pointed at
docs/*.rstby relative path, which rendered broken on PyPI, now point at the site.
GitHub stats for 2026/09/09 - 2026/10/11 (tag: v0.4.1)
These lists are automatically generated and may be incomplete or contain duplicates.
The following 3 authors contributed 105 commits.
Maharshi Gor
Serge Koudoro
dependabot[bot]
We closed a total of 51 issues, 45 pull requests and 6 regular issues.
Pull Requests (45):
PR #108: BF: spell XML-forbidden regex ranges as escapes in a raw string
PR #107: CI: Python 3.15 support, and a docs correctness pass
PR #106: NF: glTF write rebuilds COLLADA transform animations as TRS
PR #105: NF: COLLADA and glTF carry tangents across, as a TEXTANGENT frame
PR #104: NF: COLLADA keeps glTF PBR terms in a polyxios extra technique
PR #102: NF: glTF write_scene writes skins, so a skinned scene keeps its rig
PR #103: MNT: Bump pypa/cibuildwheel from 4.2.1 to 4.3.0 in the actions group
PR #101: NF: COLLADA skins read and written, so a rigged glTF keeps its skin
PR #100: BF: glTF read shares vertices across primitives with the same accessors
PR #96: NF: COLLADA animations read and written, glTF animations baked on write
PR #97: MNT: update pre-commit hooks
PR #90: NF: COLLADA (.dae) read and write as a scene
PR #94: BF: glTF animation write fixes, skin inverse bind matrices decoded
PR #92: MNT: update pre-commit hooks
PR #89: NF: VTKHDF (.vtkhdf) and PVD (.pvd) read and write, with time series
PR #88: NF: LAS and LAZ point clouds (.las, .laz) read and write
PR #87: NF: PCD and ASCII point clouds (.pcd, .xyz, .pts, .ptx) read and write
PR #86: NF: lazy reads hand back views of the mapping; VTU/VTP appended-raw writer
PR #85: NF: read and write 3MF
PR #84: Fix CLI examples, homepage transcript and codec warning output
PR #83: NF: read and write Exodus II
PR #82: DOC: sync README and docs with the code
PR #81: NF: MED, CGNS, H5M and HMF codecs over one HDF5 plumbing module
PR #80: NF: read and write XDMF, the time series through the helper
PR #79: MNT: update pre-commit hooks
PR #78: NF: write SVG pictures, a write-only projection of the mesh onto a plane
PR #77: Add glTF 2.0 / GLB read-write support and SceneData scene-graph container
PR #76: MNT: update pre-commit hooks
PR #75: MNT: check XML-spellable array names by code point, not a regex range
PR #74: DOC: bring bot-authored commits under the message convention
PR #73: Potential fix for code scanning alert no. 3: Workflow does not contain permissions
PR #72: NF: ANSYS Fluent meshes read and write as a sniffed .msh and .fluent
PR #71: MNT: README badges that read PyPI fields the release has not shipped
PR #68: NF: new codec - PERMAS decks read and write as .dato, .post and a sniffed .dat
PR #70: MNT: PyPI page carries project links, classifiers and keywords
PR #69: MNT: Bump pypa/cibuildwheel from 4.2.0 to 4.2.1 in the actions group
PR #67: MNT: Python 3.12 is the lowest version supported
PR #66: BF: an unsearchable directory still refuses on Python 3.14
PR #65: BF: the docs site keeps one directory per release, named for it
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 (6):
GH#99: COLLADA animations: follow-ups from the #96 review
GH#93: glTF write: rebuild COLLADA transform-element animations as TRS
GH#95: glTF -> COLLADA conversion: what is lost, and what can be recovered
GH#98: BF: glTF read duplicates vertices shared across a mesh’s primitives
GH#91: COLLADA write drops glTF TRS animations
GH#58: OBJ codec: texture coordinates (vt) parsed but not stored in vertex_attrs
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):