Skip to content

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 namefrd
Extensions.frd
Read / Write✓ / ✓ (ASCII by default or binary, one step per write or a series)
Extra dependencies—

Reading ​

python
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.vtu

Both 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 ​

python
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 below

write 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:

WrittenFrom
2C nodes, ids 1…nthe points (planar points get z = 0; a coordinate that is not finite is a WriteError)
3C elements, ids 1…mthe 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 resulteach 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 headermeshio: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:group and frd: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 _mises and _principal arrays derived=True adds 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.5 is a WriteError and no file is written; one too small is written as zero.
  • Values keep six significant digits. Reading a file ccx wrote and writing it back reproduces it; a double computed elsewhere is rounded.

Binary output ​

python
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 float32 rounding of each value (about seven significant digits, against E12.5's six), and a finite value beyond the float32 range is a WriteError with no file written. The ASCII range limit does not apply.
  • The 100C node 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_ids does not apply. A binary file holds as many nodes and elements as int32 does (the long form's limit).
  • A buffer has to take bytes (io.BytesIO); the Python writer writes it.
  • The default stays ASCII, and ascii / binary in-place verbs, convert --ascii and the MCP convert tool's mode (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 through write_sequence instead.

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 ​

python
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 typeCell typeNodesWritten by
1hexahedron8C3D8, and the expanded S4, B31
2wedge6C3D6, and the expanded S3
3tetra4C3D4
4hexahedron2020C3D20, and the expanded S8, B32
5wedge1515C3D15, and the expanded S6
6tetra1010C3D10
7triangle3cgx only
8triangle66cgx only
9quad4cgx only
10quad88cgx only
11line2T3D2
12line33T3D3

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 typePermutationWhat differs
4 (hexahedron20)0..11, 16..19, 12..15the four vertical mid-edge nodes come before the top ring
5 (wedge15)0..8, 12, 13, 14, 9, 10, 11the three vertical mid-edge nodes come before the top ring
12 (line3)0, 2, 1end, 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 dataMeaning
meshio:timethe 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:stepthe step counter of the 100C record (the mode number of a frequency step)
frd:analysisthe 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 _2 suffix. Components the file marks as calculated (the ALL entry of DISP) 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 .vtu written from them displays correctly. This is the convention for a six-component symmetric tensor in meshio++; see mesh data model.
  • points_only skips every result block and arrays parses 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:

ArrayMeaning
<NAME>_misessqrt(½((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:

KeyRecord
1C, 1Uheader and user text (ignored), 1PSTEP and friends (ignored)
2Cnode block: -1, node id, three E12.5
3Celement block: -1, element id, type, group, material; then -2 lines of node ids
100Cone 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
9999end

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).

BlockRecord
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.

python
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 — cgx itself is not in the test environment.
  • A second 2C or 3C block (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_metadata reads it in full (fell_back_to_full_read is 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 .inp that ccx reads.
  • Conformance — what a write and a read keep.
  • Sequences — the steps of a multi-increment file.
  • PVD and VTKHDF — the time-series outputs a .frd converts to.

Released under the MIT License.