Skip to content

visualdynamics.io.nastran

nastran

Nastran bulk data (.bdf/.dat/.nas) — geometry, read and written.

The bulk deck is the lingua franca of the FEM world this tool's measurements get correlated against, and its card formats are public knowledge documented in every Nastran vendor's reference guide and decades of literature. This reader is written from those card layouts; no other tool's source was consulted.

What is read is the geometry: GRID points with their definition and displacement systems, CORD2R/C/S coordinate systems (chained references resolved), the connection elements the vocabulary knows, and PLOTEL — Nastran's own display-only line — as tracelines. What is deliberately not read, and why:

  • Properties, materials, loads, constraints, control decks — a bulk file describes an analysis; only its mesh is geometry. These are skipped by card name, silently, the way a UNV reader skips the datasets it was not asked about.
  • Rigid elements (RBE2/RBE3/RBAR/RROD) and MPCs are constraints wearing element names — they carry no mesh and are skipped.
  • CORD1R/C/S (systems defined by grid points) refuse loudly: a guessed frame places every node in it wrongly, and the cards are rare enough that a refusal names the fix (redefine as CORD2).
  • Any other C-prefixed connection card refuses loudly with its name, because an element silently dropped is a mesh that lies.

A deck is unitless by construction, so imports arrive raw and length_unit= declares — exactly the universal-file-without-164 convention.

Writing is the mirror: coordinate systems as CORD2, grids as large-field GRID* (full precision — small-field's eight characters are why so many decks in the wild have five-digit coordinates), elements on their own cards with PID 1 throughout, and tracelines as PLOTEL chains. Properties are an analyst's statement about the structure, not the geometry's to invent, so the written deck is interchange geometry: it meshes viewers and preprocessors, and it would need PSHELL/PSOLID cards added before Nastran itself would run it — the docstring of save says so rather than leaving it to be discovered.

Functions:

Name Description
save

Write a geometry as a bulk deck.

Classes

Functions:

save

save(
    geometry: Geometry,
    path: str | PathLike,
    unit_system: Any = None,
) -> None

Write a geometry as a bulk deck.

Interchange geometry, not a runnable analysis: elements carry PID 1 and no PSHELL/PSOLID/MAT cards are written, because properties are the analyst's statement about the structure and inventing them here would put made-up stiffness in a real deck. Grids go out large-field for full precision. Tracelines become PLOTEL chains — Nastran's own display-only line. Values are written in SI, the geometry's storage.

Source code in src/visualdynamics/io/nastran.py
def save(geometry: Geometry, path: str | os.PathLike,
         unit_system: Any = None) -> None:
    """Write a geometry as a bulk deck.

    Interchange geometry, not a runnable analysis: elements carry
    PID 1 and no PSHELL/PSOLID/MAT cards are written, because
    properties are the analyst's statement about the structure and
    inventing them here would put made-up stiffness in a real deck.
    Grids go out large-field for full precision. Tracelines become
    PLOTEL chains — Nastran's own display-only line. Values are
    written in SI, the geometry's storage.
    """
    from ..core.geometry import ELEMENT_TYPES as VOCABULARY

    lines = ['$ written by visualdynamics\n', 'BEGIN BULK\n']
    for i, cid in enumerate(geometry.cs_id):
        if int(cid) == 0:
            continue
        matrix = np.asarray(geometry.cs_matrix[i], dtype=float)
        name = {0: 'CORD2R', 1: 'CORD2C', 2: 'CORD2S'}[
            int(geometry.cs_type[i])]
        a = matrix[3]
        b = a + matrix[2]          # the z axis point
        c = a + matrix[0]          # in the x-z plane
        lines.append(_card(name, int(cid), 0, *a, *b, *c))
    for i, node in enumerate(geometry.node_id):
        x, y, z = (float(v) for v in geometry.node_xyz[i])
        lines.append(
            f'GRID*   {int(node):>16d}'
            f'{int(geometry.node_def_cs[i]):>16d}'
            f'{x:16.9E}{y:16.9E}*\n'
            f'*       {z:16.9E}'
            f'{int(geometry.node_disp_cs[i]):>16d}\n')
    for i, code in enumerate(geometry.elem_type):
        code = int(code)
        name = _CARD_NAMES.get(code)
        if name is None:
            raise ValueError(
                f'no bulk card for a {VOCABULARY[code][0]} '
                f'(element {int(geometry.elem_id[i])})')
        conn = [int(n) for n in geometry.elem_conn[i]]
        if name == 'CONM2':
            lines.append(_card(name, int(geometry.elem_id[i]),
                               conn[0], 0, 0.0))
        elif name == 'CELAS2':
            lines.append(_card(name, int(geometry.elem_id[i]),
                               0.0, conn[0], 1))
        else:
            lines.append(_card(name, int(geometry.elem_id[i]), 1, *conn))
    plotel = 1 + (max((int(e) for e in geometry.elem_id), default=0))
    for i, conn in enumerate(geometry.traceline_conn):
        chain = [int(n) for n in conn]
        for a, b in pairwise(chain):
            lines.append(_card('PLOTEL', plotel, a, b))
            plotel += 1
    lines.append('ENDDATA\n')
    with open(str(path), 'w') as f:
        f.writelines(lines)