Skip to content

visualdynamics.io.nastran

nastran

Read and write Nastran bulk data decks (.bdf, .dat, .nas) as geometry.

The bulk data card formats are documented in every Nastran vendor's reference guide, and this reader is written from those card layouts. INCLUDE files are read relative to the deck. Lines before BEGIN BULK (executive and case control) and after ENDDATA are ignored; a file with neither marker is read as all bulk data. Small-field, large-field and free-field (comma) cards are all read.

What is read:

  • GRID points, with their definition (CP) and displacement (CD) coordinate systems. Positions are converted to the basic frame.
  • CORD2R, CORD2C and CORD2S coordinate systems, with chains of references resolved. A system defined in a cylindrical or spherical system is refused.
  • The connection elements CROD, CBAR, CBEAM, CTRIA3, CTRIA6, CQUAD4, CQUAD8, CTETRA, CPENTA, CHEXA and CPYRAM, one element group per property id. The node count picks the variant (CTETRA with 4 grids is a tet4, with 10 a tet10).
  • PSOLID, PSHELL and MAT1, as the properties of each element group: the material, and a shell's thickness. A group whose cards are not all in the deck has no properties.
  • CONM2 point masses, one element group per distinct mass. A CONM2 with an offset or inertia is read as an element with no properties.
  • CELAS1 (with its PELAS) and CELAS2 springs, one element group per distinct stiffness and direction. A spring joining two different components, or acting on a scalar point, is refused.
  • SPC and SPC1 supports, one ground group per distinct set of components. A deck with more than one SPC set, or an SPC that enforces a displacement, is refused.
  • PLOTEL, Nastran's display-only line, as one drawn line.

What is not read:

  • Loads, other property and material cards, and control cards. These describe the analysis, not the mesh, and are skipped by card name without a note.
  • Rigid elements (RBE1, RBE2, RBE3, RBAR, RBAR1, RROD, RTRPLT, RSPLINE), which are constraints with no mesh, and are skipped.
  • CORD1R, CORD1C and CORD1S (systems defined by grid points) are refused with an error that says to redefine them as CORD2 cards, since a guessed frame would place every node in it wrongly.
  • Any other C card shaped like an element is refused with its name, so an element is never dropped without a word.

A deck has no units, so positions and properties are read as written, and the geometry is marked as a deck's (Geometry.from_deck). length_unit declares the length the deck is written in and mass_unit its mass, with seconds: the consistent system a deck uses, from which force, stress and density follow (mm with tonnes is N and MPa, inches with lbf·s²/in is lbf and psi). The mass unit may be left out for the usual one with that length (geometry.DECK_MASS_UNITS). Declared here or later on the geometry (Geometry.define_units), the coordinates and every property value are converted to SI together.

save writes coordinate systems as CORD2 cards, grids as large-field GRID* cards (full precision, where a small field holds about five digits), elements on their own cards and drawn lines as PLOTEL chains. The deck is geometry for interchange: it opens in viewers and preprocessors, and needs property and material cards added before Nastran will run it.

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.

The deck is geometry for interchange, not a runnable analysis. The elements have PID 1 and no PSHELL, PSOLID or MAT cards are written, because properties are the analyst's statement about the structure and writing made-up ones would put a wrong stiffness in a real deck. Masses, springs and supports are written, since their values are part of the geometry: a point mass is a CONM2 with its element group's mass (0.0 when the group has none), a group of springs is one CELAS2 per element and direction with its stiffness, and a ground group is SPC1 cards (set 1) of its components. Grids are written large-field in the coordinate system each one is defined in, for full precision, and drawn lines are written as PLOTEL chains.

Values are written as the geometry stores them: SI when its units are defined. unit_system is accepted and not used.

Raises ValueError for an element type with no bulk card.

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.

    The deck is geometry for interchange, not a runnable analysis. The
    elements have PID 1 and no PSHELL, PSOLID or MAT cards are written,
    because properties are the analyst's statement about the structure
    and writing made-up ones would put a wrong stiffness in a real deck.
    Masses, springs and supports are written, since their values are part
    of the geometry: a point mass is a CONM2 with its element group's mass
    (0.0 when the group has none), a group of springs is one CELAS2 per
    element and direction with its stiffness, and a ground group is SPC1
    cards (set 1) of its components. Grids are written large-field in the
    coordinate system each one is defined in, for full precision, and
    drawn lines are written as PLOTEL chains.

    Values are written as the geometry stores them: SI when its units are
    defined. `unit_system` is accepted and not used.

    Raises ValueError for an element type with no bulk card.
    """
    # Masses, springs and supports were added on 2026-10-08.
    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)