dynars

 1from dynars._dynars import (
 2    Binout,
 3    BinoutEditor,
 4    Cmp,
 5    D3plot,
 6    D3plotEditor,
 7    D3plotWriter,
 8    Deck,
 9    Entity,
10    File,
11    Finding,
12    FsiforField,
13    IncludeNode,
14    InterfaceField,
15    IntforWriter,
16    Keyword,
17    KeywordFile,
18    Predicate,
19    Report,
20    Rule,
21    Severity,
22    StateBlock,
23    Workspace,
24    open_d3plot,
25    parse_binout,
26    parse_deck,
27    parse_include_tree,
28    parse_keyword_file,
29    write_keyword,
30)
31from dynars import injury, kw, signal  # submodules: dynars.signal.*, dynars.injury.*
32from dynars.binout import build_series
33
34# Friendly alias for the result-block enum (dynars.Block.Displacement).
35Block = StateBlock
36from dynars.schema import (
37    Card,
38    Float,
39    FloatArray,
40    Int,
41    IntArray,
42    Str,
43    keyword,
44    parse_keyword,
45    rows,
46)
47
48__all__ = [
49    "IncludeNode",
50    "KeywordFile",
51    "parse_include_tree",
52    "parse_keyword_file",
53    "write_keyword",
54    # deck: parse once, validate + navigate
55    "parse_deck",
56    "Deck",
57    "Entity",
58    "Keyword",
59    "File",
60    # workspace: batch-parse/validate many decks sharing *INCLUDEs
61    "Workspace",
62    "Rule",
63    "Predicate",
64    "Report",
65    "Finding",
66    "Severity",
67    "Cmp",
68    # binary results
69    "Binout",
70    "D3plot",
71    "D3plotWriter",
72    "D3plotEditor",
73    "IntforWriter",
74    "BinoutEditor",
75    "StateBlock",
76    "InterfaceField",
77    "FsiforField",
78    "Block",
79    "build_series",
80    "parse_binout",
81    "open_d3plot",
82    # post-processing submodules: dynars.signal.*, dynars.injury.*
83    "signal",
84    "injury",
85    # schema authoring
86    "keyword",
87    "parse_keyword",
88    "rows",
89    "Card",
90    "Int",
91    "Float",
92    "Str",
93    "IntArray",
94    "FloatArray",
95    "kw",
96]
class IncludeNode:
def total_files(self, /):

Total number of files in this subtree (including self).

def total_bytes(self, /):

Total bytes across all files in this subtree.

kind
path
children
byte_count
class KeywordFile:

A parsed LS-DYNA keyword file: keyword blocks with lossless round-trip, columnar bulk access as numpy arrays, and block-level editing.

def block_names(self, /):

The keyword name of every block, in file order.

def keyword(self, /, index):

A block as a dict: {"name": str, "options": [str], "cards": [[str]]}.

def set_keyword(self, /, index, name, cards, options=None):

Replace a block's keyword. Cards are re-emitted in free format; the rest of the file stays byte-for-byte intact.

def parse_schema(self, /, keyword, cards, repeat=False):

Parse a keyword against a user-defined schema, returning a dict of columns (numpy arrays for numeric fields, lists for strings).

Low-level: the Python @keyword class layer lowers to this. cards is a list of cards, each a list of (name, type, width, count) field tuples where type is "int" | "float" | "str".

def parse_builtin(self, /, keyword):

Parse a keyword using dynars' built-in library (generated from the pyDYNA field database), returning the same column dict. Errors if the keyword is not in the library.

def to_bytes(self, /):

The (possibly edited) file contents as bytes.

def write(self, /, path):

Write the (possibly edited) file to disk.

dirty

Whether any block has a pending edit.

num_blocks

Number of keyword blocks in the file.

def parse_include_tree(path):

Parse an LS-DYNA keyword file and return the include tree.

Releases the GIL during parsing so other Python threads can run.

def parse_keyword_file(path):

Parse an LS-DYNA keyword file into an editable [PyKeywordFile].

Releases the GIL during the file read and block split.

def write_keyword(path, name, columns):

Author a single-keyword deck from columnar arrays and write it to path — the inverse of the columnar read path (Deck.table / parse_keyword).

columns maps field name to a numpy int64/float64 array (or a list[str]), all the same length N; the cards are emitted in dict order, in free (comma) format, straight from Rust with no per-row Python objects. Writes *KEYWORD / *<name> / N card lines / *END. Rows are formatted in parallel with the GIL released.

def parse_deck(path):

Parse a deck (root + all includes) once and return a navigable [PyDeck].

class Deck:

A parsed LS-DYNA deck (root + all includes). Parse once with [parse_deck], then validate (validate) and navigate (part, material, …) off the same object — no second parse. The resolution indices are built lazily on first use.

def validate(self, /, rules):

Run a set of rules over this deck, reusing the parse. No default rule set — pass the rules you want (e.g. Rule.references_resolve()).

def part(self, /, id):

The *PART with this id, or None if none is defined. Ids are global (post-*INCLUDE_TRANSFORM); the sign is ignored, so |id| also matches.

def material(self, /, id):

The *MAT with this id, or None if none is defined. Ids are global (post-*INCLUDE_TRANSFORM); the sign is ignored, so |id| also matches.

def section(self, /, id):

The *SECTION with this id, or None if none is defined. Ids are global (post-*INCLUDE_TRANSFORM); the sign is ignored, so |id| also matches.

def curve(self, /, id):

The *DEFINE_CURVE with this id, or None if none is defined. Ids are global (post-*INCLUDE_TRANSFORM); the sign is ignored, so |id| also matches.

def parts(self, /):

Every part in the deck (enumerate, don't guess ids).

def materials(self, /):

Every *MAT in the deck (enumerate, don't guess ids).

def sections(self, /):

Every *SECTION in the deck (enumerate, don't guess ids).

def curves(self, /):

Every *DEFINE_CURVE in the deck (enumerate, don't guess ids).

def definition_counts(self, /):

(kind, count) of defined ids, most-numerous first.

def table(self, /, keyword):

Bulk columnar read of every occurrence of keyword across the whole deck (root + includes) using the built-in library, as a dict of numpy arrays (numeric fields) and string lists. The fast path alongside part/material/… navigation — the deck is the one columnar entry, include-aware (unlike the per-file KeywordFile). Raises KeyError if the keyword isn't in the built-in library (use table_with).

def table_with(self, /, keyword, cards, repeat=False):

Bulk columnar read across the whole deck against a user-defined schema — the escape hatch for a keyword not in the built-in library. cards is a list of cards, each a list of (name, type, width, count) field tuples; type is "int" | "float" | "str".

def register_schema(self, /, keyword, cards, repeat=False):

Register a user schema for a keyword the built-in library doesn't cover, so navigation (keywords, part, …) gets named, typed field access for it. cards is a list of cards, each a list of (name, type, width, count) field tuples; type is "int" | "float" | "str". Keyed by canonical base — registering the same base twice replaces it.

def keywords(self, /, keyword):

Every occurrence of keyword across the whole deck (root + includes), as Keyword handles — matched on the canonical base, so SECTION_SHELL also matches SECTION_SHELL_TITLE. The occurrence-navigation counterpart to the columnar table; unlike part/material/… it isn't limited to definition entities.

def files(self, /):

The deck's parsed files as File handles — the root first, then each *INCLUDEd file in include order. File-first navigation: pick a file, then read/edit its keywords.

def file(self, /, suffix):

The first parsed file whose path ends with suffix (e.g. "sub.k" or "mesh/part.k"), as a File — the way into a specific include. None if nothing matches.

class Entity:

A handle to one entity: typed field access, source location, and reference-following. Keeps its [PyDeck] alive.

def field(self, /, name):

Read a field by name (case-insensitive) → int / float / str.

def set_field(self, /, name, value):

Overwrite a named field in place, preserving every other byte of the deck. Returns "in_place", or "reflowed" if the value overflowed its fixed column (that one card re-emitted in free format), or None if the field isn't found. Realise the change with the owning file's write / to_bytes (deck.file(...) / deck.files()).

def reference(self, /, name):

Follow the reference in field name to the entity it points at.

def material(self, /):

Follow this entity's first field that references a *MAT to that material, or None if there is no such field or it doesn't resolve.

def section(self, /):

Follow this entity's first field that references a *SECTION to that section, or None if there is no such field or it doesn't resolve.

def eos(self, /):

Follow this entity's first field that references an *EOS to that equation of state, or None if there is no such field or it doesn't resolve.

def hourglass(self, /):

Follow this entity's first field that references a *HOURGLASS to that hourglass definition, or None if there is no such field or it doesn't resolve.

id
kind

The entity kind (e.g. "Part", "Material", "Section", "Curve").

file

The include file this entity is defined in.

line

1-based line of the entity's *KEYWORD line (jump-to location).

offsets

The effective *INCLUDE_TRANSFORM offsets applied to this entity's file (composed down the include chain) as a dict {"idnoff": …, "ideoff": …}, or None if it sits in the root or a plain *INCLUDE. These are the shifts that turn the file-local ids into the global ones id reports.

keyword

The full *KEYWORD name of the block that defines this entity.

class Keyword:

A keyword occurrence — one *KEYWORD block — reached by name (Deck.keywords) or through a file (File.keywords). Read fields, and edit one in place with set_field. Keeps its [PyDeck] alive.

def field(self, /, name):

Read a field by name (case-insensitive) → int / float / str. Honours a user schema registered with register_schema.

def set_field(self, /, name, value):

Overwrite a named field in place, preserving every other byte of the deck. Returns "in_place" / "reflowed", or None if the field isn't found. Persist via the owning file's write / to_bytes.

file

The include file this occurrence lives in.

name

The full *KEYWORD name of this occurrence (e.g. SECTION_SHELL_TITLE).

line

1-based line of this occurrence's *KEYWORD line (jump-to location).

class File:

One parsed file in a deck — the root or one *INCLUDE instance. Lists its keywords (file-first navigation) and reads/writes its (possibly edited) bytes. Keeps its [PyDeck] alive.

def keywords(self, /, name=None):

The keyword occurrences in this file as Keyword handles. With name, only occurrences of that keyword (canonical-base match); without it, every block in file order.

def to_bytes(self, /):

The (possibly edited) file contents as bytes.

def write(self, /, path):

Write the (possibly edited) file to path.

def set_field(self, /, block, row, col, widths, value):

Low-level, schema-free field write: overwrite (block, row, col) passing the fields' fixed column widths (e.g. [10]*8). For keywords dynars ships no schema for; otherwise prefer Keyword.set_field. Returns "in_place" / "reflowed", or None if the card/field is out of range.

index

This file's index in the deck (0 is the root).

path

This file's path — the resolved *INCLUDE path, or the root deck path.

dirty

Whether this file has a pending edit.

class Workspace:

An in-process batch context: parse and validate many decks that share *INCLUDEs against one shared cache, so common files (mesh, materials) are read, parsed, and indexed once no matter how many decks include them.

import dynars
ws = dynars.Workspace()
decks = ws.parse_decks(["variant_a/main.k", "variant_b/main.k"])
reports = ws.validate_decks(decks, [
    dynars.Rule.references_resolve(),
    dynars.Rule.duplicate_ids(),
])
print(ws.stats())  # {'files_parsed': ..., 'files_reused': ..., ...}

The decks handed back are ordinary Decks — validate or navigate them individually too; a deck from a workspace reuses the shared indices whether you call validate_decks or its own .validate(...).

def parse_deck(self, /, path):

Parse one deck (root + all includes), reusing any file this workspace has already read. Returns a navigable Deck.

def parse_decks(self, /, paths):

Parse several decks in one batch, sharing all file work across them. Returns a list of Decks in input order; raises RuntimeError naming the first root that fails to parse.

def validate_decks(self, /, decks, rules):

Validate several decks in parallel against the shared cache. Returns one Report per deck, in order. Warms the shared definition index first, then runs rules over every deck concurrently — a shared file's id and connectivity indices are built once, not per deck.

def stats(self, /):

Cache stats as a dict: files_parsed / files_reused (disk reads vs. cache hits) and def_indices_built / ref_indices_built (distinct files whose definition / connectivity index was extracted — a shared file counts once).

class Rule:

A built-in declarative rule. Constructed in Python, executed in Rust.

def keyword_forbidden(keyword):

Flag every occurrence of keyword — the keyword must not appear at all.

def field_forbidden_values(keyword, field, values):

Flag any occurrence of keyword whose field equals one of values.

def field_required(keyword, require, when=None):

For every occurrence of keyword, if when holds (or is omitted), the require predicate must also hold; occurrences that violate it are flagged.

def include_missing():

Flag any *INCLUDE that resolves to a file not present on disk.

def references_resolve():

Cross-keyword referential integrity: every id reference resolves (PART.mid → *MAT, *LOAD.lcid → *DEFINE_CURVE, …). Does not check element connectivity.

def references_resolve_with_connectivity():

As references_resolve, and additionally checks that every element's nodes are defined. Heavy on large meshes.

def duplicate_ids():

No two labelled definition entities of the same kind share an id (two *PART pid=5, duplicate *MAT/*SET/*SECTION/*DEFINE_CURVE ids, …). Compared on logical ids, so *INCLUDE_TRANSFORM instances don't collide.

def unreferenced_entities():

Library definition entities nothing references — dead *MAT, *SECTION, *DEFINE_CURVE, *SET, *DEFINE_COORDINATE, … Reports at Warning severity.

def rigid_context():

Rigid-body keywords (*LOAD_RIGID_BODY, *CONSTRAINED_RIGID_BODIES, *CONSTRAINED_EXTRA_NODES, *BOUNDARY_PRESCRIBED_MOTION_RIGID, …) must target a *MAT_RIGID part; flags a reference to a deformable part.

def with_severity(self, /, severity):

Set severity (default Error).

def only_in(self, /, patterns):

Apply only within files whose path contains one of patterns.

def except_in(self, /, patterns):

Apply everywhere except files whose path contains one of patterns.

class Predicate:

A boolean predicate tree over card fields (tier 2). Evaluated in Rust.

def field(field, cmp, value):

field <cmp> value.

def all_(preds):

All sub-predicates must hold (logical AND).

def any_(preds):

Any sub-predicate holds (logical OR).

def not_(pred):

Negation.

class Report:

The result of a validation run.

def is_clean(self, /):

True if there are no Error-severity findings (Warnings are allowed).

def count(self, /, severity):

The number of findings at the given severity.

findings
class Finding:

One rule violation with a clickable file:line.

def location(self, /):

The clickable file:line where this violation was found.

severity
rule
file
message
keyword
line
class Severity:

How serious a violation is.

Error = Severity.Error
Warning = Severity.Warning
Info = Severity.Info
class Cmp:

A comparison operator — used instead of a stringly "eq"/"ne".

Eq = Cmp.Eq
Ne = Cmp.Ne
Lt = Cmp.Lt
Le = Cmp.Le
Gt = Cmp.Gt
Ge = Cmp.Ge
class Binout:

LS-DYNA binout reader: walk the LSDA tree by path, read channels as numpy.

def read(self, /, *path, id=None, ids=None, name=None, names=None):

Read from the binout (lasso-style). Segments may be separate args or one list: read("nodout", …) or read(["nodout", …]).

  • read() / read("nodout")list[str] of children (a branch lists its variable names).
  • read("nodout", "x_acceleration") → the variable aggregated across all output states: float64[T, nodes] (or [T] for a scalar-per-state channel such as time).
  • read("nodout", "x_acceleration", id=1000001) → one entity's history float64[T]; ids=[…]float64[T, k]. Select by entity name (from the branch legend) instead with name= / names=[…]. Selectors decode only the requested column(s) — no full matrix. KeyError if absent.
  • A literal leaf path — read("nodout", "d000001", "x_acceleration") — returns that single state's raw array.

For the structured form (time + ids together) use read_states; for the raw child listing of any directory use channels.

def read_many(self, /, paths):

Read many paths concurrently (lock-free, GIL released), returning a list aligned with paths. Faster than a Python loop when pulling many channels: the reads run in parallel across cores.

def read_f64(self, /, path):

Read a leaf and coerce to float64 (any numeric dtype).

def read_time_series(self, /, path):

Read a time-history: {"time": float64[T], "values": float64[T], "channel": str}. time is read from the sibling time array, or synthesized as 0..T.

def channels(self, /, path=Ellipsis):

Child names at a directory path (empty path = top level).

def read_states(self, /, branch, var, id=None, ids=None, name=None, names=None):

Aggregate a per-state variable across all state dirs, as a dict.

Full matrix (default): {"time": float64[T], "values": float64[T, C], "ids": int64[C], "n_steps": int, "n_channels": int}.

With a selector — id/ids (by entity id) or name/names (by the branch legend) — only those columns are decoded (no full matrix): {"time": float64[T], "values": float64[T] or [T, k], "ids": int64[k]}, where values is 1-D for a single id/name. KeyError if absent, ValueError if more than one selector is given. The bare-array counterpart is read(branch, var, …).

def ids(self, /, branch):

LS-DYNA entity IDs for a state branch (e.g. nodout node IDs), as int64.

def legend(self, /, branch):

Per-entity legend/name strings for a state branch (trimmed).

def title(self, /, branch):

Dataset title for a state branch.

files

The binout files backing this reader, in order.

class D3plot:

LS-DYNA d3plot reader: control block, geometry, per-state nodal results.

def times(self, /):

Simulation time of each state, as a float64 array.

def node_coordinates(self, /, state):

Deformed node coordinates at state (0-based) as an (NUMNP, 3) array.

def node_coordinates_all(self, /):

Deformed node coordinates for every state as a (num_states, NUMNP, 3) array — one call, one allocation, instead of a Python loop over node_coordinates.

def displacement_magnitudes(self, /, state):

Per-node displacement magnitude at state as a (NUMNP,) array.

def max_displacement_final(self, /):

Peak nodal displacement magnitude at the final state.

def initial_coordinates(self, /):

Initial (reference) node coordinates as an (N, 3) array.

def shell_connectivity(self, /):

Shell connectivity: (conn, parts) where conn is (n_shells, 4) one-based node numbers and parts is (n_shells,).

def solid_connectivity(self, /):

Solid connectivity: (conn, parts) where conn is (n_solids, 8).

def node_ids(self, /):

User node IDs (N), default 1..=N.

def shell_ids(self, /):

User shell element IDs.

def solid_ids(self, /):

User solid element IDs.

def part_ids(self, /):

User part/material IDs.

def segment_field(self, /, field, states=None):

Extract one interface-force field's values from the per-segment block as (n_states, n_segments, k). field is an InterfaceField (intfor) or FsiforField (FSIFOR) — no magic strings. states selects states like block. Raises if the field isn't present in this file.

def available_blocks(self, /):

The result blocks present in this d3plot, as StateBlock values.

def block(self, /, block, states=None):

Generic result extraction: any result block across all states as an (n_states, count, vars) numpy array in native precision. block is a StateBlock (or its lowercase name string). Node blocks are (…, 3); element blocks return the solver's raw packed per-entity layout — reshape by integration points/layers as needed. Raises if the block is absent.

states selects which states to return: None = all; an int (or negative int, from the end) = one state; a sequence of ints = those states. Selecting fewer states reads/copies only those.

When the selected states are single-precision and contiguous within one family file, the result is a zero-copy read-only view straight over the memory map (no allocation, no copy). Otherwise the selection is copied into a fresh array (in parallel for large blocks).

def block_layout(self, /, block):

The (count, vars_per_entity) layout of a result block, or None.

filetype

Control-block file type (1 = d3plot, 4 = intfor, …).

num_nodes

Number of nodes (NUMNP) in the mesh.

num_states

Number of output states (time steps) in the file.

is_interface_force

Whether this is an interface-force (intfor) database. In an intfor file the contact segments are in the shell slot: block(StateBlock.Shell) gives (n_states, n_segments, nv2d) and shell_connectivity() the segment nodes; split the per-segment values with interface_fields.

is_fsifor

Whether this is an FSIFOR (ALE) interface-force file — use FsiforField values with segment_field.

class D3plotWriter:

Build a single-precision d3plot from a mesh + per-state nodal results.

def add_shells(self, /, conn, parts=None):

Add shell elements: conn is (M, 4) one-based node ids; parts is an optional (M,) part id per shell (default 1).

def add_solids(self, /, conn, parts=None):

Add solid elements: conn is (M, 8) one-based node ids; parts is an optional (M,) part id per solid (default 1).

def set_ids( self, /, node_ids=None, shell_ids=None, solid_ids=None, part_ids=None):

Set user IDs written into the NARBS numbering section (default 1..N): node IDs (length N), shell/solid element IDs, and part IDs.

def add_state(self, /, time, disp, vel=None, acc=None):

Append a state: time, deformed coords disp (N,3), and optional vel/acc (N,3). Velocity/acceleration presence is fixed by the first state added.

def set_double_precision(self, /, double):

Emit double-precision (8-byte word) output when double is true (default single precision). Values are stored as f64, so this is lossless.

def set_shell_layers(self, /, n_layers):

Number of through-thickness integration points packed into each shell result record (MAXINT). set_shell_results' innermost dim must be n_layers * per_layer.

def add_beams(self, /, conn, parts=None):

Add beam elements: conn is (M, 3) one-based node ids (end, end, orientation); parts optional (M,) part id (default 1).

def add_tshells(self, /, conn, parts=None):

Add thick-shell elements: conn is (M, 8) one-based node ids; parts optional (M,) part id (default 1).

def set_solid_results(self, /, results):

Per-solid result block, (n_states, n_solids, vars) — the same raw layout D3plot.solid_results() returns. Sets NV3D.

def set_shell_results(self, /, results):

Per-shell result block, (n_states, n_shells, vars). Sets NV2D.

def set_beam_results(self, /, results):

Per-beam result block, (n_states, n_beams, vars). Sets NV1D.

def set_tshell_results(self, /, results):

Per-thick-shell result block, (n_states, n_tshells, vars). Sets NV3DT.

def set_global_history(self, /, field, data):

A whole-model global scalar history (one value per state) at field's slot.

def set_part_field(self, /, field, data):

A per-part scalar history (n_states, n_parts) at field.

def set_part_velocity(self, /, data):

Per-part velocity history (n_states, n_parts, 3).

def set_node_field(self, /, field, data):

A per-node thermal/auxiliary field history at field. See D3plotWriter.set_node_field (Rust) for the per-node widths.

def set_element_deletion(self, /, block, alive):

Per-element deletion flags for one family (block): (n_states, n_elem), 1 = alive, 0 = deleted (mdlopt 2).

def set_node_deletion(self, /, alive):

Per-node deletion flags: (n_states, numnp), 1 = alive (mdlopt 1).

def set_element_ids(self, /, beam_ids=None, tshell_ids=None):

User beam / thick-shell element IDs for the NARBS numbering section.

def set_sph(self, /, materials, n_vars, results):

SPH particles: materials (P,), n_vars per particle, results (n_states, P, n_vars).

def set_airbag( self, /, n_airbags, n_particles, n_geom_vars, n_airbag_vars, n_particle_vars, geom, airbag_state, particle_state):

Airbag / CPM: geometry (n_airbags, n_geom_vars), chamber state (n_states, n_airbags, n_airbag_vars), particle state (n_states, n_particles, n_particle_vars).

def set_rigid_bodies(self, /, bodies, motion):

Rigid bodies: bodies is a list of (part_id, node_ids, active_node_ids); motion is (n_states, n_bodies, k) (k = 12 with a rigid road, else 24).

def set_rigid_road(self, /, node_ids, node_coords, segments, motion):

Rigid road: node ids (P,), node coords (P, 3), segments a list of (road_id, [4 node ids per segment]), motion (n_states, n_roads, 6).

def set_rigid_walls(self, /, n_walls, force, position=None):

Rigid walls: force (n_states, n_walls), optional position (n_states, n_walls, 3).

def to_bytes(self, /):

The d3plot as bytes.

def write(self, /, path):

Write the d3plot to path.

class D3plotEditor:

Edit an existing d3plot family in place: overwrite node coordinates or a result block at chosen states; everything else is preserved byte-for-byte.

def set_block(self, /, block, state, data):

Overwrite a result block (a StateBlock) at state with data (count, vars) — the same layout D3plot.block(...) returns.

def set_node_coordinates(self, /, state, coords):

Overwrite deformed node coordinates (N, 3) at state.

def save(self, /):

Overwrite the original files in place.

def write(self, /, path):

Write the edited family to a new base path (path, path01, …).

num_nodes

Number of nodes (NUMNP) in the mesh.

num_states

Number of output states (time steps) in the family.

class IntforWriter:

Build an interface-force (intfor) file: contact segments + per-state nodal motion + per-segment interface values.

def add_segments(self, /, conn, ids=None):

Add contact segments: conn is (M, 4) one-based node ids; ids is an optional (M,) segment id per segment (default 1..M).

def set_node_ids(self, /, node_ids):

User node IDs (length N) for the NARBS numbering section.

def set_fields(self, /, wear=0, pressure=0, shear=0, force=0, gap=0):

Declare the intfor per-segment field layout (nv2d = their sum).

def set_fsifor(self, /, n):

Mark this an FSIFOR (ALE) file with n fixed per-segment values.

def add_state(self, /, time, disp, vel, segment_values):

Append a state: time, deformed disp (N,3), vel (N,3), and segment_values (n_segments, nv2d).

def to_bytes(self, /):

The intfor file as bytes.

def write(self, /, path):

Write the intfor file to path.

nv2d

Values per segment in each state.

class BinoutEditor:

Editable binout: a directory tree of typed datasets that writes back a complete LSDA file. Construct new, or open an existing file and mutate it (save re-emits the whole file).

def list(self, /, path=Ellipsis):

Child names at a directory path (empty path = top level); None if the path is a dataset.

def get(self, /, path):

The dataset at path as a numpy array / str, or None.

def set(self, /, path, values):

Create or overwrite the dataset at path (parent dirs autocreated).

def remove(self, /, path):

Remove the dataset/directory at path; returns whether it existed.

def to_bytes(self, /):

The whole tree serialized as LSDA bytes.

def write(self, /, path):

Write the whole tree to path as an LSDA (binout) file.

class StateBlock:

A per-entity result block in a state. Node blocks are (N, 3); element blocks are (N, vars) where vars is the solver's packed per-element layout (stresses, plastic strain, history variables, per integration point/layer) — returned raw for the caller to reshape.

This is the single source of truth for block identity: the reader/writer use it directly, and (with the python feature) it is exported to Python as the StateBlock enum — no magic strings.

Displacement = StateBlock.Displacement
Velocity = StateBlock.Velocity
Acceleration = StateBlock.Acceleration
ThickShell = StateBlock.ThickShell
class InterfaceField:

A per-segment field in an interface-force (intfor) file. These partition the segment result block (StateBlock::Shell) in this order and sum to nv2d. Exported to Python as the InterfaceField enum — no magic strings.

class FsiforField:

A per-segment field in an FSIFOR (ALE interface-force) file. These are single-value fields in this fixed order; the file carries as many as |nv2d|. Exported to Python as the FsiforField enum.

RelativeVelocity = FsiforField.RelativeVelocity
VelocityX = FsiforField.VelocityX
VelocityY = FsiforField.VelocityY
VelocityZ = FsiforField.VelocityZ
Block = <class 'builtins.StateBlock'>
def build_series( branch: str, ids: Union[Sequence[int], numpy.ndarray], channels: Mapping[str, numpy.ndarray], *, times: Union[Sequence[float], numpy.ndarray, NoneType] = None, cycles: Union[Sequence[int], numpy.ndarray, NoneType] = None, labels: Optional[Sequence[str]] = None, title: str | None = None, editor: BinoutEditor | None = None) -> BinoutEditor:
 46def build_series(
 47    branch: str,
 48    ids: Sequence[int] | np.ndarray,
 49    channels: Mapping[str, np.ndarray],
 50    *,
 51    times: Sequence[float] | np.ndarray | None = None,
 52    cycles: Sequence[int] | np.ndarray | None = None,
 53    labels: Sequence[str] | None = None,
 54    title: str | None = None,
 55    editor: BinoutEditor | None = None,
 56) -> BinoutEditor:
 57    """Build a binout time-series branch and return the :class:`BinoutEditor`.
 58
 59    Parameters
 60    ----------
 61    branch:
 62        Top-level group name, e.g. ``"nodout"``, ``"elout"``, ``"rcforc"``.
 63    ids:
 64        Entity ids (nodes/elements/…), 1-D, length ``nent``.
 65    channels:
 66        Mapping of channel name -> array. A 2-D array is ``[nstate, nent]`` (one
 67        row per state); a 1-D array is treated as a per-state scalar ``[nstate]``.
 68        Numeric arrays keep their dtype; pass float32 to match LS-DYNA output.
 69    times, cycles:
 70        Optional per-state ``time`` (float64) and ``cycle`` (int32), length
 71        ``nstate``. ``time`` is strongly recommended — post-processors key on it.
 72    labels:
 73        Optional per-entity text labels (written as the 80-char ``legend`` block).
 74    title:
 75        Optional run title (80-char metadata field).
 76    editor:
 77        Add the branch to an existing editor instead of a fresh one (so several
 78        branches can share one file).
 79
 80    Returns
 81    -------
 82    BinoutEditor
 83        Ready to ``.write(path)``.
 84    """
 85    e = editor if editor is not None else BinoutEditor()
 86    ids = np.asarray(ids)
 87    if ids.ndim != 1:
 88        raise ValueError("ids must be 1-D")
 89    nent = int(ids.shape[0])
 90
 91    # Infer the number of states from the first 2-D channel, else from times.
 92    nstate = None
 93    for arr in channels.values():
 94        a = np.asarray(arr)
 95        if a.ndim == 2:
 96            nstate = int(a.shape[0])
 97            break
 98    if nstate is None:
 99        if times is not None:
100            nstate = int(np.asarray(times).shape[0])
101        elif cycles is not None:
102            nstate = int(np.asarray(cycles).shape[0])
103        else:
104            raise ValueError("cannot infer number of states: pass a 2-D channel, times, or cycles")
105
106    # Validate shapes up front so we fail before writing anything.
107    for name, arr in channels.items():
108        a = np.asarray(arr)
109        if a.ndim == 2 and a.shape != (nstate, nent):
110            raise ValueError(f"channel {name!r} has shape {a.shape}, expected {(nstate, nent)}")
111        if a.ndim == 1 and a.shape[0] != nstate:
112            raise ValueError(f"scalar channel {name!r} has length {a.shape[0]}, expected {nstate}")
113        if a.ndim not in (1, 2):
114            raise ValueError(f"channel {name!r} must be 1-D (scalar/state) or 2-D (state x entity)")
115
116    # metadata
117    e.set([branch, "metadata", "ids"], ids.astype(np.int64))
118    e.set([branch, "metadata", "legend"], _legend_int8(labels, nent))
119    if title is not None:
120        e.set([branch, "metadata", "title"], _fixed_int8(title, 80))
121
122    # per-state dirs
123    times = None if times is None else np.asarray(times, dtype=np.float64)
124    cycles = None if cycles is None else np.asarray(cycles, dtype=np.int32)
125    for s in range(nstate):
126        d = f"d{s + 1:06d}"
127        if times is not None:
128            e.set([branch, d, "time"], np.array([times[s]], dtype=np.float64))
129        if cycles is not None:
130            e.set([branch, d, "cycle"], np.array([cycles[s]], dtype=np.int32))
131        for name, arr in channels.items():
132            a = np.asarray(arr)
133            if a.ndim == 2:
134                e.set([branch, d, name], np.ascontiguousarray(a[s]))
135            else:
136                e.set([branch, d, name], np.asarray(a[s]).reshape(1))
137    return e

Build a binout time-series branch and return the BinoutEditor.

Parameters

branch: Top-level group name, e.g. "nodout", "elout", "rcforc". ids: Entity ids (nodes/elements/…), 1-D, length nent. channels: Mapping of channel name -> array. A 2-D array is [nstate, nent] (one row per state); a 1-D array is treated as a per-state scalar [nstate]. Numeric arrays keep their dtype; pass float32 to match LS-DYNA output. times, cycles: Optional per-state time (float64) and cycle (int32), length nstate. time is strongly recommended — post-processors key on it. labels: Optional per-entity text labels (written as the 80-char legend block). title: Optional run title (80-char metadata field). editor: Add the branch to an existing editor instead of a fresh one (so several branches can share one file).

Returns

BinoutEditor Ready to .write(path).

def parse_binout(pattern):

Open an LS-DYNA binout for reading (mirrors [PyBinout::new]).

def open_d3plot(path):

Open an LS-DYNA d3plot for reading (mirrors [PyD3plot::new]).

def keyword(name: str, repeat: bool = True):
 93def keyword(name: str, repeat: bool = True):
 94    """Register a class as the schema for keyword `name`.
 95
 96    Fields directly on the class define a single card; a ``cards = [...]`` list
 97    of card classes defines a multi-card layout. `repeat=True` (the default)
 98    parses the card group repeatedly over the block body — the common case for
 99    `*NODE`, `*ELEMENT_*`, multiple `*PART`s, etc. Pass `repeat=False` only to
100    read a single entity per block.
101    """
102
103    def deco(cls: type) -> type:
104        cls._dynars_schema = _lower(name, cls, repeat)
105        _REGISTRY[name.upper()] = cls
106        return cls
107
108    return deco

Register a class as the schema for keyword name.

Fields directly on the class define a single card; a cards = [...] list of card classes defines a multi-card layout. repeat=True (the default) parses the card group repeatedly over the block body — the common case for *NODE, *ELEMENT_*, multiple *PARTs, etc. Pass repeat=False only to read a single entity per block.

def parse_keyword(kf, schema):
111def parse_keyword(kf, schema):
112    """Parse a keyword from `kf` (a `KeywordFile`) and return a dict of columns:
113    numpy arrays for numeric fields, lists for string fields.
114
115    `schema` may be a `@keyword` class, or a keyword name (str). A name is first
116    looked up among your registered `@keyword` classes, then falls back to
117    dynars' built-in keyword library (generated from the pyDYNA field database),
118    so common keywords parse with no declaration at all.
119    """
120    if isinstance(schema, str):
121        cls = _REGISTRY.get(schema.upper())
122        if cls is None:
123            return kf.parse_builtin(schema)  # fall back to the built-in library
124    else:
125        cls = schema
126    name, cards, repeat = cls._dynars_schema
127    return kf.parse_schema(name, cards, repeat)

Parse a keyword from kf (a KeywordFile) and return a dict of columns: numpy arrays for numeric fields, lists for string fields.

schema may be a @keyword class, or a keyword name (str). A name is first looked up among your registered @keyword classes, then falls back to dynars' built-in keyword library (generated from the pyDYNA field database), so common keywords parse with no declaration at all.

def rows(columns):
130def rows(columns):
131    """Iterate a parsed keyword's columns as per-row dicts — handy for
132    low-volume keywords (materials, sections, ...):
133
134        for mat in dynars.rows(dynars.parse_keyword(kf, "MAT_ELASTIC")):
135            print(mat["MID"], mat["E"])
136
137    The columns stay columnar/numpy; this is a lazy view, so bulk keywords
138    should index the arrays directly rather than materialize millions of dicts.
139    """
140    if not columns:
141        return
142    n = len(next(iter(columns.values())))
143    for i in range(n):
144        yield {k: v[i] for k, v in columns.items()}

Iterate a parsed keyword's columns as per-row dicts — handy for low-volume keywords (materials, sections, ...):

for mat in dynars.rows(dynars.parse_keyword(kf, "MAT_ELASTIC")):
    print(mat["MID"], mat["E"])

The columns stay columnar/numpy; this is a lazy view, so bulk keywords should index the arrays directly rather than materialize millions of dicts.

class Card:
69class Card:
70    """Base for a keyword card (one line). Subclass and assign fields in order."""

Base for a keyword card (one line). Subclass and assign fields in order.

def Int(width: int) -> dynars.schema._Field:
44def Int(width: int) -> _Field:
45    """A signed-integer field `width` columns wide (fixed format)."""
46    return _Field("int", width)

A signed-integer field width columns wide (fixed format).

def Float(width: int) -> dynars.schema._Field:
49def Float(width: int) -> _Field:
50    """A floating-point field `width` columns wide (fixed format)."""
51    return _Field("float", width)

A floating-point field width columns wide (fixed format).

def Str(width: int) -> dynars.schema._Field:
54def Str(width: int) -> _Field:
55    """A string field `width` columns wide (fixed format)."""
56    return _Field("str", width)

A string field width columns wide (fixed format).

def IntArray(count: int, width: int) -> dynars.schema._Field:
59def IntArray(count: int, width: int) -> _Field:
60    """`count` consecutive integer fields, returned as one `(N, count)` column."""
61    return _Field("int", width, count)

count consecutive integer fields, returned as one (N, count) column.

def FloatArray(count: int, width: int) -> dynars.schema._Field:
64def FloatArray(count: int, width: int) -> _Field:
65    """`count` consecutive float fields, returned as one `(N, count)` column."""
66    return _Field("float", width, count)

count consecutive float fields, returned as one (N, count) column.