XDMF#
.xdmf .xmf read + write lazy: binary and contiguous HDF5 time series
Summary of the specification#
The eXtensible Data Model and Format splits a dataset in two. The light data is an XML document: an <Xdmf> root holding a <Domain>, which holds <Grid> elements. A uniform grid names its <Topology> - the element type and count, or Mixed with a type code ahead of each cell - and its <Geometry> - XYZ or XY coordinates, or the axes and spacing of a lattice - then any number of <Attribute> elements centred on Node, Cell or Grid, <Set> elements naming nodes or cells, and <Information> pairs. A collection grid holds other grids, spatially - several meshes side by side - or temporally, each child a step carrying a <Time Value>. The heavy data is every array, and each <DataItem> says where its own lives: inline in the XML (Format="XML"), in an HDF5 file as file.h5:/dataset (Format="HDF", the usual case and the reason the format exists), or in a raw file at a byte offset (Format="Binary"). A DataItem may also be a HyperSlab selecting from another, or a Reference to one elsewhere in the document by XPath, and a grid may borrow another’s topology and geometry through an XInclude pointer rather than spelling them again.
Specification at a glance#
structure |
XML: |
topology |
|
geometry |
|
heavy data |
|
time series |
|
sharing |
|
Reading#
import polyxios as px
mesh = px.read("result.xdmf") # the first step of a series
mesh = px.read("result.xdmf", step=-1) # the last
mesh.global_attrs["time"] # that step's <Time Value>
mesh.vertex_tags, mesh.element_tags # <Set> elements
mesh.global_attrs["case"] # <Information Name="case">
from polyxios import helper
times, meshes = helper.read_time_series("result.xdmf")
polyxios.read() hands back one mesh, always. Every uniform grid at the chosen step is read - a spatial collection flat, several grids beside one another merged - and where there are several, each grid’s elements are tagged with the grid’s Name, so a domain split into parts keeps saying which was which. A temporal collection is read at one step, the first without step=; polyxios.helper.read_time_series() reads them all.
lazy=True maps the sidecars instead of reading them, and hands back
arrays that view the mappings, read-only, in the dtype and byte order the
sidecar holds. A Binary DataItem is one run of values at an offset; so is
an HDF5 dataset stored contiguously without a filter, which is what
write emits unless asked for compression - h5py finds where the
dataset sits and the .h5 file is mapped around it. A chunked or
compressed dataset, or values spelled inline as text, have no such run and
raise LazyReadError naming which. Arrays the
file does not store but implies - a uniform topology’s offsets, the element
types, a mixed topology’s gathered connectivity, coordinates padded from two
columns to three - are built in memory; the offsets are int32, or int64 when
they need it, whatever dtype the connectivity keeps.
The HDF5 flavour needs h5py, which is optional - pip install "polyxios[hdf5]". Without it the inline and binary flavours still read, and a file naming an HDF5 sidecar raises UnsupportedFormatError spelling that command.
Writing#
px.write(mesh, "out.xdmf") # arrays in out.h5, beside it
px.write(mesh, "out.xdmf", data_format="xml") # arrays inline
px.write(mesh, "out.xdmf", data_format="binary") # arrays in out.bin
px.write(mesh, "out.xdmf", compression="gzip", compression_opts=4)
px.write(mesh, "out.xdmf", time=0.5)
helper.write_time_series(((t, solve(t)) for t in times), "run.xdmf")
data_format says where the arrays go: "hdf", the default, writes them to an HDF5 file named after the XDMF one - out.h5 beside out.xdmf, never in the working directory - and needs h5py; "binary" writes them all to one out.bin beside it, each at its own Seek; "xml" keeps them inline, which is the one flavour that writes to a file object or a .gz path, since the other two need a beside. compression and compression_opts go to h5py for every dataset. time writes the grid’s <Time Value>; without it, a number under global_attrs["time"] does. Any other option is warned about and ignored.
polyxios.helper.write_time_series() takes (time, mesh) pairs from any iterable, a generator included, and writes each step as it arrives: the first step spells the topology and geometry, and every later one includes them by pointer rather than spelling them again, so the mesh is in the file once. A step whose vertices moved writes its own geometry; one whose elements differ is refused, being another mesh rather than another step.
Quirks worth knowing#
A mesh of one element type writes a topology named for it, the free-size kinds with
NodesPerElement; a mesh mixing types writesMixed, one type code ahead of each cell - and after the code of aPolyvertex,PolylineorPolygon, its node count, which is how the Xdmf library and VTK spell those and what lets a line sit beside a triangle. A pixel and a voxel go out as the quadrilateral and hexahedron they are, corners reordered. The quadratic and higher-order kinds -Edge_3throughHexahedron_27- are read and written in VTK’s node order, which is the order every other reader takes them in;Hexahedron_64and up read aslagrange_hexahedronwith the file’s own order. An element type XDMF has no name for - a triangle strip, a polyhedron, a Lagrange triangle - is dropped with a warning per type, and the values and tags over the elements are cut to match; a polyhedron in a mixed topology is stepped over on the way in the same way.<Attribute>elements centred onNodeandCellarevertex_attrsandelement_attrs, their shape the DataItem’s, so a vector stays(n, 3)and a tensor(n, 3, 3); theAttributeTypeis written from the shape and not read. One centred onGridlands inglobal_attrsas an array, an<Information Name Value>as text - a repeated name as a list of them - and a<Time Value>underglobal_attrs["time"]. On the way out a number under that key is the<Time>and a text is an<Information>; atime=option wins over either, and a text it displaces is warned about. An attribute centred on a face or an edge, or holding a finite element function, is neither one value per point nor one per cell and is skipped with a warning, as is one whose length matches neither count.vertex_tagsandelement_tagsare<Set>elements ofNodeandCelltype; a face or edge set is skipped.Every
DataItemflavour is read: inline text, HDF5 byfile.h5:/dataset, a binary file at aSeekin eitherEndian, aHyperSlabover any of those, and aReferenceto another item by XPath. The sidecar a file names is found beside it; a spelled path that leaves the file’s own directory - a..or an absolute one - is refused, while a symlink beside the file is followed wherever it leads, that being the usual layout on a cluster. ADataItemdeclaring more values than it holds is refused naming the count; a count past the safety caps is refused before anything is sized.A grid may include another’s topology and geometry with
xi:include xpointer="xpointer(//Grid[@Name="…"]/*[self::Topology or self::Geometry])", the way a solver’s time series does. The pointer’s steps may name a grid, count one (Grid[1]) or start from/Xdmf/Domain; a pointer of another form, or an include of another document, is skipped with a warning. A series reads an included array once and hands each step its own copy. A file holding more than one temporal collection is read at the first; the others are named in a warning and left. A grid named by such a pointer and lying beside a temporal collection rather than in it is the series’ shared mesh, not a mesh of its own, and is not read twice.A lattice topology -
2DSMesh,3DRectMesh,3DCoRectMeshand the rest - is expanded into explicit hexahedra or quadrilaterals, the way.vts,.vtrand.vtiare. ItsDimensionsrun slowest axis first,nz ny nx, and so do the origin and spacing ofORIGIN_DXDYDZ, which is how VTK reads them. The writer spells every mesh unstructured.A planar geometry (
XY) is padded to three columns and flaggedglobal_attrs["was_2d"]; a mesh so flagged that stayed in the plane writesXYback.Several grids merged keep each grid’s own points, so a point block two grids share through a
Referencecomes back once per grid;merge_duplicate_vertices()joins them.
See also
Supported formats - the full format table.