Results¶
dynars reads the two LS-DYNA binary result formats — d3plot (the state
database: geometry + per-state fields) and binout (LSDA time histories) —
with numeric data coming back as NumPy arrays in Python and typed Vecs in Rust.
It also writes both formats, so you can synthesize or edit result files.
Reading a d3plot¶
Open the family by its base name; d3plot, d3plot01, d3plot02, … are picked
up automatically.
Geometry and connectivity¶
Geometry is constant across states. Node ids, element connectivity, and part ids
come back as arrays; connectivity is (conn, parts) — the node numbers per
element and the part id each element belongs to.
ids = d.node_ids() # int64[N] user node IDs
xyz0 = d.initial_coordinates() # (N, 3) reference coordinates
parts = d.part_ids() # int64 user part/material IDs
shell_conn, shell_parts = d.shell_connectivity() # (n_shells, 4), (n_shells,)
solid_conn, solid_parts = d.solid_connectivity() # (n_solids, 8), (n_solids,)
print(shell_conn[0], "belongs to part", shell_parts[0])
Per-state nodal data¶
Deformed coordinates and displacement are per state. Read one state, all states at once, or the peak displacement directly.
last = d.num_states - 1
xyz = d.node_coordinates(last) # (N, 3) deformed coords at one state
allc = d.node_coordinates_all() # (num_states, N, 3) — one allocation
disp = d.displacement_magnitudes(last) # (N,) magnitude at one state
peak = d.max_displacement_final() # scalar: peak magnitude at final state
Generic result blocks¶
Element results (stress, plastic strain, history variables) live in per-entity
blocks. block(StateBlock.X) returns the block across selected states as an
(n_states, count, vars) array in the solver's native precision — the raw packed
layout, which you reshape by integration points/layers as needed.
from dynars import StateBlock
solid = d.block(StateBlock.Solid) # (n_states, n_solids, vars)
print(d.block_layout(StateBlock.Solid)) # (count, vars_per_entity)
# `states` selects which to return: None = all, an int = one, a list = those.
last_only = d.block(StateBlock.Shell, states=d.num_states - 1)
# A single-precision contiguous read is zero-copy — a view over the mmap.
disp = d.block(StateBlock.Displacement) # (n_states, N, 3)
use dynars::results::{element, StateBlock};
// The whole block as f64: (data, [n_states, count, vars], entity_ids).
let (data, [n_states, count, vars], _ids) =
d.element_block_f64(StateBlock::Solid).unwrap();
// Or stream a per-part reduction straight off the mmap — no materialization:
let vm = d.part_max_history(StateBlock::Solid, part, element::von_mises_stress);
let _ = (data, n_states, count, vars, vm);
The column meanings are the solver's standard packing (6 stress components, then effective plastic strain, then any history variables you requested). See the API reference for the layout per block.
Element invariants¶
Derived per-element quantities — von Mises, principal stress/strain, pressure,
triaxiality, effective plastic strain — are computed on the d3plot element blocks
(results::element in Rust). Per-part histories (max / percentile /
failure-fraction over states) build on top. See the API reference
for the full set.
Reading a binout¶
binout is a tree of channels addressed by path. Continuation files
(binout0000, binout0001, …) are picked up from a glob pattern automatically.
In Python read is lasso-style: read(branch, var) aggregates a variable
across all output states for you (segments may be separate args or one list).
b = dynars.parse_binout("binout*") # or dynars.Binout("binout*")
print(b.read()) # top-level branches: ['glstat', 'nodout', …]
print(b.read("glstat")) # a branch lists its variable names
# read(branch, var) stacks the variable over every state:
ke = b.read("glstat", "kinetic_energy") # [T] (a global scalar over time)
acc = b.read("nodout", "x_acceleration") # [T, nodes]
# One entity's history — by LS-DYNA id, or by legend name:
n101 = b.read("nodout", "x_acceleration", id=101) # [T] (contiguous)
head = b.read("nodout", "x_acceleration", name="left_head") # [T]
subset= b.read("nodout", "x_acceleration", ids=[101, 102]) # [T, 2]
# The raw tree is still there: channels() lists a directory's children, and a
# literal path (with the state) reads one raw record.
print(b.channels(["nodout"])) # ['d000001', …, 'metadata']
snap = b.read("nodout", "d000001", "x_acceleration") # [nodes] at one state
print(b.ids("nodout")[:5], b.legend("nodout")[:2], b.title("nodout"))
In Rust read is the raw tree walker (returns a ReadResult); aggregate
with read_states (full matrix) or read_columns (chosen columns only).
use dynars::results::Binout;
let b = Binout::new("binout*").unwrap();
let top = b.read(&[]).unwrap(); // raw tree: top-level branches
let st = b.read_states("nodout", "x_acceleration").unwrap(); // dense (T, C) + ids + time
let n101 = st.column_by_id(101); // one node's history (Vec<f64>)
let ke = b.read_time_series(&["glstat", "kinetic_energy"]).unwrap(); // {time, values, ..}
let _ = (top, st, n101, ke);
The structured reader¶
read_states(branch, var) (both languages) returns the aggregation as a struct
/ dict — {time, values (T×C), ids, n_steps, n_channels} — one node's history
is a column. In Python it also takes the same id/ids/name/names
selectors as read, returning {time, values, ids} for just those columns.
read_time_series(path) pairs a single channel with its sibling time array.
Reading many channels at once¶
Pulling a lot of channels? read_many runs the reads in parallel across cores
(GIL released), faster than a Python loop.
Signal processing & injury criteria¶
Result channels are plain arrays, so they chain straight into SAE J211 filtering, Butterworth filters, integration/differentiation, and occupant-injury criteria — implemented in the Rust core and verified bit-exact against SciPy.
import numpy as np, dynars
from dynars import signal, injury
b = dynars.parse_binout("binout*")
# A node's 3-axis acceleration history. read(branch, var) aggregates a
# variable across all output states; `id=` returns one node's contiguous [T].
node = 1000001
t = b.read("nodout", "time") # [T]
dt = t[1] - t[0] # s
ax = b.read("nodout", "x_acceleration", id=node)
ay = b.read("nodout", "y_acceleration", id=node)
az = b.read("nodout", "z_acceleration", id=node)
a = injury.resultant(ax, ay, az) # sqrt(x^2 + y^2 + z^2)
a60 = signal.cfc(a, 60, dt) # SAE J211 CFC-60 (zero-phase)
low = signal.butterworth(a, 4, 300.0, 1/dt, "low") # 4-pole, 300 Hz
v = signal.integrate(a, dt) # cumulative integral (accel -> velocity)
# Occupant injury criteria (acceleration in g):
hic36 = injury.hic36(a, dt) # HIC over a 36 ms window (also hic15, hic)
a3ms = injury.clip(a, dt) # 3 ms clip
csi = injury.severity_index(a, dt) # Gadd severity index
read(branch, var) is the time-history path — it walks the per-state
dNNNNNN records and returns the full dense [T, nodes] matrix (or [T]
for a scalar-per-state channel like time); id=/ids= decode one/several
entities by id, and name=/names= by the branch legend (e.g.
read("nodout", "x_acceleration", name="left_head")) — each without building
the full matrix. A literal read("nodout", "d000001", …) still gives one raw
state; channels([...]) lists a directory's raw children; and
read_states(...) is the structured {time, values, ids} form.
(Filtering and injury criteria ship in the published wheels.)
The Rust injury module also carries the tier-2 criteria (bric, ubric, vc,
nij, nic, tibia_index); see the API reference.
Writing result files¶
You can build a d3plot or binout from scratch — for synthetic test data, for
round-tripping an edited field, or for emitting a derived result LS-PrePost can
open.
Build a d3plot¶
import numpy as np, dynars
from dynars import StateBlock
# A unit cube: 8 nodes, one hex solid + one quad shell.
coords = np.array([[0,0,0],[1,0,0],[1,1,0],[0,1,0],
[0,0,1],[1,0,1],[1,1,1],[0,1,1]], dtype=float)
w = dynars.D3plotWriter(coords, title="demo")
w.add_solids(np.array([[1, 2, 3, 4, 5, 6, 7, 8]]), parts=np.array([1])) # 1-based nodes
w.add_shells(np.array([[1, 2, 3, 4]]), parts=np.array([2]))
w.set_ids(node_ids=list(range(101, 109)), solid_ids=[9001], part_ids=[10, 20])
for s in range(4): # append states
w.add_state(s * 1e-3, coords + [0, 0, 0.1 * s], vel=np.zeros_like(coords))
w.write("demo.d3plot")
# Read it straight back:
d = dynars.open_d3plot("demo.d3plot")
print(d.num_states, d.solid_connectivity()[0].tolist())
use dynars::results::D3plotWriter;
// 8 nodes as flat x,y,z; one hex solid + one quad shell.
let coords: Vec<f64> = vec![
0.,0.,0., 1.,0.,0., 1.,1.,0., 0.,1.,0.,
0.,0.,1., 1.,0.,1., 1.,1.,1., 0.,1.,1.,
];
let mut w = D3plotWriter::new(coords.clone()).unwrap();
w.add_solid([1, 2, 3, 4, 5, 6, 7, 8], 1); // 1-based node ids, part index 1
w.add_shell([1, 2, 3, 4], 2);
w.set_part_ids(vec![10, 20]);
for s in 0..4 {
let disp: Vec<f64> = coords.iter().enumerate()
.map(|(i, &c)| if i % 3 == 2 { c + 0.1 * s as f64 } else { c }).collect();
w.add_state(s as f64 * 1e-3, disp, None, None).unwrap();
}
w.write("demo.d3plot").unwrap();
Editing an existing family in place is D3plotEditor — overwrite node
coordinates or a result block at chosen states and save(); everything else is
preserved byte-for-byte. A full end-to-end walk-through (write → read → edit →
resample) is examples/d3plot_demo.py / examples/d3plot_demo.rs.
Build a binout¶
A binout curve is a value per state (dNNNNNN directories) plus a sibling time
array. BinoutEditor writes an arbitrary tree; the build_series helper writes
the LS-DYNA time-series convention (metadata + per-state dirs) so files read back
as proper histories in dynars and LS-PrePost.
import numpy as np, dynars
# Low-level: an arbitrary curve.
e = dynars.BinoutEditor()
t = np.linspace(0, 1, 12)
for i, ti in enumerate(t):
d = f"d{i + 1:06d}"
e.set(["mycurve", d, "time"], np.float64([ti]))
e.set(["mycurve", d, "energy"], np.float32([np.sin(6 * ti)]))
e.write("out.binout")
# High-level: a proper nodout-style series (one value per entity per state).
ids = np.array([101, 102, 103])
x_disp = np.outer(np.linspace(0, 1, 5), [1.0, 2.0, 3.0]).astype(np.float32) # (T, C)
w = dynars.build_series("nodout", ids=ids, channels={"x_displacement": x_disp},
times=np.linspace(0, 1, 5), labels=[f"node {i}" for i in ids])
w.write("series.binout")
use dynars::results::{BinoutEditor, Data};
// Low-level: an arbitrary curve (one value per dNNNNNN state + a `time`).
let mut e = BinoutEditor::new();
for i in 0..12 {
let t = i as f64 / 11.0;
let d = format!("d{:06}", i + 1);
e.set(&["mycurve", &d, "time"], Data::F64(vec![t])).unwrap();
e.set(&["mycurve", &d, "energy"], Data::F32(vec![(6.0 * t).sin() as f32])).unwrap();
}
e.write("out.binout").unwrap();
The build_series time-series helper (metadata + per-state dirs) is a Python
convenience; in Rust, write the metadata/{ids,legend} and dNNNNNN entries
with BinoutEditor directly.
examples/binout_demo.py / examples/binout_demo.rs are the runnable versions
(create → read → build series → edit).