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. A point mass is the exception that is no invention: a CONM2 carries the mass its element group was given, when it was given one, and 0.0 otherwise. Springs and supports likewise: a group of springs goes out as a CELAS2 per element and direction with its stiffness, and a ground group as SPC1 cards of its components (2026-10-08). Grids go out large-field for full precision. Drawn lines 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.
    A point mass is the exception that is no invention: a CONM2 carries
    the mass its element group was given, when it was given one, and 0.0
    otherwise. Springs and supports likewise: a group of springs goes
    out as a CELAS2 per element and direction with its stiffness, and a
    ground group as SPC1 cards of its components (2026-10-08). Grids go
    out large-field for full precision. Drawn lines 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))
    # each grid is stated in the frame its CP field names, or the deck
    # would carry basic-frame values under a local CP and every reader
    # of it, this one included, would resolve them a second time
    written = to_local(np.asarray(geometry.node_xyz, dtype=float),
                       geometry.node_def_cs, geometry.cs_id,
                       geometry.cs_type,
                       np.asarray(geometry.cs_matrix, dtype=float))
    for i, node in enumerate(geometry.node_id):
        x, y, z = (float(v) for v in written[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')
    # a drawn line — an element group of two-node line elements with no
    # properties — goes out as PLOTEL chains and not as elements too
    drawn = geometry.drawn_lines()
    drawn_groups = {line['group'] for line in drawn}
    groups = getattr(geometry, 'group_properties', None) or {}
    # a spring of several directions is a CELAS2 per direction, the ones
    # after the first numbered past every element; a ground point is a
    # grid on an SPC1 of its components, and no element (2026-10-08)
    spare = 1 + max((int(e) for e in geometry.elem_id), default=0)
    supports: dict[str, list[int]] = {}
    for i, code in enumerate(geometry.elem_type):
        if int(geometry.elem_group[i]) in drawn_groups:
            continue
        props = groups.get(int(geometry.elem_group[i]))
        conn = [int(n) for n in geometry.elem_conn[i]]
        if props is not None and props.kind == 'ground':
            held = ''.join(str(AXES.index(a) + 1) for a in props.ground)
            supports.setdefault(held, []).append(conn[0])
            continue
        if props is not None and props.kind == 'spring':
            eid = int(geometry.elem_id[i])
            for k, stiffness in enumerate(props.stiffness):
                if not stiffness:
                    continue
                far = (conn[-1], k + 1) if len(conn) == 2 else ()
                lines.append(_card('CELAS2', eid, float(stiffness), conn[0],
                                   k + 1, *far))
                eid, spare = spare, spare + 1
            continue
        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])})')
        if name == 'CONM2':
            mass = props.mass if props is not None and props.mass else 0.0
            lines.append(_card(name, int(geometry.elem_id[i]),
                               conn[0], 0, float(mass)))
        elif name == 'CELAS2':
            # a spring element in no group of springs: where it is, with
            # no stiffness to say
            far = (conn[1], 1) if len(conn) == 2 else ()
            lines.append(_card(name, int(geometry.elem_id[i]),
                               0.0, conn[0], 1, *far))
        else:
            lines.append(_card(name, int(geometry.elem_id[i]), 1, *conn))
    for held, grids in supports.items():
        lines.append(_card('SPC1', 1, int(held), *grids))
    plotel = spare
    for line in drawn:
        for chain in line['chains']:
            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)