FEBio plot file (.xplt)
The .xplt file is the binary results database FEBio writes during a run and FEBio Studio opens. It holds the mesh once and then one state per saved time. meshio++ reads it one state at a time, so a run converts to a .vtu sequence. The model's input is the separate .feb file.
| Format name | xplt |
| Extensions | .xplt (also recognised by content: its magic, in either byte order) |
| Read / Write | ✓ / ✓ (conformance) |
| Extra dependencies | zlib for compressed files: built into the C++ core with -DMESHIOPLUSPLUS_WITH_ZLIB=ON; the Python fallback uses the standard library's |
Reading
import meshioplusplus
mesh = meshioplusplus.read("run.xplt") # the first state
last = meshioplusplus.read("run.xplt", time_step=-1) # the last one
meshioplusplus.xplt.time_values("run.xplt") # every state's time
for time, mesh in meshioplusplus.read_sequence("run.xplt"):
...meshioplusplus convert run.xplt 'out_{step}.vtu' # one .vtu per state
meshioplusplus info run.xplt # blocks, regions, arrays, timesread also takes points_only/arrays (read only the named arrays) and lenient (read FEBio's tet5/tet15 domains as tetra/tetra10). Both engines read every state alike.
Writing
meshioplusplus.write("run.xplt", mesh) # one state
meshioplusplus.write_sequence("run.xplt", steps) # a state per stepmeshioplusplus convert model.vtu model.xpltBoth engines write the same bytes: plot version 0x0035, little-endian, uncompressed unless compress=True is asked for, with the node ids FEBio 4 writes. Each cell block is a domain (one element type each: the types of the mapping table except surfaces' facets; hexahedron27 goes through the "febio" node-ordering table), and a cell region that is exactly one block names its domain and, if it has a tag, is the domain's part id. The other point and cell regions are written as node sets and element sets, which is how they come back. Point data are nodal variables, cell data per-element variables (FMT_ITEM, on every domain, so a block must carry every array) and numeric field data global variables; one component is a float, three a vec3f, six a symmetric mat3fs, nine a mat3f, any other width an array. The time of a state is field_data["meshio:time"] and its status "xplt:status". A series is a state per step over the first step's variables, and the file is written when the writer closes. A step whose mesh differs from the previous step's (other points, cells, facets or sets: a remeshed run, as FEBio's <MeshAdaptor> writes it) is preceded by a new mesh section, and each state is read on the mesh before it (a compressed file packs it as a stream of its own); a step with other variables, or other widths, is refused, since a plot file holds one dictionary. Surfaces, edges and facet sets are the inverse of the mapping table. A block of triangle, quad, triangle6, triangle7, quad8, quad9 or triangle10 cells whose cell_data["xplt:surface"] is one positive id is not a domain but part of a surface: the blocks of one id are one surface (numbered in id order), named after the cell region that holds exactly its cells, else Surface<k>, and written as the surface section's facet list ([facet id, node count, nodes], padded to the widest facet). A line or line3 block with a positive cell_data["xplt:edge"] is an edge in the same way (Edge<k>). Neither marker is written as a variable, and the cell region of a surface or edge is not also written as an element set. Every side region whose name no written surface carries is a facet set of the faces it names (nodes as the cell lists them, each padded to ten), which is exactly the facet set the reader turns back into that side region; a facet that is a 2-D cell's edge or does not exist is dropped with a warning. Surface and edge variables are the cell data that has a finite value on the cells of a surface (edge): one per-facet (FMT_ITEM) variable of that name in the surface (edge) section of the dictionary, with a region per surface (edge) in id order holding its facets' values in order (a vector or tensor keeps its width). A surface whose values are all NaN gets no region, which is how a variable FEBio plots on one surface only reads back. An array with a finite value on a domain cell is a domain variable instead, never also a surface or edge one, since a read file has one array per name. A per-region (FMT_REGION) variable is written per facet. A point-valued surface variable is averaged onto the points by the reader, and one read per surface node (FMT_NODE) leaves field_data["xplt:surface_node:<name>"] beside its point array (xplt:edge_node:<name> for an edge); a point array with that marker is written back as a surface (edge) variable per node: for each data surface the values at its nodes, each node once, in the order its facets first list them, and a surface whose values are all NaN gets no region. Where a node lies on two surfaces the reader's average is one value, which both surfaces then write; a marker on a mesh with no such facets is ignored, and a per facet-node (FMT_MULT) variable comes back as a plain point array and is written as a nodal variable. A read file therefore writes and reads back with its surfaces, edges, facet sets and their variables, but a surface or edge block without its marker is an ordinary shell or beam domain. Vertices and other cell types without a plot element and material-point variables are not written (a warning names the cells). Compression is off by default; write(path, mesh, compress=True) (and SeriesWriter(path, compress=True), so write_sequence(..., compress=True)) sets the header's compression flag and stores every state as its own zlib stream at level 6, which FEBio and the reader inflate. The C++ core needs -DMESHIOPLUSPLUS_WITH_ZLIB=ON for this and otherwise declines, so the Python writer (standard-library zlib) does it; both engines produce identical bytes whenever they link the same zlib, and are otherwise value-equal. FEBio Studio opens the files in the same layout the fixtures here use; no FEBio run has read a file meshio++ wrote.
File structure
After a 4-byte magic (0x00464542), the file is a tree of chunks, each a u32 id, a u32 payload size and the payload. The layout follows FEBio's writer (FEBioPlot/FEBioPlotFile.cpp, PltArchive.cpp) and FEBio Studio's reader (XPLTLib/xpltReader3.cpp):
- a root with the header (plot version, compression flag, software) and the dictionary: every plot variable's name, type (
float,vec3f,mat3fs,mat3fd,mat3f,tens4fs, arrays) and storage format; - the mesh: nodes (from version 0x0033 with their ids), domains (element type, part, elements), surfaces, edges, node sets, element sets, facet sets, parts;
- one state per saved time: the time, a status, and each variable's values per region.
When compression is on, every chunk after the first mesh (every state) is its own zlib stream. Plot versions 0x0030 and later are read, which covers FEBio 3 and 4 (FEBio 4.12 writes 0x0035), in either byte order. FEBio 2's plot files are refused, naming the version.
Mapping
.xplt | meshio++ |
|---|---|
| a domain | a cell block, and a cell region tagged with its part id. The region is named after the domain; FEBio 4 names only solid domains, so a nameless one takes the name of the element set holding exactly its elements (FEBio writes one per <Elements> block), else its part's. |
| node set / element set | point / cell region |
| surface (a data surface: the plot variables' regions) | a block of facet cells per facet type (triangle, quad, triangle6, quad8, quad9...) after the domains, a cell region of the same name over them, and a side region when every facet is a face of a solid (since v16.10.0) |
| facet set | a side region when every facet is a face of a solid, otherwise its own cell block and a cell region, as for .feb |
| which data surface a cell is | cell_data["xplt:surface"]: its 1-based surface id, 0 on the domains |
| edge (FEBio 3.5 and later: named line segments, the carrier of edge variables) | a line (or line3) block per segment type after the surfaces, and a cell region of dimension 1 of the same name over them (since v16.11.0) |
| which edge a cell is | cell_data["xplt:edge"]: its 1-based edge id, 0 elsewhere |
| the state's time, index, status | field_data["meshio:time"], "xplt:step", "xplt:status" |
| nodal variables | point data |
domain variables stored per element (FMT_ITEM) | cell data, NaN on domains that do not carry the variable |
domain variables stored once per domain (FMT_REGION) | cell data, the value repeated over the domain's cells |
domain variables stored per element node (FMT_NODE, FMT_MULT) | point data, averaged over the elements that share each node |
| global variables | field data |
| surface variables | on the facet cells, in the same way: per facet (FMT_ITEM) and once per surface (FMT_REGION) as cell data, NaN on the domains and other surfaces; per surface node (FMT_NODE) and per facet node (FMT_MULT) as point data, averaged at shared nodes. A variable with values on no surface of the mesh gives no array. |
| edge variables | on the line cells, exactly as surface variables are on facet cells: per segment and once per edge as cell data, per edge node (edge-local nodes numbered in first-seen order, as FEBio's FEEdge does) and per segment node as point data (since v16.11.0) |
| material-point variables | not read; one warning names them |
Values are FEBio's float32, widened to float64. Components keep FEBio's order. A symmetric tensor (mat3fs) has 6 components, xx, yy, zz, xy, yz, xz, which is also VTK's order. A diagonal tensor has 3 components, and a full one (mat3f) 9 in row-major order.
Compared with FEBio Studio's VTK export. FEBio Studio exports node data and per-element data the same way. For per-element-node data it keeps the value of the last element written at a shared node, where meshio++ averages; the two agree for continuous fields. FEBio Studio drops mat3f and tens4fs data, while meshio++ keeps them. It writes hex27 as a 20-node hexahedron, while meshio++ keeps all 27 nodes through the "febio" node-ordering table.
Notes
- A state cut short (FEBio killed mid-write) is dropped with a warning, and the earlier states are still read.
- A remeshed run (FEBio's
<MeshAdaptor>) writes a new mesh before the states that use it. Since v16.10.0 each state is read on its own mesh, so the points, cells and regions change from one step of a sequence to the next.read_metadatadescribes the first mesh. - FEBio 4.12 plots a variable on one surface only: asked for the same variable on two surfaces, it writes values for one of them, and the other is NaN. It also writes the traction record of a pressure-loaded surface empty.
- Validation, made outside the repository with FEBio 4.12 built from source: the displacement read from FEBio's plot files equals FEBio's own
node_datalog tofloat32precision at every state, for three models. febio-python's reader (MIT) gives identical displacements, strains, stresses and fibre vectors for its0x0031samples. The test fixtures are plot files FEBio 4.12 wrote for the modelstools/gen_febio_fixtures.pygenerates, one of them compressed. FEBio 4.12 has one edge variable,edge contact gapon the edge of an edge-to-surface sliding contact; thexplt_edgefixture plots it for three free nodes above a bar bent towards them, and the gap read shrinks along the edge as the bar rises. In the suite, thefacet areaa surface fixture plots equals the area of the deformed facets andsurface areatheir sum, and the remeshed fixture's states have the node counts FEBio's log reports (12, 45, 78, 111) with the prescribed end displacement at each.