Changelog#

0.5.1 (2026-10-15)#

Bug fixes#

  • A binary legacy .vtk POLYDATA file whose cell sections carry no OFFSETS block 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, binary and binary_compressed, the last packed with LZF, which polyxios now ships as a compiled module with a pure-Python twin behind it. x y z are the vertices, normal_x/y/z fold into normals, a packed rgb or rgba into colors in 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 its COUNT is above one. An organised cloud keeps pcd_width and pcd_height, a sensor pose pcd_viewpoint; padding fields are skipped and NaN points kept. Write picks the body with data_format= and F 8 coordinates with double=True. lazy=True maps 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 new laz extra. The scaled integer coordinates become float64 vertices; intensity, the return and classification bit fields, the scan angle (scan_angle_rank in degrees before format 6, scan_angle in 0.006-degree steps from it), user_data, point_source_id, gps_time, nir and the wave packet fields are vertex attributes in the file’s own dtypes, red green blue fold into colors in 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 is crs_wkt, the GeoTIFF key VLRs las_geokeys, every other VLR or EVLR raw in las_vlrs; the scale, offset, version, point format and header strings are las_* globals. Write picks the point format from the attributes present or takes point_format=, the version from the format or version=, scale= and offset= from the globals or the cloud, and sends every other numeric attribute out as an extra bytes field. lazy=True maps an uncompressed file: the vertices are the int32 the file holds, the colours its uint16 and 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 the hdf5 extra. An UnstructuredGrid reads as its cells are, a PolyData with its vertices, lines, polygons and strips typed by point count, an ImageData expanded into the lattice its extent spans with the vti_* globals a .vti carries; every partition is merged and tagged part_<k>, PointData, CellData and FieldData are the attributes and globals, and a temporal file reads one step - step= on read, every step through helper.read_time_series. Write emits an UnstructuredGrid, or a PolyData with polydata=True or when the mesh was read as one, time= a Steps group of one, and helper.write_time_series appends 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 a SceneData. 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, TEXCOORD and COLOR inputs as normals, texcoords and colors, and each block’s material symbol resolved through the instancing node’s <bind_material> into element_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 in extras), images by uri, <hex> or data: 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=True raises.

  • COLLADA animations are read and written. <animation> channels land under global_attrs["animations"] in the shape glTF uses, each target holding the node, the path, the transform sid and 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’s sid. 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 mesh joints and weights (the four heaviest influences per vertex) and its node a skin index into global_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 inverseBindMatrices accessor into inverse_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 no bufferView (all zeros), cannot be read or holds fewer matrices than joints, is kept undecoded with a warning rather than failing the read.

  • glTF write_scene writes skins, so a skinned scene keeps its rig on its way back to glTF: each entry of global_attrs["skins"] becomes a glTF skin with its name, joints, skeleton, extras and its inverse_bind_matrices as a float MAT4 accessor, and a node’s extras["skin"] its skin. A bind_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’s joints and weights do 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 as JOINTS_1 and WEIGHTS_1 rather than dropped, so a vertex with eight influences keeps weights summing to one. A mesh’s mesh_name and the scene’s extras are written; vertex and element attributes, tags and globals glTF has no place for, once dropped silently, now warn. A value that is not JSON raises CodecError rather than a bare TypeError, 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 TEXTANGENT of its xyz and, when the mesh has normals, a TEXBINORMAL on the side its w gives; a glTF write rebuilds TANGENT from 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 in polyxios._trs, shared by both codecs.

  • PVD (.pvd), ParaView’s collection of datasets over time, is read and written. Every dataset at one timestep is read through its own codec and merged, tagged by group or part_<n>, with the step’s time under global_attrs["time"]; step= picks the step and helper.read_time_series reads them all. Write puts one .vtu - or the extension format= names - per step beside the index.

  • ASCII point clouds (.xyz, Leica .pts and .ptx) are read and written. Columns past the coordinates are named by count and kind - an intensity, three whole numbers in 0..255 as colors, three others as normals, anything else kept whole as extra - with comment lines and a column-label line skipped and the body parsed by numpy in one pass. .pts honours and checks its count line. .ptx reads every scan, applies each one’s transform, drops the 0 0 0 0.5 invalid cells with a warning, tags the scans in vertex_tags when there are several and keeps the matrices and grid sizes in global_attrs. A nameless buffer takes variant= 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 gain appended=True to emit that layout, a third smaller than base64, and declare header_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. .splat maps everything, a binary legacy VTK v5.1 file every block and a v4.2 file its points and attributes, and binary PLY and Medit .meshb their vertex records. What cannot be a view is built in memory and the guide says which; what cannot be mapped at all raises LazyReadError naming 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 .vtu reads in half a second instead of fourteen, a v5.1 .vtk in half a second instead of nine. Cell offsets that run backwards or past the connectivity raise CodecError naming 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, every DataItem flavour read - a HyperSlab and a Reference by XPath among them - and a sidecar found beside the file, refused outside its directory. A uniform grid reads with its Node and Cell attributes in the shape the file gives them, Grid attributes and Information into global_attrs, Set elements into the tags and Time under global_attrs["time"]; a Mixed topology 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 by helper.read_time_series. A grid that borrows its mesh through an xi:include pointer 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 spells Mixed otherwise, sends the arrays to out.h5 beside out.xdmf by default - data_format="xml" inline, "binary" one raw sidecar with a seek per array - and helper.write_time_series streams (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 the polyxios[hdf5] extra; without it the other two work and an HDF5 reference raises UnsupportedFormatError spelling 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 and mesh= 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 in global_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 its GEO code, one family per distinct combination of groups, and per-node values as ELNO; 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 and zone= and base= picking one; sections split by type, MIXED by the code ahead of each cell with or without start offsets, NGON_n as polygons in either layout, a structured zone expanded into cells with its solutions and its PointRange boundary conditions laid over the lattice; boundary conditions become tag groups over the vertices or the elements their location says, flow solutions attributes, one over a PointList or PointRange landing 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 a PointList.

  • H5M (.h5m), MOAB’s native file, is read and written: handle ids resolved through each group’s start_id, dense tags as attributes laid over every element, meshsets as tag groups named by their NAME tag or their material, Neumann or Dirichlet number, range compression expanded, GLOBAL_ID as original_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 a NAME and a set number.

  • HMF (.hmf), XDMF’s model in one HDF5 file, is read and written, the topologies in the order they are numbered, with NodeSets, CellSets and Attributes added 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 by face_parent and face_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/_z and _0/_1/… families folded into one array and split again on write; the step’s time and the file’s title travel in global_attrs, the number maps in original_ids. A HEX20, HEX27, WEDGE15 or WEDGE18 is 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 new polyxios[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 is element_attrs["colors"], RGBA in 0..1; the model’s unit and its metadata are global_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 one basematerials, the unit from unit= 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 raises MissingPackageError - an UnsupportedFormatError and an AttributeError both - 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, the viewBox its 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 its class; 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 and float_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 .svg raises UnsupportedFormatError naming 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_scene returns a SceneData with the full node hierarchy, PBR materials, textures, and animations; read flattens to a single PolyData with a warning. write_scene round-trips from SceneData; write serialises a flat mesh as a single-node scene.

  • ANSYS Fluent meshes are read and written: .msh when the file opens with a parenthesised section, and .fluent to 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 through face_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 under global_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, wall for 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. .msh is 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 .dat when the file opens with a PERMAS record, gzip included. $COOR nodes and $ELEMENT cells come back renumbered from zero with the deck’s own numbering kept under original_ids; every $NSET and $ESET is a tag, as is the NSET = a $COOR record names and the ESET = an $ELEMENT record names; a & continuation line joins the record before it; and the component name is kept under global_attrs["permas_component"] when it is not the default. The solver element class is read as its geometry - SHELL4, LOADA4 and QUAD4 are all a quad - and written as the plain structural class of that geometry unless element_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 $COOR block flagged CYL or SPH, a coordinate may carry a Fortran D exponent, 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. .dat now has three candidates, and the PERMAS test - an $ENTER section 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 (or SceneNode, or SceneMaterial) compared with == no longer raise when their global_attrs or extras hold 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 two SceneData are compared the same way, array by array, where any two scenes holding a mesh raised unless they shared the very same PolyData objects.

  • 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, weights or pointer, a node that is not one of the scene’s, an interpolation other than LINEAR, STEP or CUBICSPLINE, 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 raised TypeError or KeyError, 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 its matrix instead of being spelled as translation, rotation and scale. A matrix channel 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 VEC3 keys, 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’s extras, is written rather than raising TypeError. The values of a pointer channel are written as the accessor type as wide as one key - 9 as MAT3, 16 as MAT4 - where any width but 1, 2 and 4 was labelled VEC3 over the wrong byte count; a shape glTF has no type for is skipped with a warning. A target whose node is None is written without the key rather than as null, and an animation name that is not a string is left out.

  • glTF read raises CodecError naming the animation and sampler, rather than a bare KeyError, IndexError or AttributeError, 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 not SCALAR.

  • 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 through polyxios or directly.

  • glTF write of an animated node whose matrix mirrors (a negative determinant) no longer raises ValueError when 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 raises CodecError. 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_scene emits 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 from POSITION raises CodecError.

  • 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 element line, a legacy VTK POINTS or CELLS header, an XDMF Seek - raises CodecError naming 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 OFFSETS run backwards or end past the CELLS total, a VTU whose types array 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 whose offset is not one, a raw appended section with no _ marker, a VTK XML head that is not well-formed, and a Medit .meshb shorter than its header are each refused with CodecError where they read a broken mesh or raised a bare ValueError or ParseError.

  • 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 with stat now, which refuses on every version and says by errno which case it is.

  • STL write refuses, with CodecError naming 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 bare IndexError.

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.exists change above reached a packaged build before any test saw it.

  • Python 3.15 is in the test matrix. Its rows install netcdf and pytest only, 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.md says how to report a vulnerability privately and what counts as one in a file parser; CITATION.cff gives 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/*.rst by 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 literal in them opened a command substitution and the step died before gh was 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 *.dat is 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() and write() now take an open binary file object wherever they take a path, so a mesh round-trips through io.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, so fmt= is required there; a handle from open() carries its own extension and does not need it. TetGen is the exception: a .node/.ele pair 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 LazyReadError naming 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 .gz is compressed on the way out. .vol.gz is read as Netgen rather than refused, .gz names the compression rather than the format when a codec is chosen - in fmt= 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 - what cat a.gz b.gz leaves - 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 from gzip.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 .splat whose 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 .splat read 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, a sniff(head) -> bool test and a SNIFF_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.

  • .dat is the first user of this: a Tecplot header resolves to the Tecplot codec, a bulk data card to the Nastran one. It no longer needs fmt=, which still works for the cases sniffing cannot settle.

  • .plt is 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_dimension reports 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_vertices welds 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 a tol so 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 with remove_orphan_vertices to drop those too.

  • Kratos MDPA .mdpa is read and written: nodes, elements named by Kratos element class, conditions, Properties ids, NodalData and ElementalData variables, ModelPartData and nested SubModelPart groups. The class name and the geometry do not identify each other - Element3D4N is 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>N suffix is read off whatever application element carries it (SmallDisplacementElement3D4N, VMS3D4N), and the explicit geometry names (Tetrahedra3D4, Prism3D6) are recognised too. Conditions are read as elements, since they are cells of the same mesh; they are written back under Elements: 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 naming GRID 7000001, a report keyed on element 4001 - pointing at the wrong thing. The reader records what the file said in vertex_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 .bdf and written as .msh keeps its numbering. Only a numbering the index does not already say is stored: a file numbering 1..n in order records nothing, since the writer’s own renumbering reproduces it. Ids that a transform has since invalidated - merge collides 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 .vtu keeps 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 in global_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 an NDIME= 2 SU2 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, .vtr and .vts read and write a <FieldData> block, and legacy .vtk reads and writes a FIELD FieldData block between the DATASET line and the geometry - so a time value, a material constant or a solver tolerance in global_attrs survives a write instead of being dropped. A legacy STRUCTURED_POINTS, RECTILINEAR_GRID or STRUCTURED_GRID file 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_extent and 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_attrs travels through the VTK XML family. A <FieldData> block holds a String array beside its numeric ones, so a name, a title or a solver’s own label - an Abaqus *HEADING among them - reaches a .vtu, .vtp, .vti, .vtr or .vts and 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 legacy FIELD block spells no text at all, so a string handed to a .vtk is still dropped with a warning naming the key.

  • Abaqus .inp reads a *HEADING card into global_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_blocks and helper.read_multiblock read every VTK index file - .vtm, .pvtu, .pvtp, .pvtr, .pvts, .pvti, and a .vtp holding a <vtkMultiBlockDataSet> - where read_multiblock_vtp read only the last of those. The first hands back one PolyData per 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 raises PermissionError, now helper.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, .vtr and .vts write one point or cell column of ones and zeros per group, named polyxios_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 *SURFACE is 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, and face_index, which of that element’s faces. Writing puts the *Surface card back, with its parent *Elset marked internal the 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=NODE becomes 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 *Nset it now is, a node set and a node surface being the same members under two cards. SPOS or SNEG on 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 .vtk among them, still writes its surfaces back. transforms.merge shifts face_parent onto 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 way write() already did, and so does the dispatcher a contested extension resolves through. An option a reader does not take raises TypeError naming it. STL’s merge_vertices was written as a read option and had never been reachable without importing the codec itself; it is now read(path, merge_vertices=False).

  • OBJ reads split_seams=True, which keeps the texture coordinates and normals a per-vertex array otherwise drops. OBJ indexes vt and vn per 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 the v records 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 .mesh records a two-dimensional file in global_attrs["was_2d"] rather than keeping the file’s own number in global_attrs["medit_dimension"], which is now the shared spelling every format uses. A three-dimensional file sets nothing, where it used to set medit_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 .node header declares 2, a Tecplot zone declares X and Y alone, an Abaqus node card carries two coordinates, and a .meshb header declares Dimension 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 named Z, 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 an element_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 MFEM vertices block, a Tecplot ET=TETRAHEDRON zone, a TetGen .ele file, a Medit Tetrahedra section in either spelling and a DOLFIN celltype="tetrahedron" each need three, whatever the flag says. A dim= given to the DOLFIN writer by name is the caller’s own word and is left alone.

  • Text formats are written with \n line endings on every platform. Writing used to go through Path.write_text, which turned them into \r\n on Windows; every write now goes through the same binary path a buffer does, so the bytes a path receives and the bytes a BytesIO receives are the same ones. Every reader already accepted either ending.

Bug fixes#

  • A legacy .vtk SCALARS header names between one and four components and no more. The writer spelled every tuple it had no VECTORS or TENSORS branch for as SCALARS whatever its width, so a nine-component per-element array went out as SCALARS 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 a FIELD array, whose own header carries the component count and so has no ceiling; four components and under keep the SCALARS spelling 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 spelled SCALARS name double 0.

  • A binary block in a legacy .vtk file is closed by a newline, and the keyword line after it is found by reading to the next one. Only the dataset’s FIELD block 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 first 0x0a inside 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 a TENSORS section holds using the classical Voigt order instead, whose last three run the other way, so XZ and YZ came 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 .vtk v4.2 CELLS line 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 .vtk v5.1 CELLS line 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 the CELL_TYPES after 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 .vtk TENSORS6 section is read as the six components it holds. VTK 9 writes a symmetric tensor with that keyword, and every reader here matched it as a TENSORS and took its tuples for nine: an ASCII file failed with a CodecError about 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 Name attribute 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 .vtk has 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 .vtk FIELD header that leaves its type field out is read as the float the 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 .vtk reader looks for its keywords below the DATASET line. 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: a POLYDATA mesh titled Vertices of a cow or Points of interest was refused, and the FIELD block this release adds widened that to any title beginning Field. The line is found rather than counted, so a header carrying a blank line between its four is read too, where an UNSTRUCTURED_GRID reader counting to four missed the first keyword after it.

  • A legacy .vtk cell array is read against what its block actually holds. A CELLS row, or a POLYGONS cell, 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 bare IndexError naming neither the file nor the cell. Both are now a CodecError that names them.

  • Every remaining legacy .vtk section that reads numbers off ASCII lines says which file and which section a malformed one came out of. POINTS, CELL_TYPES, a X_COORDINATES array and a STRUCTURED_GRID POINTS block used to hand a bare ValueError about 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. A DATASET FIELD array 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 only CodecError.

  • transforms.merge keeps the surfaces of a mesh whose face_parent and face_index columns 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 -1 that 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 one Piece of a .vtu or .vtp spells 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 .vtk POINT_DATA or CELL_DATA header 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 the FIELD block already refused to. The names are now checked once per section rather than once per array, so the warning names them together.

  • A legacy .vtk ASCII POINTS or CELLS block 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 wrapped POINTS block silently kept three numbers of each line and dropped the rest, and a wrapped CELLS block 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 a CELLS row 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 C int besides, 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_tags and element_tags are not checked on the way in, and such a group used to reach a bare ValueError about 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, .obj and 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 two Piece elements 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 .vtk FIELD block 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 FIELD file holding a string array 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 the FIELD block 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_surface handed 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 Abaqus S<n> and a face_index still 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 RecursionError surfaced as a block that could not be read rather than as the file it was.

  • An abaqus_heading whose text would end the *Heading card 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 *Node followed 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 *Heading line 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 of turbine blade, to the rev 3 under it, and the deck came back one line where its author had written two.

  • An Abaqus *Surface, type=NODE row 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 *Surface row 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 *Surface row 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 CodecError naming the truncation, where the whole-block read used to build a record dtype for it first and hand back numpy’s ValueError about 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.py now holds one corrupt header per format as a matrix.

  • .vti, .vts and .vtr hold 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 raises CodecError naming 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 CellData dropped on the way out for covering the wrong number of cells, and the mesh that came back was not the one that went in. Cells the extent does not read back now raise CodecError at the point of writing.

  • .vts holds 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.

  • .vtr writes 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.

  • .vti and .vts keep the extent of the file they were read from, and .vti the 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 .vti took 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 raises CodecError pointing at .vtr, which spells its coordinates out.

  • Reading back the empty .vts this codec writes raised ValueError: 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 .vti measures 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 - raised IndexError from 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 the 0 -1 extent VTK spells an empty image with - the one the .vts and .vtr writers here already spell - rather than the 0 0 that reads back as a point the mesh never held.

  • Reading a .vti whose Origin or Spacing spells two numbers where three belong raised IndexError from inside the parse. The axes it does give are taken and the rest defaulted, and a bare number given as vti_spacing is 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.

  • .vtr keeps the extent of the file it was read from, the way .vti and .vts do. A block that did not begin at zero was slid to the origin on the way out, which is the one thing a .pvtr assembling 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 WholeExtent of the file they were read from as well as their own Extent. The two differ exactly when the file is one piece of a parallel set, which is the case the piece indices are kept for, and writing the piece extent into both narrowed the grid to the piece: a block that went back out at the indices it stood on still claimed to be the whole domain, so the .pvti, .pvts or .pvtr assembling it read one neighbour where it should have read several. Kept only while the piece extent is, since an extent re-derived from the cells is zero-based and says nothing about the grid the mesh used to stand in.

  • A WholeExtent that is not six whole numbers now raises CodecError naming the file and the attribute. Unpacked straight into ints, it failed with a bare ValueError about a literal instead.

  • A .vti or .vtr holding 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 as ValidationError: declared_n_verts=64 implies 1536 bytes of vertex data. The heuristic is asked only of a format that spells what it declares.

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

  • The evenness an ImageData demands of an axis is measured against the step, 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 2 came back as four quadrilaterals over no vertices at all, every corner naming point zero of an empty array. An end two or more before its start turned the point count negative on top of that, which .vts then handed to reshape as a second unknown dimension. Such an extent now reads as the empty mesh it describes. Legacy .vtk says the same thing with DIMENSIONS and had the same hole: a STRUCTURED_POINTS or RECTILINEAR_GRID of 0 3 3 came back as four quadrilaterals over no vertices, built from strides that were themselves zero, and a negative DIMENSIONS was reported to the caller as a negative count of points.

  • The extent a mesh carries in global_attrs may be no extent at all. A bare number has no length and a string that looks like one has characters rather than numbers, and either reached len and failed with a TypeError from inside the .vti, .vts or .vtr writer. So could a vti_origin or vti_spacing holding something no float reads. None of them is trusted now: what cannot be read is named in a warning and the writer reads the mesh’s own extent, origin and step off it instead.

  • .vtr builds 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 every PointData array 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 raises CodecError naming 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 expanded 0 -1 0 2 0 2 into nine vertices under an extent declaring none. An axis the file leaves no array for takes the one plane at zero the extent gives it, which is how a two-dimensional grid writes its third.

  • .vts reads 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 a CodecError now. 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 strip to take off, so a .mesh the registry had already claimed as MFEM failed for not starting with MFEM mesh.

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

  • .meshb hands back vertices with three columns. A file declaring Dimension 2 was read into an (n, 2) array, which is not what a PolyData holds: every consumer indexing vertices[:, 2] - the transforms, the other writers, faces - raised on it. The coordinates are padded with z=0 like every other codec’s, and the write side takes its Dimension from 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_vertices and filter_element_type carried 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 - raised IndexError from 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"), ...) raised TypeError: filter_element_type takes the mesh first and does not curry. The example now composes it with functools.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 read is 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 from open() 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 CodecError naming the line instead of wrapping around into a different vertex.

  • OBJ vt records 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. vt and vn are written back, so texture coordinates and normals survive a round trip.

  • OBJ writes a bare g before a face that belongs to no group, so it no longer inherits the group of the face above it, and a bare g on read clears the active groups rather than inventing a default tag.

  • An OBJ file whose vn or vt records cannot be lined up with its vertices now leaves the attribute out. The fold that gives up returned None, and only the vt path checked for it, so vertex_attrs could hand back a None where an array belongs and the writer raised TypeError on 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 nan is not a record another reader takes; the row nothing indexes is written as zero. A vt array narrower than two columns is padded rather than indexed past its end.

  • OBJ leaves out a vn or vt attribute 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 indexing vt records 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 as CodecError. A one-dimensional attribute is now read as one value per vertex rather than as a single row.

  • Legacy .vtk files now read COLOR_SCALARS and NORMALS, 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 binary COLOR_SCALARS component 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 .vtk reads TEXTURE_COORDINATES, and steps over a LOOKUP_TABLE definition 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 .vtk attribute 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.

  • NORMALS and COLOR_SCALARS are read from STRUCTURED_POINTS, STRUCTURED_GRID and RECTILINEAR_GRID files too. Those three datasets scan their attributes themselves rather than through the shared parser, and knew only SCALARS, VECTORS and FIELD.

  • A binary legacy .vtk attribute that runs past the end of the file now raises CodecError naming the array and the bytes that are missing. The slice came up short in silence and the reshape after it failed with a ValueError naming neither the array nor the file.

  • A legacy .vtk attribute section that declares more values than the file holds raises CodecError naming the array and the count, rather than running off the end of the line list with an IndexError that names nothing. This covers SCALARS, COLOR_SCALARS, VECTORS, NORMALS, TENSORS and FIELD.

  • A Nastran real field is now written in whatever spelling fits it. Bulk data allows the exponent’s E to be dropped when its sign is there (1.234-10), and nothing reads +07 differently 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: 1e7 went out as 9999999. where 1.E+07 was available, and 1e15 as 999999999999999. 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 than version="0.1", which no VTK release ever defined. Reading a file that declares 0.1 is unchanged.

  • A <DataArray> polyxios cannot decode - a type="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_POINTS file keeps its DIMENSIONS, ORIGIN and SPACING in global_attrs as vtk_dimensions / vtk_origin / vtk_spacing, and STRUCTURED_GRID / RECTILINEAR_GRID keep their DIMENSIONS. Expanding the header into a point array used to throw the grid away.

  • A .vtu or .vtp Piece that declares points and does not deliver them raises CodecError, 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 .vtu or .vtp file 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 bare ValueError from numpy.concatenate.

  • A CELL_DATA section is read from STRUCTURED_POINTS, STRUCTURED_GRID and RECTILINEAR_GRID files. 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 own SampleStructGrid.vtk among them. The three now share one scanner, which also gives them TEXTURE_COORDINATES and TENSORS. An array whose declared length matches neither the points nor the cells is dropped with a warning rather than reaching PolyData as a validation error about lengths.

  • A structured legacy .vtk grid extends along whichever axes it declares. A DIMENSIONS 3 1 3 sheet was read as two lines over the first two points instead of four quads over all nine, and a column along y or z was read with the stride of a row along x. Only grids flat in z and rows along x came 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 .vtk file warns as well. Only the binary scan said anything; ASCII skipped the keyword and its value lines without a word.

  • A legacy .vtk attribute section whose values run into the next header raises CodecError naming the array, the line and what it holds, rather than float() answering with a bare ValueError that names neither the array nor the file.

  • A binary attribute in a structured legacy .vtk file is checked against the end of the file before it is sliced, which the shared parser already did. A truncated file gave a nameless reshape ValueError.

  • A .vtu or .vtp Points array whose size is not a whole number of tuples per point raises CodecError naming the Piece. Only a short array was caught, so ten values for three points reached reshape and came back as a ValueError naming neither the file nor the Piece.

  • A v, vn or vt record in an OBJ file that does not carry the components its directive needs, or carries something that is not a number, raises CodecError naming the line. Both used to reach the caller as a bare IndexError or ValueError. A vt carrying 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_attrs entry that holds no numbers - a label per vertex, say - drops it with a warning instead of raising out of numpy.asarray.

  • Legacy .vtk files written by VTK 5.1 - the default since VTK 9.0 - are read. The two numbers on a v5.1 CELLS line are the length of the OFFSETS array and the length of CONNECTIVITY, not the cell count; reading the first as a cell count ran the offsets into the CONNECTIVITY keyword and answered with a bare ValueError. The offsets are now counted up to that keyword, so files spelling the line either way are read. POLYDATA gained the layout altogether: its POLYGONS, LINES, VERTICES and TRIANGLE_STRIPS sections knew only the v4.2 form, so no polydata file VTK 9 writes could be read at all.

  • Writing a v5.1 legacy .vtk file declares the length of its OFFSETS array on the CELLS line. 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 METADATA block 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 a FIELD block it was read as an array header, which took the array after it with it.

  • A binary STRUCTURED_GRID keeps 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 - a CELL_DATA written before POINT_DATA, as VTK writes it - was skipped.

  • A RECTILINEAR_GRID follows its coordinate arrays rather than its DIMENSIONS header. 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, .vts and .vtr honour NumberOfComponents when reading an attribute. A three-component array on 27 points came back as 81 rows, which belongs to no mesh and fails validate; .vtu and .vtp of the same family already cut it into tuples.

  • .vtr declares 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 an Int32 attribute read back as the bit pattern of a double.

  • A .vtu or .vtp Points array of a type that holds no numbers - type="String", or any type this reader does not know - raises CodecError naming the type. It warned that the array was skipped and then blamed the point count for the zero values that left.

  • A .vtr attribute of a dtype no VTK type names - a boolean mask, a float16 - is written as the Float64 its header declares. Only the header fell back; the bytes stayed the dtype’s own, so eight booleans went out as eight bytes under a Float64 header 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 legacy TENSORS section reads back as - was declared as one component in .vti, .vts, .vtp and .vtu and 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_order they all declare says those bytes are. On a big-endian machine every binary array went out reversed.

  • .vti, .vts and .vtr drop an attribute whose rows cannot be matched to the mesh, with a warning naming it, as .vtu and .vtp already did. It used to reach PolyData and fail validate with a message about lengths rather than about the file.

  • A binary legacy .vtk file holds its TENSORS as 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 a CodecError about 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, .vtp or .vtr file declaring double or Float64 held seven digits fewer than that, so coordinates came back about 1e-10 off what was written; they are now exact.

  • A METADATA block 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 a POINT_DATA section it swallowed the CELLS that 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 .vtk file costs the section rather than the mesh. A count running past the end of the file raised CodecError out 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 .vtu or .vtp Piece that 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 vn or vt attribute wider than the record says how many components are being left out, rather than truncating in silence.

  • A legacy STRUCTURED_GRID whose DIMENSIONS does not cover its POINTS array 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 a PolyData that fails validate. RECTILINEAR_GRID already 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 .vtk SCALARS section without a LOOKUP_TABLE line - which the format leaves optional - reads its own values. The line was consumed unconditionally, so the payload up to its first 0x0a byte was swallowed, and a payload holding none rewound the scan to the top of the file.

  • A binary legacy .vtk POINTS, CELLS or CELL_TYPES block 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 a ValueError naming neither the array nor the file.

  • A legacy .vtk attribute header missing a field, or spelling a count as something that is not a number, names the file and the line it is on. SCALARS with no array name reached the caller as IndexError: list index out of range and SCALARS s float x as a bare ValueError, 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, .vtp and .vtu write each attribute in the type the array is held in, as .vtr does. Everything was cast to a double under a Float64 header, so an int64 identifier past 2**53 came back a different number.

  • The ASCII body of a <DataArray> is parsed into the type the element declares rather than through float() first. An Int64 array 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 .vtk block is read as the type its header names. A POINTS n int was 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 raises CodecError naming 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 .vtk geometry 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, SPACING and the coordinate arrays reached the caller as a bare ValueError or IndexError, which the attribute headers had already stopped doing.

  • A CELL_DATA array in a legacy STRUCTURED_GRID is measured against the cells the mesh ends up with rather than the cells DIMENSIONS describes. A header its POINTS array does not cover leaves the mesh with no cells, and the array was kept against the header’s count, so the read returned a PolyData that fails validate.

  • An OBJ face index spelled with a superscript digit raises CodecError naming the line. str.isdigit admits ² and int() then refuses it, so the fast path let a bare ValueError out; the test is now str.isdecimal, which still admits the non-Latin digits int() reads.

  • A bare mtllib or o directive 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 an IndexError naming an axis.

  • A .vtu or .vtp Piece whose NumberOfPoints is not a count, and a .vti, .vts or .vtr Extent that is not six whole numbers, raise CodecError naming the file. They reached the caller as a bare ValueError about int() or about unpacking.

  • A <DataArray> whose NumberOfComponents is not a count is read flat with a warning naming it, rather than raising a bare ValueError from int().

  • A legacy .vtk file whose header declares DATASET FIELD is read. The dispatch asked what the line starts with while the line still carried its DATASET keyword, so the branch never ran and every field data file was refused by the one below it.

  • A METADATA block inside a v5.1 CELLS section 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 raised CodecError about a line of words where numbers belong.

  • A v5.1 CELLS section is found by what follows the header rather than by the version in the first line. Versions were compared as strings, so a file declaring 10.0 sorted below 5.1 and its offsets would have been read as a v4.2 cell stream. The binary scan already asked this way.

  • A .vti, .vts or .vtr extent 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 every CellData array 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 bare OverflowError from numpy. A token that names no number at all raises CodecError naming the array.

  • Writing a point or cell attribute of a kind no <DataArray> can hold - a label per vertex, say - raises CodecError naming the array and its dtype, rather than a ValueError about 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, at polyxios.read or polyxios.write itself, which tells a caller nothing about which of their own calls found the file.

Optimizations#

  • A legacy .vtk write 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_TYPES is 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, vn or vt record carrying exactly the components its directive spells skips the padding and the slice that feeds it.

  • A float32 attribute is spelled at its own width in an ASCII <DataArray>. Widened to a double first, 0.1 went out as 0.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 repr carries 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 thousand GRID* 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 .5 as 0.5, so a third in an eight-column field goes out as .3333333 rather than 0.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 .vtk file 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 MB STRUCTURED_POINTS file reads about a fifth faster.

  • .vti, .vts and .vtr build 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 .vtk readers cut their file into lines in one pass rather than a find per line, and they all do it in the same place. The lines are byte-identical to what the walk produced.

  • Folding OBJ vt and vn records onto their vertices is one pass over the corners rather than a numpy row assignment per corner.

  • A legacy .vtk ASCII 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 .vtr header declares against the bytes under it, the component count of a tensor attribute in every XML writer, a binary legacy TENSORS section, an unterminated METADATA block followed by geometry, an attribute section declaring more than its file holds, an OBJ conflict hiding behind a NaN first component, and the Piece index in a .vtu error.

  • test_a_double_section_holds_every_digit_of_a_double writes a coordinate no ten-digit spelling can hold and asks for it back unchanged.

  • test_a_power_of_ten_is_written_exactly spells its powers as literals rather than computing them with **. pow is not correctly rounded everywhere - glibc and MSVC answer 10.0 ** 23 with the double one unit above 1e23, macOS with 1e23 itself - 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, preserving mat_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 .nas and .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 + .ele pairs (markers, regions).

    • UGRID .ugrid (AFLR ASCII, boundary tags).

    • WKT .wkt (Well-Known Text).

  • pxios command line interface with fetch, list, convert and viz subcommands. Visualization requires the optional viz extra (pip install polyxios[viz]).

  • polyxios.read_polydata and polyxios.visualize_mesh helpers, plus polyxios.supported_extensions to 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.json catalog and downloads files individually, instead of pinned per-format release zips. Downloads are checksum-verified and the result is cached across runs; set POLYXIOS_MODELS_URL to pin the catalog to an immutable URL.

  • pxios fetch gained --verbose, requires a sha256 for 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 --list from 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 dotless fmt override.

  • Fixed multi-channel vertex attributes in merge and vertex_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 convert and polyxios visualize-mesh CLI 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):