4. Data exchange formats for multi-solver workflows

In a multi-solver CAE pipeline, the weakest link is rarely the physics — it is the handoff between tools. A geometry kernel, a mesher, a CFD solver, a structural solver, and a post-processor each have their own native data model, and the file format you choose at each boundary determines what survives the trip. This chapter maps the six formats that dominate Python-driven multi-solver workflows — STEP, IGES, STL, CGNS, HDF5, and JSON — to the three payload classes they carry: geometry, mesh, and results.

Geometry Interchange: STEP and IGES

STEP (ISO 10303, typically AP203/AP214/AP242) and IGES (ASME Y14.26M) are the two B-rep interchange standards you will encounter when moving CAD into a meshing or solver environment. STEP is the modern default: it carries analytic surfaces, NURBS, assembly structure, colors, and — in AP242 — PMI (Product Manufacturing Information). IGES is older, stores geometry as trimmed surfaces with weaker topology, and frequently produces gaps or duplicate edges on export. Prefer STEP unless a legacy tool forces IGES.

In Python, the practical entry point is pythonocc-core (OCCT bindings) or cadquery. A minimal STEP read-and-inspect looks like this:

from OCC.Core.STEPControl import STEPControl_Reader
from OCC.Core.IFSelect import IFSelect_RetDone

reader = STEPControl_Reader()
status = reader.ReadFile("bracket.step")
if status != IFSelect_RetDone:
    raise RuntimeError("STEP read failed")
reader.TransferRoots()
shape = reader.OneShape()
print("solids:", shape.NbChildren())

Two failure modes dominate. First, unit mismatch: STEP files declare units in the header, but some exporters write millimetres while the receiving tool assumes metres, producing a 1000× scale error. Always verify the bounding box after import. Second, healing requirements: IGES surfaces often need ShapeFix_Shape before meshing. A quick sanity check is to compute the shape's volume and compare it against the CAD-reported value within 0.1%.

Mesh Exchange: STL and CGNS

STL is a triangle soup — no topology, no units, no metadata. It is adequate for surface tessellation and 3D printing, but for volumetric CFD or FEA it is a dead end: you cannot recover element connectivity or boundary patches from an STL. Use STL only as an intermediate for surface remeshing or visualization.

CGNS (CFD General Notation System, now an ISO standard) is the correct choice for mesh plus solution data in CFD. It stores a structured tree: Base → Zone → GridCoordinates and FlowSolution nodes, with boundary conditions attached as BC_t nodes. The Python binding is CGNS (pip install cgns) or h5py when the file is written in ADF/HDF5-backed mode (CGNS 4.x defaults to HDF5).

import CGNS.MAP as CGM
import CGNS.PAT.cgnslib as CGL

tree, links, paths = CGM.load("wing.cgns")
base = tree[2]
zone = base[2][2]
coords = zone[2][1]   # GridCoordinates node
print("nodes:", coords[1][1].shape)

CGNS is verbose but unambiguous. The critical discipline is naming consistency: if the mesher writes a boundary patch as wall and the solver expects WALL, the BC will silently default to a farfield. Validate patch names against the solver's expected list before submitting a run.

Results and Metadata: HDF5 and JSON

HDF5 is the container of choice for large result arrays — displacement fields, pressure histories, transient time series. It supports chunking, compression, and partial reads, which matters when a single field is 4 GB. The h5py API is the standard Python interface:

import h5py, numpy as np

with h5py.File("results.h5", "w") as f:
    grp = f.create_group("case_01/step_0001")
    grp.create_dataset("displacement",
                       data=np.zeros((120000, 3), dtype="f8"),
                       compression="gzip", compression_opts=4)
    grp.attrs["units"] = "m"
    grp.attrs["solver"] = "CalculiX 2.21"

JSON handles the small, human-readable payloads: solver configuration, material cards, run manifests, and cross-tool metadata. It is not suitable for bulk numeric arrays — a 100k-node mesh serialized as JSON is both slow and bloated. The idiomatic split is JSON for control, HDF5 for data, with a JSON manifest referencing HDF5 dataset paths.

A robust multi-solver handoff writes a JSON manifest alongside every binary artifact. The manifest records the source solver, version, units, coordinate system, timestamp, and the HDF5/CGNS path to each field. Downstream tools read the manifest first and fail fast if a required field is missing.

Choosing the Right Format at Each Boundary

BoundaryRecommended formatWhy
CAD → mesherSTEP AP242Preserves B-rep topology, units, and PMI
Legacy CAD → mesherIGESOnly option; expect healing work
Surface → remesher / viewerSTL (binary)Compact, universally readable
Mesher → CFD solverCGNS 4.xCarries zones, BCs, and solution fields
Solver → post-processorHDF5Chunked, compressed, partial reads
Orchestration / configJSONHuman-readable, diff-friendly

The diagram below shows how these formats connect across a typical three-solver workflow, with the payload class carried on each edge.

CAD Kernel SolidWorks / FreeCAD Mesher Gmsh / snappyHexMesh CFD Solver OpenFOAM / SU2 FEA Solver CalculiX / Code_Aster Post-Processor ParaView / Python Orchestrator Python + JSON manifest STEP CGNS HDF5 HDF5 HDF5 JSON Payload classes Geometry (STEP / IGES) Mesh (CGNS / STL) Results (HDF5)

Validation and Failure Symptoms

Format conversion is where silent corruption enters a workflow. Three checks catch most problems before they reach a solver:

  • Bounding-box check. After any geometry import, compare the axis-aligned bounding box against the source CAD. A 1000× discrepancy means a unit mismatch; a 2× discrepancy usually means a mirrored or duplicated body.
  • Patch-name audit. After CGNS import, list all BC_t node names and diff them against the solver's expected boundary list. Missing patches default to wall or farfield and produce plausible-but-wrong results.
  • Array shape and dtype check. Before reading an HDF5 field, assert dataset.shape == (n_nodes, 3) and dataset.dtype == "f8". A transposed array or a float32/float64 mismatch will propagate through the solver without error.
Never assume a format round-trips losslessly. STEP → IGES → STEP will lose topology. CGNS → STL → CGNS will lose boundary conditions. HDF5 → JSON → HDF5 will lose compression and may lose precision. Always keep the native artifact as the source of truth and treat conversions as derived products.

A Minimal Manifest Template

The following JSON manifest is a practical starting point for any multi-solver run. It is intentionally small — extend it with solver-specific fields as needed, but keep the core keys stable so downstream tools can rely on them.

{
  "case_id": "wing_aoa_04",
  "created_utc": "2025-01-15T09:30:00Z",
  "coordinate_system": "right-handed, +Z up, metres",
  "artifacts": [
    {"role": "geometry", "format": "STEP", "path": "geom/wing.step", "units": "m"},
    {"role": "mesh",     "format": "CGNS", "path": "mesh/wing.cgns", "zones": ["fluid"]},
    {"role": "result",   "format": "HDF5", "path": "results/wing.h5",
     "datasets": ["case_01/step_0001/pressure"]}
  ],
  "solver": {"name": "SU2", "version": "7.5.1", "turbulence": "SST"}
}

With this manifest in place, a Python orchestrator can resolve every artifact by role, verify units and coordinate system before each handoff, and fail fast when a required dataset is absent. The format choices — STEP for geometry, CGNS for mesh, HDF5 for results, JSON for control — are not arbitrary; each one is the smallest container that preserves the information the next tool actually needs.