Skip to content

MSC Nastran HDF5 results (.h5) ​

The result database MSC Nastran writes when the bulk data carries MDLPRM,HDF5,... (MSC Nastran 2016 and later). Patran and post-processing scripts read it instead of the older OP2. It holds the model and the results in one self-describing HDF5 file of compound tables, so reading it needs no Nastran installation: .h5 → .vtu / VTKHDF / any meshio++ format, with every subcase, mode, time or frequency as a step.

Format namenastran_h5
Extensions.h5 (GiD's longer .post.h5 stays gid)
Read / Write✓ / ✓ (conformance)
Extra dependenciesHDF5 (the C++ core); h5py for the Python reader

Reading ​

python
import meshioplusplus

mesh = meshioplusplus.read("job.h5")                   # the first result domain
mode3 = meshioplusplus.read("job.h5", time_step=2)     # the third one

mesh.point_data["DISPLACEMENT"]          # (n, 3) translations
mesh.point_data["DISPLACEMENT_ROT"]      # (n, 3) rotations
mode3.point_data["EIGENVECTOR"]          # a mode shape
mesh.cell_data["STRESS:X"]               # per block, NaN where the card has no such output
mesh.cell_data["STRESS:X@corner"]        # (cells, corners): the corner values of solids and shells
mesh.cell_data["STRESS:X1@ply"]          # (cells, plies): composite ply stresses
mesh.field_data["nastran:subcase"], mesh.field_data["meshio:time"]

meshioplusplus.nastran_h5.time_values("job.h5")   # every step's TIME_FREQ_EIGR
bash
meshioplusplus convert job.h5 'mode_{step}.vtu'    # one .vtu per step
meshioplusplus convert job.h5 job.pvd              # or a ParaView collection

Both engines read the whole format: the C++ core, and a Python reference reader on h5py that the core falls back to.

Writing ​

bash
meshioplusplus convert model.vtu job.h5 -o nastran_h5

Both engines write the same tables, value for value, and complete each one to MSC's own column set: the columns, their order, their types and the defaults MSC writes for what the mesh gives no value for (zeros, CEN/ for a shell's TERM, GRID for a solid's CTYPE), taken from the six MSC Nastran 2020 files in the test suite (tools/gen_nastran_h5_schema.py). Members of an array with no MSC column come after MSC's columns and before DOMAIN_ID. /NASTRAN/INPUT holds the GRID table, one table per element card (CROD, CTUBE, CONROD, CBAR, CBEAM, CSHEAR, CELAS1/2, CDAMP1/2, CVISC, CONM2, PLOTEL, CTRIA3/6, CQUAD4/8, CQUAD, CTETRA, CPYRAM, CPENTA, CHEXA) and the PROD, PTUBE, PBAR, PBEAM, PELAS, PDAMP, PVISC, PSHEAR, PSHELL, PSOLID and grouped PCOMP (IDENTITY and PLY) tables the properties need, with the MAT1, MAT2 and MAT4 tables of the materials they name; the values are the nastran:<CARD>_<id>:<MEMBER> field data the readers return, and nastran:card, nastran:x/nastran:g0 and the spring and damper values pick the card of a line. /NASTRAN/RESULT holds the nodal tables (DISPLACEMENT, EIGENVECTOR, ...) and the element tables, named as MSC names them (a CQUAD4's stress is QUAD_CN, its force QUAD4_CN, a composite's QUAD4_COMP, a bar's BARS, a beam's BEAM), with the /INDEX twins and the DOMAINS table. The model and the result model are those of the OP2 writer, so the mesh round trips through either file. Element ids may repeat across cards, as they do in MSC's own files, but not within one. A series (write_sequence, SeriesWriter) is one domain per step. A <G>:<M>@corner array is written as the array member M of the group's table: entry 0 is the centre value <G>:<M> and entry k the value at the cell's k-th node, with a GRID column naming those nodes (a 0 marks an entry with no node). A <G>:<M>@ply array is written as the per-ply rows of the _COMP table, with a PLY column; a <G>:<M>@station array as the station rows of BARS (one row per station, in SD) or the 11 station slots of BEAM. What has no table: CBUSH, CGAP, CMASS1/2 and CONM1 elements and the MAT5, MAT8 and MAT9 materials have no table in the MSC files at hand, so their layout is not guessed; the elements are left out with one warning (the OP2 file holds them) and the materials are written as MAT1. PBARL, PBEAML and PCOMPG values have no table either: their bars, beams and shells are written on the plain PBAR, PBEAM and PSHELL cards, with a warning. Arrays with no other place in the file are dropped with a warning.

Which files ​

Only MSC Nastran's schema is read. The reader refuses, with a ReadError naming the reason and never a guess:

  • a .h5 without /NASTRAN/INPUT/NODE/GRID, i.e. any other HDF5 file under that extension;
  • a file whose /NASTRAN VERSION attribute does not start with msc. Other vendors write incompatible HDF5 layouts under similar names, and a file that reads without error but means something else is worse than no file. A file without the attribute is read.

The schema grows with every MSC release. The reader relies on nothing specific to one release: it reads the tables and members it knows by name, and treats a table it does not know as described below. It was checked against MSC Nastran 2020 output (SCHEMA 20200).

The model ​

  • Points are the GRID rows, in file order, in the basic coordinate system; see Coordinate systems. SPOINT/EPOINT scalar points are not points.
  • Cells. Each /NASTRAN/INPUT/ELEMENT/<card> table with a cell type gives up to two cell blocks, in card-name order:
CardCell typeQuadratic
CTRIA3, CTRIARtriangle
CTRIA6triangletriangle6
CQUAD4, CQUADR, CSHEARquad
CQUAD8quadquad8
CQUADquadquad9
CTETRAtetratetra10
CPYRAMpyramidpyramid13
CPENTAwedgewedge15
CHEXAhexahedronhexahedron20
CROD, CTUBE, CONROD, CBAR, CBEAM, CBUSH, CVISC, PLOTEL, CMASS1, CMASS2line
CONM1, CONM2vertex
  • Order. MSC stores the node list of a solid at its full quadratic width, zero-padded (a linear CHEXA has 20 slots, 12 of them 0). An element with every mid-side node is quadratic, one with none is linear, and one with only some (a CTRIA6 listing 6 4 60 0 65 64) is read as the linear type with a warning. A card's linear elements come first, then its quadratic ones.
  • Node order. CHEXA/CPENTA list their mid-side nodes bottom ring, vertical edges, top ring; meshio++ lists bottom, top, vertical. They are permuted on read (connectivity[k] = G[perm[k]], hexahedron20 0..11, 16..19, 12..15, wedge15 0..8, 12, 13, 14, 9, 10, 11), the same tables as the bulk Nastran reader. CTETRA, CPYRAM, CTRIA6, CQUAD8 need none. The tests build each quadratic card from its edge midpoints and check that every mid-node lies on its edge.
  • Springs and dampers (since v16.12.0): CELAS1, CELAS2, CDAMP1 and CDAMP2 are lines between their two GRIDs, or vertices when grounded (one of them 0).
  • Skipped cards. Scalar-point elements (CELAS3/4, CDAMP3/4, CMASS3/4, which attach to scalar points, an entity Mesh does not have) and any card with no cell type (CHBDYE, CBUSH1D...) are skipped with one warning listing them. An element that connects a SPOINT is skipped too; one that names a GRID the file does not define is an error.
  • Field data. The property and material values nastran:<CARD>_<id>:<MEMBER> of the INPUT/PROPERTY tables PSHELL (MID1, T, NSM), PROD (MID, A, J, C, NSM), PTUBE (MID, OD, T, NSM), PSOLID (MID) and the MAT1 material table (E, G, NU, RHO, A, TREF, GE), float64 (the MID members int64), one value each; the writer takes them back into those columns, and a property with no value keeps the placeholder (material 1, thickness, area and outer diameter 1). One MAT1 row is written for every material a property names and every nastran:MAT1_<id>:* given (since v17.4.0).
  • Point data. nastran:nid, the GRID ids, when they are not 1 to n; the writer numbers the GRIDs, the element cards and the nodal tables from it. A read with points_only=True drops it, like all point data, and an arrays= read keeps it only when "nastran:nid" is named (since v17.4.0).
  • Cell data. nastran:eid (the element id) and nastran:pid (the property id, -1 for a card without one, such as CONM2 or CONROD), integer, per block.
  • Regions. One cell region per property id, named after the property card that defines it (PSHELL_4, PSOLID_2, or PID_<n> when no card does; a flat table such as PSHELL wins over a grouped one such as PCOMP/IDENTITY for an id both define), with the property id as its tag and the highest cell dimension among its cells as its dimension.

Coordinate systems ​

Since v16.10.0 a GRID whose CP is not 0 is moved from its local system to the basic one, and every nodal vector result of a GRID whose CD is not 0 (the system its results are output in) is rotated to basic at that point. The systems are the CORD1R/CORD1C/CORD1S and CORD2R/CORD2C/CORD2S tables of /NASTRAN/INPUT/COORDINATE_SYSTEM. They are resolved in any order: a CORD1* through its three GRIDs (which may lie in another local system) and a CORD2* through its reference system. Cylindrical (r, θ, z) and spherical (r, θ, φ) coordinates, in degrees, are converted to the system's Cartesian axes first. A vector at a point of a cylindrical or spherical system is rotated by the local axes at that point, so the rotation differs from point to point.

What is rotated: every nodal table with X, Y, Z members (and its RX, RY, RZ rotations) and all of GRID_FORCE. Scalar tables, element results (in the element's own system) and complex tables stay as written. point_data["nastran:cp"] and ["nastran:cd"] still record the system ids when any is non-zero, and an info line says how many GRIDs moved. A system that cannot be resolved (a missing CORD* card, or a cycle) leaves its GRIDs and their results as written, with a warning naming the ids. The bulk-data nastran reader does not apply CP.

Results and steps ​

Every result table has an INDEX twin, /INDEX/NASTRAN/RESULT/<same path>, whose rows give the position and length of the table's rows for each domain. A domain is one row of /NASTRAN/RESULT/DOMAINS: a subcase, and within it a mode, a time step or a frequency. The domains any nodal or elemental INDEX references are the steps of the sequence engine, in DOMAINS order. time_step picks one (0 is the first, negative counts from the end, out of range is an error), read_metadata(...).time_values lists them, and read_sequence walks them. Only the chosen domain's rows are read, through HDF5 hyperslabs. A DOMAINS row no table uses (the eigenvalue summary's own domains, for instance) is not a step. A file with no results is one step with no field data.

Field dataMeaning
meshio:timethe domain's TIME_FREQ_EIGR, as written: the time of a transient run, the frequency of a frequency response, the eigenvalue of a mode (for a real mode that is λ = ω², not a frequency; for a buckling mode the load factor)
nastran:domainthe DOMAINS id
nastran:subcase, nastran:step, nastran:modethe domain's subcase, step and mode number
nastran:analysisthe domain's ANALYSIS code, as written
nastran:eigithe imaginary part of a complex eigenvalue

meshio:time is not converted to a frequency: the ANALYSIS codes are not documented publicly, and the one that looks like "modes" also carries buckling load factors in a SOL 105 file, so no conversion could be applied safely. Rigid-body modes all have eigenvalue 0, so a .pvd written from them repeats timestep="0.0"; write one file per step ('mode_{step}.vtu') or select them by nastran:mode.

A result table can cover only some domains: in a SOL 105 file DISPLACEMENT is in the static domain and EIGENVECTOR in the buckling ones. Each step has the arrays of the tables that have rows for it.

Nodal results ​

Each /NASTRAN/RESULT/NODAL/<T> table with at most one row per node and domain becomes point data. Members are grouped by name:

MembersPoint data
X, Y, Z<T> (n, 3)
RX, RY, RZ<T>_ROT (n, 3)
XR, YR, ZR / XI, YI, ZI (a <B>_CPLX table)<B>_real / <B>_imag (n, 3)
RXR, RYR, RZR / RXI, RYI, RZI<B>_ROT_real / <B>_ROT_imag
a lone VALUE (TEMPERATURE)<T> (n,)
any other float member M (KINETIC_ENERGY's KET1...)<T>:<M> (n,)

A point the table has no row for is NaN. MSC writes rows for SPOINTs into the same tables (a EIGENVECTOR of 40 GRIDs and 3 SPOINTs has 43 rows per mode); those rows are dropped. GRID_FORCE, which has one row per (node, element), is read as described under Several values per element.

Element results ​

Each /NASTRAN/RESULT/ELEMENTAL/<G>/<T> table (<G> is STRESS, STRAIN, ELEMENT_FORCE, ENERGY...) with at most one row per element and domain becomes cell data, one array per float member M, named <G>:<M> and shared by every table of the group: STRESS:X holds the X of the HEXA, PENTA and TETRA tables alike, STRESS:X1 the fibre-1 stress of the shell tables. A member that is an array (a solid's X(9): the centre and then each corner; a corner-output shell's (5); a beam's 11 stations) contributes its first entry to <G>:<M>, which is the centre for solids and CEN shells and end A for a beam; its other entries are read as described next. A cell whose card has no such member is NaN. Rows are joined to cells by EID (ID for the ENERGY tables).

Several values per element ​

Since v16.10.0 the values beyond the centre are read too. They are added beside <G>:<M>, which keeps the centre value, as (cells, columns) cell data, NaN-padded, with field_data["nastran:layout:<name>"] = [columns, 1]:

ArraySourceColumn
<G>:<M>@cornerthe corner entries of a solid's or corner-output shell's array memberthe GRID's position in the cell's connectivity
<G>:<M>@plythe per-ply rows of a _COMP tablePLY - 1
<G>:<M>@stationa BEAM table's 11 stations, and the SD rows of BARSthe station; stations not output stay NaN
GRID_FORCE:<M> (F1...M3)the element rows of GRID_FORCEthe GRID's position in the cell's connectivity

The GRID_FORCE rows that belong to no element (*TOTALS*, APP-LOAD, F-OF-SPC...) become point data GRID_FORCE:<label>:<M>, the label being the row's ELNAME without blanks and asterisks (GRID_FORCE:TOTALS:F1). A cell whose card has no such values is NaN in every column. A table with repeated rows of another kind (ENERGY's summary row with ID 100000000) is still skipped with a warning.

CONM2 elements are not matched by element results: they have none, and MSC accepts a CONM2 sharing its id with a structural element. Any other id shared by two cards gets a warning, and the results go to the first.

What is not read ​

  • Everything outside /NASTRAN/INPUT/NODE, /ELEMENT, /PROPERTY and /NASTRAN/RESULT/NODAL, /ELEMENTAL: loads, constraints, materials, the eigenvalue summary, optimization and aerodynamic results.
  • Other vendors' HDF5 schemas, refused as above until a file is in hand. OP2 files are read by nastran_op2, with the same mesh and member names; OP4 and punch files are not read.

Verification ​

The test suite reads six files MSC Nastran 2020 wrote (SOL 101, 103, 105, 107, 108 and 159, from the pyNastran repository, BSD 3-Clause, credited in tests/python/meshes/nastran_h5/README.md). Every translation and rotation vector of every domain — 38 tables, the SOL 103 eigenvectors included — equals pyNastran 1.4.1's own reading of the same file bit for bit, and a .vtu sequence converted from the SOL 103 file, read back by ParaView, carries the same eigenvectors. The two engines agree exactly on every array of every step of every file, and every solid has a positive volume. The corner, ply, station and grid point force values match pyNastran's on the same files. The coordinate systems are checked on a probe derived from a real file, with GRIDs in a tilted CORD2C, a CORD2S defined in it and a CORD1R through GRIDs of the CORD2C (tools/gen_nastran_cord_reference.py): points match pyNastran's to 1e-16 and displacements to 4e-16, compared with upstream pyNastran's rotation matrices, because 1.4.1 composes cylindrical and spherical ones the wrong way round.

Released under the MIT License.