CalculiX results (.frd)
The result file of CalculiX: the ASCII .frd that ccx writes and its post-processor cgx reads. CalculiX takes Abaqus-style .inp input, which meshio++ already writes, so reading its results closes the loop for an open-source solver: mesh → .inp → ccx → .frd → .vtu / VTKHDF, with no proprietary tool in the chain. Writing one is the other direction: a surrogate's prediction, an anonymised or converted result, or a fixture for a post-processing script, handed to cgx or ccx2paraview beside the real run.
| Format name | frd |
| Extensions | .frd |
| Read / Write | ✓ / ✓ (ASCII by default or binary, one step per write or a series) |
| Extra dependencies | — |
Reading
import meshioplusplus
mesh = meshioplusplus.read("beam.frd") # the first increment
last = meshioplusplus.read("beam.frd", time_step=-1) # the last one
mesh.point_data["DISP"] # (n, 3) displacements
mesh.point_data["STRESS"] # (n, 6) xx yy zz xy yz zx
mesh.field_data["meshio:time"]
# von Mises and principal values beside every stress / strain tensor
mesh = meshioplusplus.frd.read("beam.frd", derived=True)
mesh.point_data["STRESS_mises"], mesh.point_data["STRESS_principal"]
meshioplusplus.write("beam.vtu", mesh) # or: meshioplusplus convert beam.frd beam.vtuBoth engines (the C++ core and the Python reference) read the whole format, including its binary layout (*NODE OUTPUT/*ELEMENT OUTPUT, auto-detected — no separate call needed). Buffers are read by the Python reader.
Writing
import meshioplusplus
mesh = meshioplusplus.read("beam.frd", time_step=-1)
mesh.point_data["DISP"] *= 2.0 # or a surrogate's prediction
meshioplusplus.write("beam_scaled.frd", mesh) # the long (I10) ASCII layout `ccx` writes
meshioplusplus.frd.write("small.frd", mesh, long_ids=False) # the short (I5) layout, 99999 nodes or elements at most
meshioplusplus.frd.write("beam_bin.frd", mesh, binary=True) # the binary layout, see belowwrite is the inverse of read, in the ASCII layout ccx itself writes (or the binary one), by both engines (the two write the same bytes; MESHIOPLUSPLUS_STRICT_CORE=1 proves the core wrote it). One write is one step; the same step fields drive the 100C header that read fills:
| Written | From |
|---|---|
2C nodes, ids 1…n | the points (planar points get z = 0; a coordinate that is not finite is a WriteError) |
3C elements, ids 1…m | the cells of the twelve types above, in the file's node order (the inverse of Node order); frd:group (default 0) and frd:material (default 1) are each element's group and material and must fit I5 (int32 in the binary layout) |
one -4 block per result | each point_data array, under its own name (at most eight printable characters), (n,) or (n, c); the -5 component names are ccx's for DISP, VELO, FORC, NDTEMP, STRESS, TOSTRAIN, MESTRAIN and ZZSTR and C1…Cn otherwise; DISP and FORC get the calculated ALL component ccx writes |
the 100C header | meshio:time (default 0), frd:step (default 1) and frd:analysis (default 0, static) |
.frd is a nodal format, holds no sets and prints six digits (E12.5), so a write loses what the format cannot express, each with a warning and a provenance note:
- Cell data other than
frd:groupandfrd:material, regions, and field data other than the three step keys are dropped. - Cells with no FRD element (
vertex,pyramid,polygon,polyhedron, the other high-order types) are dropped; their points stay. - A point-data array whose name is longer than eight characters (so the
_misesand_principalarraysderived=Trueadds are never written), that is not one value or vector per node, or whose dtype is not numeric is dropped. - A node with a non-finite value in an array is left out of that block (the reader returns NaN there). A value too large for
E12.5is aWriteErrorand no file is written; one too small is written as zero. - Values keep six significant digits. Reading a file
ccxwrote and writing it back reproduces it; a double computed elsewhere is rounded.
Binary output
meshioplusplus.frd.write("beam_bin.frd", mesh, binary=True) # Python; the C++ core writes it
meshioplusplus.write("beam_bin.frd", mesh, binary=True) # the generic write passes the keyword on
meshioplusplus.frd.SeriesWriter("run_bin.frd", binary=True) # and so does a series
meshioplusplus.write_sequence("run_bin.frd", steps, binary=True)binary=True (C++: FrdWriteOptions::mBinary, or WriteOptions::mEncoding = WriteEncoding::Binary through registry_write_ex, mio_write_ex and the native CLI) writes the layout ccx writes for *NODE OUTPUT and *ELEMENT OUTPUT, which is the binary layout the reader already takes: the ASCII banner and headers, with the 2C and 3C format flags 3 and 2 and the 100CL flag 2, then raw little-endian records with no -1/-2/-3 markers, and 9999 to end. Ids, element types, groups and materials are int32 (so a group or material beyond I5 is fine, one beyond int32 is a WriteError), coordinates are float64 (so the points round-trip exactly) and results are float32. What changes against the ASCII write:
- Results keep single precision. A round trip gives back the
float32rounding of each value (about seven significant digits, againstE12.5's six), and a finite value beyond thefloat32range is aWriteErrorwith no file written. The ASCII range limit does not apply. - The
100Cnode count is the number of records that follow. A node with a non-finite value is left out of the block as in ASCII, and the binary header counts only the nodes written, because the reader takes the record count from it (the ASCII header still names every node). long_idsdoes not apply. A binary file holds as many nodes and elements asint32does (the long form's limit).- A buffer has to take bytes (
io.BytesIO); the Python writer writes it. - The default stays ASCII, and
ascii/binaryin-place verbs,convert --asciiand the MCPconverttool'smode(ascii,binary) reach both layouts. The in-place verbs read the first increment only, as they do for every format, so run them on a multi-step file throughwrite_sequenceinstead.
How it is checked. The reader of both engines reads what the writer writes (tests/python/test_frd.py::TestBinaryWriter, tests/cpp/test_frd.cpp): every fixture, including the three binary files ccx 2.23 wrote, round-trips to the single-precision rounding of its values, and the two engines write the same bytes. Reading a real ccx binary file and writing it back reproduces ccx's own node and element records byte for byte, and a hand-assembled file in the test pins the layout independently of both. ccx2paraview opens the file as text and splits it into lines on the -1/-2 markers, so it cannot read the binary layout (it parses the long ASCII layout only); there is no external validator for the binary files in the test suite or the CI workflow, and cgx -b is not available there.
Time series
with meshioplusplus.frd.SeriesWriter("run.frd") as w: # or: meshioplusplus.write_sequence("run.frd", steps)
for time, mesh in steps:
w.write(time, mesh)A result file holds one mesh and any number of increments, so a series writes the mesh with the first step and one 100C increment (a 1PSTEP and 100C header and a -4 block per array) per step; read(time_step=k) reads step k back, and write_sequence / convert in_*.vtu out.frd / the Sequence pipeline step reach it as a fan-in. A step's value is the time you pass; its number is its frd:step when positive and not used by an earlier step, else the next free one; its analysis type is its frd:analysis. Every step must have the first step's cells (otherwise a WriteError asks for one file per step with {step}); points that move are written as the first step's, with one warning. The C++ class is FrdSeriesWriter (its constructor takes a FrdWriteOptions for the binary layout), the Python one meshioplusplus.frd.SeriesWriter(path, long_ids=True, binary=False); both write the same bytes. A fan-in reaches the binary layout with Encoding = binary (write_sequence(..., binary=True), the C++ SequenceOutput::mOptions.mEncoding); ASCII stays the default.
Validator for the ASCII layout: ccx2paraview 3.2.0 (optional, pip install ccx2paraview vtk) reads written files in tests/python/test_frd.py and returns the same points, cells, DISP, STRESS and TOSTRAIN. It parses the long (I10) ASCII layout only, and its principal-value step fails under numpy 2 on ccx's own files, so that step is stubbed in the test; cgx -b is not in the test environment. The ccx2paraview test is not wired into the CI workflow either (it skips unless the packages are installed). See the roadmap (§3.2 and §2.2.15) for the validators still to wire.
What is read
The mesh is the mesh CalculiX wrote, not the mesh you gave it. ccx expands shells and beams into solids before it writes the results, so an S8 shell comes back as a hexahedron20 and a B32 beam as another one, with more nodes than the .inp had. Trusses (T3D2, T3D3) and solids come back as themselves.
| FRD type | Cell type | Nodes | Written by |
|---|---|---|---|
| 1 | hexahedron | 8 | C3D8, and the expanded S4, B31 |
| 2 | wedge | 6 | C3D6, and the expanded S3 |
| 3 | tetra | 4 | C3D4 |
| 4 | hexahedron20 | 20 | C3D20, and the expanded S8, B32 |
| 5 | wedge15 | 15 | C3D15, and the expanded S6 |
| 6 | tetra10 | 10 | C3D10 |
| 7 | triangle | 3 | cgx only |
| 8 | triangle6 | 6 | cgx only |
| 9 | quad | 4 | cgx only |
| 10 | quad8 | 8 | cgx only |
| 11 | line | 2 | T3D2 |
| 12 | line3 | 3 | T3D3 |
Types 7 to 10 are the shells of the cgx manual; ccx 2.23 never writes them, so they are tested against a hand-written file rather than solver output. An element of any other type is skipped with a warning.
Consecutive elements of one type form one cell block. The element group and material numbers are the integer cell data frd:group and frd:material.
Node order
The he20, pe15 and be3 layouts differ from the Abaqus order that meshio++ uses, and are permuted on read (connectivity[k] = frd_nodes[perm[k]]):
| FRD type | Permutation | What differs |
|---|---|---|
4 (hexahedron20) | 0..11, 16..19, 12..15 | the four vertical mid-edge nodes come before the top ring |
5 (wedge15) | 0..8, 12, 13, 14, 9, 10, 11 | the three vertical mid-edge nodes come before the top ring |
12 (line3) | 0, 2, 1 | end, mid, end instead of end, end, mid |
The tables live in the shared node-ordering registry under "frd". They were confirmed against ccx 2.23: the .frd of each single-element deck in the test suite lists its nodes in the permuted order, and after the permutation every mid-node lies on its edge. The Python tests check exactly that. A quadratic beam is the exception: ccx expands it into a hexahedron20 whose "mid-edge" nodes are the far end of the beam, which is its own convention and no geometry to check against.
Results and steps
Each 100C block of the file is one result (DISP, STRESS, TOSTRAIN, NDTEMP, FORC, FLUX, ERROR and whatever else the -4 record names) at one increment. The increments are the steps of the sequence engine: time_step picks one (0 is the first, negative counts from the end, out of range is an error), read_metadata(...).time_values lists them, and read_sequence walks them.
| Field data | Meaning |
|---|---|
meshio:time | the increment's value: the time of a transient run, the load factor of a static one, the frequency of a frequency step (cycles per time unit, as ccx prints it, not the eigenvalue) |
frd:step | the step counter of the 100C record (the mode number of a frequency step) |
frd:analysis | the analysis type: 0 static, 1 time step, 2 frequency, 3 buckling, 4 user |
Increments are told apart by their frame id as well as their value, because the modes of a frequency step can share a frequency (the two bending modes of a square beam do).
- Point data. One array per result, named as the file names it: one component is
(n,), three(n, 3), six(n, 6), more(n, c). A node the block has no value for is NaN. A repeated name within one increment gets a_2suffix. Components the file marks as calculated (theALLentry ofDISP) are not data and are dropped. - Symmetric tensors keep the file's order
xx yy zz xy yz zx(SXX SYY SZZ SXY SYZ SZX), which is also VTK's, so a.vtuwritten from them displays correctly. This is the convention for a six-component symmetric tensor in meshio++; see mesh data model. points_onlyskips every result block andarraysparses only the blocks it names, so reading one field of a large file does not parse the rest.
Derived fields
derived=True adds two arrays beside each six-component STRESS, TOSTRAIN, MESTRAIN or ZZSTR:
| Array | Meaning |
|---|---|
<NAME>_mises | sqrt(½((xx−yy)² + (yy−zz)² + (zz−xx)²) + 3(xy² + yz² + zx²)), the formula of ccx2paraview, also for strains |
<NAME>_principal | (n, 3), the eigenvalues in ascending order (min, mid, max) |
Both are NaN where any of the six components is. derived is an argument of the format's own reader (meshioplusplus.frd.read, _core.frd_read, read_frd(path, options, FrdReadOptions{}) in C++) and calls the shared tensor_invariants operation internally, keeping these two output names for backward compatibility; the generic read, the CLIs, the MCP server and the C, Fortran, Julia, R and WASM surfaces do not carry the derived flag itself, but tensor_invariants reaches every one of them and also adds hydrostatic and deviatoric. Where derived is not available, read the raw tensor and run tensor_invariants on it.
Record layout
A .frd is a stream of fixed-column records keyed by their first columns:
| Key | Record |
|---|---|
1C, 1U | header and user text (ignored), 1PSTEP and friends (ignored) |
2C | node block: -1, node id, three E12.5 |
3C | element block: -1, element id, type, group, material; then -2 lines of node ids |
100C | one result block: the frame id, the value, the node count, the analysis type, the step counter and the format flag; then -4 (name, component count, type), one -5 per component, -1 rows and -3 |
9999 | end |
The format flag is 0 for the short ASCII layout (I5 ids), 1 for the long one (I10 ids), 2 for binary with float32 reals and 3 for binary with float64 reals; each header line carries its own flag, since ccx writes node coordinates as flag 3 and result values as flag 2 by default. ccx writes the long ASCII layout for *NODE FILE/*EL FILE and the binary layout for *NODE OUTPUT/*ELEMENT OUTPUT. ASCII values are E12.5 with no separator, so negatives run together (7-1.18144E-06 is a node id and a value) and every field is sliced by column. A result with more than six components continues on -2 lines with a blank node field.
Binary layout
*NODE OUTPUT/*ELEMENT OUTPUT (mirroring *NODE FILE/*EL FILE's own syntax, but note the keyword is *ELEMENT OUTPUT, not *EL OUTPUT) write a file whose header lines (1C/1U, 2C, 3C, 1PSTEP, 100CL, -4, -5) stay plain ASCII text, each terminated by \n. Immediately after a 2C/3C header, or after a 100C frame's last -5 line, comes a raw little-endian record blob: one fixed-size record per node, element or result entry, back to back, with no -1/-2/-3 line markers and no line boundaries of its own — a record's bytes may well contain 0x0A. The record count is read from the header's own count field (2C/3C columns 7-36; a 100C header's "numnod" field, columns 25-36, shared by ASCII and binary but only the binary reader needs it, since ASCII instead scans to the next -3).
| Block | Record |
|---|---|
2C (nodes) | int32 node id, then PointDim reals (always float64 / flag 3 from ccx) |
3C (elements) | int32 element id, int32 FRD type, int32 group, int32 material, then nodesPerType int32 node ids — the same four-field header and node list as the ASCII long format's -1/-2 lines, concatenated with no markers |
100C (results) | int32 node id, then DataComps reals (float32 / flag 2 from ccx by default) — DataComps is ncomps minus any component the -5 lines mark calculated, exactly as in ASCII |
Verified against real ccx 2.23 output (tests/python/meshes/frd/*_bin.frd, generated by tools/gen_frd_fixtures.py): every value matches the ASCII rendition of the same run to float32 precision. The host is assumed little-endian, like every other binary format in this codebase. The file ends with 9999 (one leading space, as ccx ends a binary file; the reader accepts the ASCII form's two as well). The writer (above) produces this layout: flag 3 on 2C, 2 on 3C and 100CL, float64 coordinates, float32 results.
The .dat tabular print
*NODE PRINT and *EL PRINT write a companion jobname.dat file: whitespace-separated tables, no mesh and no fixed-column layout to slice. meshioplusplus.frd.read_dat(path) is a helper that parses it into plain tables (a list of dicts, below); it does not return a Mesh, is not registered as a format (so meshioplusplus.read("beam.dat") does not reach it, the extension already belongs to Tecplot) and has no writer. It is Python-only by design: .dat needs a free-format tokenizer that detail/fast_number.hpp does not provide, and a table list has no place in the Mesh-in, Mesh-out native registry, so no C++, C, Fortran, Julia, R or WASM counterpart is planned.
import meshioplusplus
tables = meshioplusplus.frd.read_dat("beam.dat")
for t in tables:
print(t["quantity"], t["kind"], t["set"], t["components"])
# "displacements" "node" "LOAD" ["vx", "vy", "vz"]
# "stresses" "element" "E" ["sxx", "syy", "szz", "sxy", "sxz", "syz"]Each table is a dict with step, increment, time, kind ("node" or "element"), quantity, set, ids, components and a (len(ids), len(components)) values array; an element table also carries int_points, the integration-point index of each row. The tensor component order is the file's own sxx syy szz sxy sxz syz, not .frd's xx yy zz xy yz zx (the last two are swapped) — and *EL PRINT values are per integration point, not nodally averaged the way .frd's STRESS is, so the two do not compare row for row, only in aggregate (see tests/python/test_frd.py::TestDatFile).
Quirks & limitations
- The cgx shell types 7-10 and the derived fields are checked against a hand-written file and NumPy/ccx2paraview's own formula respectively —
cgxitself is not in the test environment. - A second
2Cor3Cblock (some post-processor exports repeat them) is ignored with a warning; the first wins. - The whole file is parsed before a step is chosen, and
read_metadatareads it in full (fell_back_to_full_readis true): there is no header-only path. - No node ids are kept. Points are numbered in the order of the node block; results are matched by id.
See also
- Abaqus — the
.inpthatccxreads. - Conformance — what a write and a read keep.
- Sequences — the steps of a multi-increment file.
- PVD and VTKHDF — the time-series outputs a
.frdconverts to.