3. Extracting results from CAE solvers

Every CAE results dashboard ultimately depends on one unglamorous but critical capability: turning solver output into clean, typed, Python-native data. Abaqus, ANSYS, and Nastran each produce results in fundamentally different formats, and the extraction strategy you choose determines whether your dashboard pipeline is robust or brittle. This section covers the three dominant extraction patterns — native Python APIs, binary result readers, and text parsing — and shows how to normalize their output into structures that downstream visualization and reporting code can consume without special-casing.

Three Extraction Patterns, Three Trade-offs

Before writing any extraction code, classify your data source. The three solver families expose results through different mechanisms, and each has a distinct cost profile.

Solver Primary extraction route Format Key trade-off
Abaqus odbAccess Python API Binary .odb Rich API, but requires Abaqus Python environment
ANSYS Mechanical PyMAPDL / DPF, or rst via PyAnsys Binary .rst Modern Python stack, version-sensitive
Nastran pyNastran OP2 reader Binary .op2 Pure Python, no solver install needed
Any solver Text report / .csv / .pch ASCII Portable but fragile to format changes

The decision rule is straightforward: prefer a binary reader when one exists for your solver and version, because binary formats carry full precision and structured metadata. Fall back to text parsing only when the binary route is unavailable or when the solver writes a purpose-built report file.

Extracting from Abaqus ODB Files

Abaqus stores results in an output database (.odb). The odbAccess module is only importable inside the Abaqus Python interpreter, so extraction scripts run via abaqus python extract_odb.py or abaqus cae noGUI=extract_odb.py. A typical extraction walks the step, frame, and field output hierarchy:

from odbAccess import openOdb
import json

odb = openOdb('bracket_modal.odb', readOnly=True)
step = odb.steps['Step-1']
frame = step.frames[-1]          # last frame = final converged state

disp = frame.fieldOutputs['U']   # nodal displacement vector
stress = frame.fieldOutputs['S'] # element stress tensor

records = []
for v in disp.values:
    records.append({
        'node': v.nodeLabel,
        'ux': v.data[0], 'uy': v.data[1], 'uz': v.data[2],
        'magnitude': v.magnitude,
    })

with open('disp.json', 'w') as f:
    json.dump(records, f)
odb.close()

Two details matter for correctness. First, fieldOutputs['S'] returns values at integration points by default; call stress.getSubset(position=ELEMENT_NODAL) if you need nodal-averaged stress for contour plots. Second, node labels are not guaranteed to be contiguous, so never assume nodeLabel == index. Always carry the label as an explicit field.

Extracting from ANSYS Result Files

For ANSYS Mechanical, the modern route is PyDPF (Data Processing Framework) or PyMAPDL. DPF reads the .rst file directly and exposes results as fields with associated meshes:

from ansys.dpf import core as dpf

model = dpf.Model('housing.rst')
disp = model.results.displacement.on_last_time_freq()
stress = model.results.stress.on_last_time_freq()

mesh = model.metadata.meshed_region
nodes = mesh.nodes
node_ids = [n.id for n in nodes]

disp_fields = disp.eval()
values = disp_fields[0].data   # shape (n_nodes, 3)

The critical gotcha here is ordering. DPF field data is ordered by the mesh's internal node ordering, which may differ from the node ID order. If you build a DataFrame by zipping node_ids with values, verify alignment by checking that len(node_ids) == values.shape[0] and spot-checking a known node. When in doubt, use dpf.fields_factory scoping to extract by explicit node ID list.

Extracting from Nastran OP2 Files

Nastran's .op2 is a self-describing binary format, and pyNastran handles it without any solver installation — a major advantage for CI pipelines and containerized dashboards:

from pyNastran.op2.op2 import OP2

op2 = OP2()
op2.read_op2('wing_static.op2')

# Displacements: dict keyed by subcase
for subcase, result in op2.displacements.items():
    node_ids = result.node_gridtype[:, 0]
    t1 = result.data[:, 0]   # x translation
    t2 = result.data[:, 1]
    t3 = result.data[:, 2]

# Element stresses
for subcase, result in op2.cquad4_stress.items():
    element_ids = result.element_node[:, 0]
    von_mises = result.data[:, 0]

Note that pyNastran exposes results as dictionaries keyed by subcase ID, and each result object carries its own data array plus identifier columns. This is convenient but means your normalization layer must handle the fact that different result types have different column layouts.

Normalizing to Python-Friendly Structures

The extraction scripts above produce solver-specific objects. The dashboard should never see them. Instead, convert everything to a common shape — typically a pandas DataFrame with a stable schema:

import pandas as pd

def to_frame(node_ids, vectors, quantity='displacement'):
    df = pd.DataFrame(vectors, columns=['x', 'y', 'z'])
    df.insert(0, 'node_id', node_ids)
    df['quantity'] = quantity
    df['magnitude'] = (df[['x','y','z']]**2).sum(axis=1)**0.5
    return df

Adopt a small set of conventions and enforce them everywhere: node and element IDs are integers, never indices; vector components use consistent x/y/z column names; units are recorded in a metadata column or a sidecar JSON, never implied; and every frame carries a subcase or step identifier so time-history and multi-load-case data can be concatenated safely.

Precision and units: Binary readers preserve full double precision. If you round during extraction, round once at the presentation layer, not in the stored DataFrame — otherwise downstream comparisons and contour interpolation lose accuracy.

Validation and Failure Symptoms

Extraction bugs are silent. A misaligned node ordering produces a plausible-looking but wrong contour plot. Build a minimal validation step into every extractor:

  • Count check: number of extracted nodes equals the mesh node count reported by the solver.
  • Range check: maximum displacement magnitude is within a physically reasonable band for the load case.
  • Spot check: a known constrained node shows near-zero displacement.
  • Sum check: for static equilibrium, reaction forces balance applied loads within tolerance.

Typical failure symptoms and their causes: all-zero results usually mean you read the wrong frame or subcase; wildly large values often indicate unit mismatch (mm vs. m); missing nodes point to a subset or set-based extraction that silently dropped entries; and KeyError on a field name means the solver did not write that output — check the *OUTPUT / OUTPUT request block in the input deck.

Solver Output → Python-Native Extraction Pipeline Abaqus .odb (binary) odbAccess API ANSYS .rst (binary) PyDPF / PyMAPDL Nastran .op2 (binary) pyNastran OP2 Text reports .csv / .pch / .txt regex / pandas Extraction Layer read step / frame read subcase pull field outputs carry node/elem IDs Normalization node_id (int) x / y / z columns magnitude subcase / step tag units in metadata Vali- date count range spot pandas DataFrame (stable schema) node_id | x | y | z | magnitude | quantity | subcase Common failure symptoms: all-zero → wrong frame/subcase • huge values → unit mismatch • missing nodes → subset extraction • KeyError → output not requested in deck

Practical Guidance for Dashboard Pipelines

Keep extraction and presentation strictly separated. The extractor's only job is to produce a validated DataFrame; it should not know about colors, chart types, or report layouts. This separation lets you swap solvers without touching dashboard code, and it makes the extraction layer independently testable with small reference models.

For batch processing across many load cases, wrap the extractor in a function that takes a file path and a result-type list, and returns a dictionary of DataFrames keyed by quantity. Cache the extracted results to Parquet rather than re-reading binary files on every dashboard refresh — a 500 MB .op2 can take tens of seconds to parse, while the equivalent Parquet loads in under a second. Record the source file hash and solver version in the cache metadata so stale results are detectable.

Finally, treat the extraction layer as versioned infrastructure. Solver versions change field naming, DPF APIs evolve, and pyNastran result object attributes shift between releases. Pin your reader library versions, and include a smoke test that runs against a tiny reference model in CI so format regressions surface before they reach a report.