3. Reading d3plot files with Python

A d3plot file is not a single monolithic record. LS-DYNA writes it as a sequence of binary control blocks and data blocks, each with its own fixed layout and word size. Before any contour plot or time-history curve can be produced, a Python reader must walk that sequence in the correct order: read the header, discover how many nodes and elements exist, locate the geometry arrays, then step through each state record. This section shows how to do that with lasso-python, and what each block actually contains.

Why a Dedicated Reader Is Necessary

The d3plot format is documented only partially, and its layout depends on runtime flags. The number of integration points, whether solid elements are present, whether the model is 2D or 3D, and whether the file was written in single or double precision all change the byte offsets. Reading it with numpy.fromfile alone is possible but fragile. lasso-python encapsulates the format knowledge and exposes a clean object model, so the same script works across LS-DYNA versions and precision modes.

Install it into the working environment with:

python -m pip install lasso-python

For a typical run, the solver produces a family of files: d3plot, d3plot01, d3plot02, and so on. The first file holds the header, the full geometry, and the first batch of states; subsequent files hold additional states only. A reader must therefore treat the set as one logical database.

Opening the Database and Reading the Header

The entry point is d3plot.D3plot. It accepts either a single file or a glob pattern that captures the whole family:

import lasso.d3plot as d3plot

db = d3plot.D3plot("run/d3plot*")

print(db.title)
print(db.version)
print(db.n_nodes, db.n_solids, db.n_shells, db.n_beams)
print(db.n_states, db.n_timesteps)

The header block carries the model title, the LS-DYNA version string, the code (e.g. LS-DYNA3D), and the global counts. These counts are the contract for everything that follows: the geometry arrays must have exactly n_nodes rows, and each state record must contain one value per node or per element for every requested result. If a script assumes a fixed node count and the model changes, every downstream array is silently misaligned. Always read the counts from the header rather than hard-coding them.

Two header details matter in practice. First, db.n_states is the number of stored time states, which is usually smaller than the number of solver cycles because output is written at intervals. Second, the header records the precision. A double-precision run doubles the byte size of every floating-point array; lasso-python handles this internally, but it explains why a file that "looks the same" can be twice as large.

Geometry: Nodes, Elements, and Connectivity

Geometry is read once and reused for every state. The node coordinates are exposed as a NumPy array:

nodes = db.nodes          # shape (n_nodes, 3), float32 or float64
shells = db.shells        # connectivity for shell elements
solids = db.solids        # connectivity for solid elements

Connectivity arrays are zero-based in lasso-python, whereas the LS-DYNA input deck numbers nodes from one. This off-by-one is the single most common source of wrong element lookups. When cross-referencing a node ID from the keyword file, subtract one before indexing nodes.

Element connectivity also encodes the element type. A shell is defined by four node indices, a solid by eight, a beam by two. The reader groups them accordingly, so a script can iterate over shells and solids separately without parsing the raw stream. For models with multiple parts, the part list and its element ranges are available through the database object, which lets you restrict processing to a single component instead of the whole mesh.

If db.n_solids is zero but the model clearly contains bricks, the file was probably written without solid output, or the reader picked up only the first file in the family. Check the glob pattern and the header counts together.

State Data: Time, Displacement, and Stress

Each state corresponds to one output time. The state object holds the time value and the result arrays for that instant:

state = db.states[10]
print(state.time)

disp = state.node_displacements      # (n_nodes, 3)
vel  = state.node_velocities         # (n_nodes, 3)
stress = state.shell_stress          # (n_shells, n_integration_points, n_components)

The displacement array is the basis for deformed-shape plots: add it to the reference coordinates to obtain current positions. Stress and strain arrays are stored per integration point, so a contour plot must first reduce them — for example, taking the through-thickness maximum of the von Mises equivalent. The component ordering follows the LS-DYNA convention (for shells, typically sig_xx, sig_yy, sig_xy, ...), and the reader preserves that order.

Because states are independent, a time-history extraction is a loop over states at a fixed node or element index:

import numpy as np

node_id = 4821
history = np.array([s.node_displacements[node_id] for s in db.states])
times   = np.array([s.time for s in db.states])

This produces a (n_states, 3) array that can be plotted directly or written to CSV. The same pattern applies to element stress, with the element index replacing the node index.

How the Blocks Fit Together

The diagram below shows the read order and the relationship between the header, the shared geometry, and the per-state result blocks. Geometry is read once; states are read repeatedly, each referencing the same node and element indexing.

d3plot Read Order: Header → Geometry → States Header Block title, version, code n_nodes, n_shells n_solids, n_states precision flag Geometry (read once) nodes (n_nodes, 3) shell connectivity solid connectivity part / element ranges State Records time node displacement node velocity shell / solid stress State loop (repeats for each output time) state[i] time = t_i state[i+1] time = t_i+1 state[n-1] last time shared indexing

Practical Checks and Common Failures

Three symptoms appear repeatedly when reading d3plot files programmatically. First, a FileNotFoundError or an empty state list usually means the glob pattern missed the family — confirm that d3plot01 exists and that the pattern is not quoted in a way the shell expands unexpectedly. Second, arrays that are shorter than n_nodes indicate that the reader stopped at the first file; the state count from the header will not match the number of states actually loaded. Third, contour values that look shifted by one element point to the one-based versus zero-based indexing mismatch described earlier.

A reliable script therefore validates the header counts against the loaded array shapes before doing any computation:

assert db.nodes.shape[0] == db.n_nodes
assert len(db.states) == db.n_states
assert db.states[0].node_displacements.shape[0] == db.n_nodes

With the header, geometry, and state data correctly parsed, the database is ready for the operations that follow: extracting time histories, computing derived quantities such as von Mises stress, and rendering deformed or contoured geometry. The key discipline is to treat the header as the authoritative description of the file and to let every array shape be checked against it.