4. Reading LS-DYNA d3plot files with Python

The d3plot file is LS-DYNA's native binary results database. Every explicit run writes one or more of these files, and almost every post-processing task — contour plotting, nodal history extraction, energy checks, or automated report generation — begins by reading them. Unlike ASCII outputs such as nodout or glstat, the d3plot format is a compact, state-based binary layout: geometry (nodal coordinates and element connectivity) is written once at the beginning, followed by a sequence of state blocks, each holding displacement, velocity, acceleration, stress, strain, and other element/nodal fields for a single time step. Understanding this two-part structure is the key to reading it efficiently in Python.

File Structure and the State Model

A d3plot file is organized as a header, a geometry section, and then repeated state data. The header stores control information such as the number of nodes, number of solid/shell/beam elements, and the number of states. The geometry section contains the initial nodal coordinates and the connectivity arrays that define which nodes belong to each element. Each subsequent state block contains the per-node and per-element results at one output time.

This means a reader must distinguish between static data (coordinates, connectivity — read once) and transient data (displacement, stress — read per state). Confusing the two is the most common source of bugs: for example, plotting stress against the initial coordinates instead of the deformed coordinates produces a visually plausible but physically wrong contour.

Key concept: d3plot stores geometry once and results many times. Always separate "read geometry" from "read state" in your code so you can loop over time steps without re-reading connectivity.

Reading with lasso-python

lasso-python is the Python interface to the LS-DYNA LS-PrePost reader library. It exposes a D3plot reader class that handles the binary parsing for you and returns NumPy arrays, which makes downstream processing fast and vectorized.

from lasso.dyna import D3plot, ArrayType

# Open the d3plot (and any d3plot01, d3plot02 ... family files)
d3plot = D3plot("run01/d3plot")

# --- Static geometry ---
node_ids      = d3plot.arrays[ArrayType.node_ids]          # (n_nodes,)
node_coords   = d3plot.arrays[ArrayType.node_coordinates]  # (n_nodes, 3)
solid_conn    = d3plot.arrays[ArrayType.element_solid]     # (n_solids, 8)
shell_conn    = d3plot.arrays[ArrayType.element_shell]     # (n_shells, 4)

# --- Transient state data ---
disp = d3plot.arrays[ArrayType.node_displacement]   # (n_states, n_nodes, 3)
vel  = d3plot.arrays[ArrayType.node_velocity]       # (n_states, n_nodes, 3)
sig  = d3plot.arrays[ArrayType.element_solid_stress]# (n_states, n_solids, 6)

print("nodes:", node_coords.shape[0],
      "states:", disp.shape[0])

The array keys are the important part. ArrayType.node_coordinates gives the reference geometry; ArrayType.node_displacement gives the per-state displacement. To obtain deformed coordinates at state i, add them:

deformed = node_coords + disp[i]   # (n_nodes, 3)

Note the sign and coordinate convention: LS-DYNA uses a right-handed Cartesian system, and displacement is reported as the total displacement from the initial position, not an increment. If you need incremental displacement between two states, subtract them explicitly.

Extracting Connectivity and Building a Mesh

Connectivity arrays are zero-based in lasso-python but reference node indices, not node IDs. If your model uses non-sequential node IDs (common after merging or renumbering), map them through node_ids before indexing coordinates:

import numpy as np

# Map node ID -> array index
id_to_index = {nid: i for i, nid in enumerate(node_ids)}

# Coordinates of the first solid element's nodes
elem0 = solid_conn[0]
xyz0  = node_coords[[id_to_index[nid] for nid in elem0]]

For shell elements, the connectivity is typically 4 nodes (quad) or 3 nodes (tri, padded). Always check the array shape rather than assuming a fixed width, because mixed meshes produce ragged connectivity that the reader pads or splits.

Alternative Parsers

When lasso-python is unavailable or you need finer control, several alternatives exist:

  • LS-PrePost batch mode — run lsprepost -nographics -batch script.cfile to export selected data to CSV, then read the CSV in Python. Robust but slower and requires LS-PrePost installed.
  • pyD3plot / d3plot-reader — lightweight community parsers that read the header and state blocks directly. Useful for scripting on headless clusters, but they support fewer element types.
  • Manual binary parsing with struct or numpy.fromfile — read the control words, then the geometry and state blocks. This is the most portable approach and worth understanding even if you normally use lasso-python.
import numpy as np

with open("d3plot", "rb") as f:
    # First control word: file size / format flag
    (file_flag,) = np.fromfile(f, dtype=np.int32, count=1)
    # ... read header words, then geometry, then states

Manual parsing is error-prone because the format has version-dependent word sizes and optional sections. Use it for targeted extraction (for example, pulling only nodal coordinates) rather than full-database reads.

Practical Workflow and Verification

A reliable extraction routine follows a fixed order: open the file, read geometry, confirm node and element counts against the LS-DYNA output, then loop over states. The count check is your first verification — if node_coords.shape[0] does not match the "number of nodes" reported in the LS-DYNA message file, the reader has mis-parsed the header or you are reading the wrong d3plot family member.

Typical failure symptoms and their causes:

SymptomLikely causeFix
Displacement array is emptyOnly geometry read; no state blocks parsedConfirm the file is not truncated and the reader supports the format version
Coordinates look scaled or shiftedUnit system mismatch (mm vs m)Check the LS-DYNA input units; d3plot stores raw values
Connectivity indices out of rangeUsing node IDs instead of array indicesMap through node_ids
Stress contour looks mirroredSign convention or element orderingVerify against LS-PrePost for one element

Once geometry and state arrays are loaded, the same data feeds contour plotting, nodal history curves, and automated pass/fail checks. The next step is to convert these raw arrays into physically meaningful quantities — which is where the coordinate and sign conventions you established here must remain consistent throughout the analysis.

d3plot Structure and Python Read Path d3plot binary layout Header / control words counts, format version Geometry (static) node coordinates element connectivity State 0 disp, vel, stress State 1 disp, vel, stress ⋮ State N (last time step) parse Python reader lasso-python D3plot("d3plot") ArrayType keys LS-PrePost batch export CSV read with pandas Manual struct numpy.fromfile targeted extraction Verification node/elem count check compare vs LS-PrePost unit consistency arrays NumPy arrays node_coordinates (n_nodes, 3) element_solid (n_solids, 8) node_displacement (n_states, n_nodes, 3) element_solid_stress (n_states, n_solids, 6) Deformed coords coords + disp[i] right-handed XYZ total, not incremental