Skip to content

Nastran OP2 results (.op2) ​

The binary result file MSC Nastran and Simcenter Nastran (NX Nastran) write with PARAM,POST,-1 (or -2), and that Femap, Patran and most post-processors read. MSC runs that write HDF5 are read by nastran_h5; OP2 is the route for Simcenter/NX Nastran and for MSC decks without MDLPRM,HDF5. meshio++ reads it as a sequence, .op2 → .vtu / VTKHDF / any meshio++ format, with every subcase, mode, time or frequency as a step.

Format namenastran_op2
Extensions.op2
Read / Write✓ / ✓ (the model, the nodal tables and the element results in the reader's layouts: stresses, strains, forces, heat fluxes, energies and grid point forces, real, complex and random)
Extra dependenciesnone

Reading ​

python
import meshioplusplus

mesh = meshioplusplus.read("job.op2")                 # the first step
mode3 = meshioplusplus.read("job.op2", 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:VON_MISES"]     # solids' centre von Mises, NaN elsewhere
mesh.cell_data["STRESS:VON_MISES1"]    # shells' fibre-1 von Mises
mesh.cell_data["STRESS:X@corner"]      # (cells, nodes): solid corner values
mesh.cell_data["GRID_FORCE:F1"]        # (cells, nodes): grid point forces
mesh.field_data["nastran:subcase"], mesh.field_data["meshio:time"]

meshioplusplus.nastran_op2.time_values("job.op2")
bash
meshioplusplus convert job.op2 'mode_{step}.vtu'

Both engines read the whole format: the C++ core, and a Python reference reader the core falls back to. The writer is below.

Which files ​

An OP2 is a Fortran unformatted file of named tables. meshio++ reads MSC and NX/Simcenter files, 32-bit (4-byte words) and 64-bit (8-byte words, still with 4-byte record markers), in either byte order. The table names of 64-bit NX files, written four characters per word, are read as the 32-bit ones. A formatted (text) OP2 is refused. A file is recognised by its .op2 extension, or by content: the PARAM,POST,-1 header (a date and the tape code record) is sniffed in any word size and byte order.

The model ​

The mesh is built as the nastran_h5 reader builds it, from the same card table (detail/nastran_model.hpp):

  • Points are the GEOM1 GRIDs, in file order, in the basic coordinate system (since v16.10.0): a GRID with CP != 0 is moved from its local system, which the GEOM1 CORD1R/C/S and CORD2R/C/S records define, and point_data["nastran:cp"] (["nastran:cd"]) records the system ids. Each system the file defines is returned as field_data nastran:CORD2_<id>:TYPE, A, X, Y and Z (its type, its origin and its axes, all in basic coordinates), which a write uses to put the GRIDs back in their systems. The rules are those of nastran_h5. NX's 32-bit GRID records with double-precision coordinates are read as such. A mesh taken from a sibling deck keeps the deck reader's raw coordinates.
  • Cells. The GEOM2 element records with a cell type: CQUAD4, CQUADR, CSHEAR, CQUAD8, CQUAD, CTRIA3, CTRIAR, CTRIA6, CTETRA, CPYRAM, CPENTA, CHEXA (linear or quadratic by their mid-side nodes), CROD, CTUBE, CONROD, CBAR, CBEAM, CBUSH, CGAP, CVISC, PLOTEL as lines, CONM1 and CONM2 as vertices, and (since v16.12.0) the springs, dampers and masses CELAS1, CELAS2, CDAMP1, CDAMP2, CMASS1 and CMASS2 as lines between their two GRIDs or vertices when grounded (one of them 0), in card-name order; NX 2019 and later's CQUAD4/CTRIA3 records (keys 15401 and 15301) are read as those cards. A card whose record length fits more than one layout (MSC and NX CQUAD4s differ by a word) takes the first whose entries validate: ids increasing, corner nodes that are GRIDs. Records with no cell type (CELAS3/4, CDAMP3/4 and CMASS3/4, which join scalar points, CBUSH1D and CBEND, whose properties are nonlinear tables and curved-pipe sections, and the axisymmetric elements) are named in one warning, and a spring with a scalar point at either end is skipped.
  • Field data. The property and material values nastran:<CARD>_<id>:<MEMBER> of the PSHELL, PROD, PTUBE, PSOLID, PBAR, PELAS, PDAMP, PVISC, PGAP, PSHEAR, PMASS, MAT1, MAT2, MAT4, MAT5, MAT8 and MAT9 records, and of the composite PCOMP records: nastran:PCOMP_<id>:Z0, NSM, SB, TREF, GE, FT and SYM (1 for a symmetric layup), and PLY, the plies as (n, 4) rows of MID, T, THETA and SOUT (see Writing; since v17.4.0). A PBUSH gives nastran:PBUSH_<id>:K and B as (6,) arrays, GE as one value (the 18-word NX record) or six (the 23-word MSC record), and SA, ST, EA and ET as scalars. A PBEAM gives nastran:PBEAM_<id>:MID, NSEG (the segments; the stations are NSEG + 1) and CCF as scalars, and for its stations SO, XXB, A, I1, I2, I12, J and NSM as (n,) arrays, with the outline points C1, C2, D1 and D2 as (n, 2) rows of x and y. The library sections give nastran:PBARL_<id>:MID, GROUP and TYPE (the last two as uint8 text), DIM (the section's dimensions) and NSM, and a PBEAML the same with DIM as (stations, n) rows and SO, XXB and NSM per station; a PCOMPG gives the PCOMP scalars with LAMOPT and PLY as (n, 5) rows of GPLYID, MID, T, THETA and SOUT. The sections and global plies are read from 4-byte-word files only (a 64-bit file writes their texts four characters a word). A PMASS gives nastran:PMASS_<id>:M, a MAT4 K, CP, RHO, H, MU, HGEN, REFENTH, TCH, TDELTA and QLAT, a MAT5 K1...K6, CP, RHO and HGEN, and a MAT9 the upper triangle of its stiffness matrix G11, G12...G66, RHO, A1...A6, TREF and GE.
  • Element identity. Every cell block carries nastran:card, the GEOM2 record's element type number of its card (CROD 30, CONROD 16, CTUBE 37, CONM2 15, CONM1 14, CMASS1 10, CMASS2 11, CELAS1 6, CELAS2 7, CDAMP1 2, CDAMP2 3, CBAR 24, CBEAM 54, CBUSH 26, CVISC 39, PLOTEL 52, CSHEAR 31; the shells and solids by theirs). Lines that share a cell type and have no property id, such as CONROD, CELAS2 and CDAMP2, are told apart by it. A spring or damper also carries its components at each end, nastran:c1 and nastran:c2 (c2 is 0 at a vertex, which is grounded at its second end; every other element has 0). A CBAR carries its orientation vector nastran:x (X1 to X3, zeros with a G0), its G0 nastran:g0 (0 with a vector), its pins nastran:pin (PA and PB) and its end offsets nastran:w (W1A to W3A and W1B to W3B); these four arrays are present only when the file holds CBARs. Since v17.4.0. A CMASS2 carries its mass nastran:m, and a CONM1 its mass matrix nastran:conm1, (n, 21) rows of M11, M21, M22, M31...M66 (the record's order); a CONM1 in a coordinate system other than the basic one is read as given, with a warning.
  • Point data. nastran:nid, the GRID ids, when they are not 1 to n. 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 and nastran:pid (-1 for CONM2, CONROD, PLOTEL).
  • Regions. One cell region per property id, named <PTYPE>_<pid> after its EPT record (PSHELL, PSOLID, PCOMP, PCOMPG, PBAR, PBARL, PBEAM, PBEAML, PROD, PSHEAR, PTUBE, PBUSH...), PID_<pid> when none defines it. NX writes each PCOMP a second time as a PSHELL with material ids from 100000000: that twin is ignored, and a PCOMPG wins over a PCOMP of the same id, as pyNastran reads them.
  • Without geometry. A file with no GEOM1 GRIDs (written without the model, or with PARAM,POST,-2 defaults) takes its mesh from the input deck beside it: <stem>.bdf, .dat, .nas or .blk, read by the bulk Nastran reader, with nastran:eid added. The bulk reader does not follow INCLUDE cards. Without a deck (since v16.12.0) the points come from the basic grid point table: BGPDTS rows (output system and basic coordinates, in internal order) named by the EQEXINS table, or NX's newer BGPDT, whose rows carry the GRID id; scalar points are left out, and elements still come from GEOM2 when the file has it. Their output systems are unknown without GEOM1, so results in a non-basic CD stay in it, with a warning. With neither, the ReadError lists the paths it looked for. A fluid (acoustic) GRID's CD = -1 reads as the basic system. A GRID repeated in a second GEOM1 table (a restart's) with the same definition is kept once; a different one is an error.
  • PARAM,POST,-2 files carry no header: the first table's name opens them. They read as a POST,-1 file does, and are recognised without the .op2 extension by that name (a one-word 2, an eight-character upper-case name, a one-word -1).

Results and steps ​

A results table alternates a 146-word header (approach, table and element-type codes, subcase, mode or time, width) with the data it describes. Every distinct (subcase, analysis code, word 5 of the header: load set, mode number, time or frequency) of the tables below is a step, in the order the file first names it; grid point force and energy tables join a step the other tables made (their word 5 is 0 in a static subcase), and an energy table makes its own only when no table names its subcase and analysis. time_step picks one, read_metadata(...).time_values lists them and read_sequence walks them.

Field dataMeaning
meshio:timethe time (transient) or frequency, the eigenvalue of a mode (real modes: λ = ω²) or the post-buckling eigenvalue, 0 for a static subcase; as written, not converted
nastran:subcase, nastran:analysisthe subcase and the approach's analysis code (1 statics, 2 modes, 5 frequency, 6 transient, 8 post-buckling...)
nastran:modethe mode number (analysis 2, 8 and 9), else 0
nastran:eigia complex mode's imaginary eigenvalue (analysis 9), with meshio:time its real part, both from the CLAMA table (since v16.12.0)

Nodal results ​

Real SORT1 tables with 8 words per node (OUG*, BOUG*, OQG*, OQMG*, OPG*) become point data by their table code, with the nastran_h5 names: DISPLACEMENT, EIGENVECTOR, VELOCITY, ACCELERATION, SPC_FORCE, MPC_FORCE (the OQMG tables, which share the SPC forces' code) and APPLIED_LOAD, each (n, 3) with a _ROT twin; a thermal displacement table is TEMPERATURE (n,). A point with no row is NaN, and scalar-point rows are dropped. When two tables give the same point a value in the same step, the first one read keeps it. Vectors of a GRID with CD != 0 are rotated from that system to basic (since v16.10.0), except in the BOUG* tables, which Nastran already writes in basic.

Since v16.12.0:

  • Complex tables (frequency response, complex modes; 14 words per node) give <name>_real and <name>_imag (DISPLACEMENT_real, DISPLACEMENT_ROT_imag...), the nastran_h5 names. Magnitude and phase (format 3) are converted, the phase in degrees. Both parts are rotated to basic.
  • Random tables (OUGPSD, OUGRMS, OQGNO...: the table code's hundreds name the quantity) give <name>_PSD, _ATO, _RMS, _NO or _CRM. They are spectral quantities, not vectors, so they stay in the GRID's output system.
  • SORT2 tables (one GRID or element over every step, each row led by its time, frequency or mode) are pivoted: every row joins the step its first word names, so a SORT2 table and its SORT1 twin give the same arrays.

Element results ​

Stress (OES*) and strain (OSTR*) tables become cell data STRESS:<M> or STRAIN:<M>, the centre value per element (a beam's first station), NaN on cells without it:

Element type (code)Members
CROD (1), CONROD (10)A, MSA, T, MST
CTUBE (3)AS, MSA, TS, MST
CSHEAR (4)TMAX, TAVG, MS
CBAR (34)X1A...X4A, AX, MAXA, MINA, MST, X1B...X4B, MAXB, MINB, MSC
CQUAD4 (33), CTRIA3 (74), and the centre of CQUAD8 (64), CTRIAR (70), CTRIA6 (75), CQUADR (82), corner CQUAD4 (144)per fibre k = 1, 2: FD<k>, X<k>, Y<k>, TXY<k>, ANGLE<k>, MAJOR<k>, MINOR<k>, VON_MISES<k> (or MAX_SHEAR<k>)
CTETRA (39), CHEXA (67), CPENTA (68), CPYRAM (255)X, Y, Z, TXY, TYZ, TZX, PRINCIPAL_A, PRINCIPAL_B, PRINCIPAL_C (in the file's A, B, C order, not sorted), PRESSURE, VON_MISES (or OCT_SHEAR)
CBEAM (2)SD, XC, XD, XE, XF, MAX, MIN, MST, MSC
CBAR stations (100)SD, XC, XD, XE, XF, AX, MAX, MIN, MS
composite CQUAD4 (95), CQUAD8 (96), CTRIA3 (97), CTRIA6 (98), NX CQUADR (232), CTRIAR (233)per ply: X1, Y1, T1, L1, L2, ANGLE, MAJOR, MINOR, VON_MISES (or MAX_SHEAR)
CELAS1...CELAS4 (11-14)S
CBUSH (102)TX, TY, TZ, RX, RY, RZ
NX CHEXA (300), CPENTA (301), CTETRA (302), CPYRAM (303)corners only (no centre value): X, Y, Z, TXY, TYZ, TZX, VON_MISES as <M>@corner

Since v16.12.0 the other element tables are read too:

  • Complex stresses and strains (rods, springs, CSHEAR, CBAR, CBEAM, centroidal and corner shells, solids, composites, CBUSH, and NX's von Mises variants OESVM1/OSTRVM1) give <M>_real and <M>_imag for each complex member; the real ones (fibre distances, von Mises of the VM tables) keep their names.
  • Random stresses and strains (OESPSD, OESRMS...) have their own magnitude-only layouts: rods A, T; CSHEAR TMAX, TAVG; CBAR X1A...AX...X4B; CBEAM stations; shells FD<k>, X<k>, Y<k>, TXY<k> (and VON_MISES<k>); solids X...TZX (and VON_MISES); composites per ply; CBUSH. The names carry the random suffix (STRESS:X1_RMS). No file with random element tables was available: these layouts follow pyNastran's reader and are not checked against one.
  • Element forces (OEF*) become ELEMENT_FORCE:<M>, real, complex (_real, _imag) or random (suffixed): rods, CTUBE, CONROD and CVISC AF, TRQ; springs and dampers F; CBAR BM1A, BM2A, BM1B, BM2B, TS1, TS2, AF, TRQ; CBAR stations (100) SD, BM1, BM2, TS1, TS2, AF, TRQ per station; CSHEAR F41...F14, KF1, S12...KF4, S41; shells MX, MY, MXY, BMX, BMY, BMXY, TX, TY (centre, and @corner for corner output); CBUSH FX...MZ; CGAP FX, SFY, SFZ, U, V, W, SV, SW; CBEAM SD, BM1, BM2, TS1, TS2, AF, TTRQ, WTRQ per station. The complex CSHEAR layout is the forces' real then imaginary parts, then the kick forces' and shear flows' (pyNastran pairs them otherwise).
  • Heat fluxes (an OEF of a heat-transfer run) become HEAT_FLUX:<M>: line, shell and solid elements XGRAD, YGRAD, ZGRAD, XFLUX, YFLUX, ZFLUX; CHBDYE/G/P FAPPLIED, FREECONV, FORCECONV, FRAD, FTOTAL; CONV FREECONV, FREECONVK. CHBDY* elements have no cell, so their fluxes are not kept.
  • Energies: strain energy (ONRGY*) becomes ENERGY:ENERGY, ENERGY:PCT, ENERGY:DEN, and kinetic energy (OEKE*) KINETIC_ENERGY:<M> likewise; complex energy has ENERGY_real/_imag. The total row of each set is no element and is dropped.

The members common to nastran_h5 (X1, TXY1, X, TZX, A, TMAX...) have its names, so an OP2 and an HDF5 file of the same run give the same arrays; the derived values the HDF5 schema does not store have their own names. Whether the last plate and solid column is von Mises or the maximum (octahedral) shear, and whether a table holds stress or strain, come from the header's stress code. Elements share results by id; CONM2 elements, which have none, are left out.

Since v16.10.0 the values beyond the centre are read too, with the arrays and nastran:layout:<name> field data of nastran_h5: <name>@corner for the corners of solids (39, 67, 68, 255) and corner-output shells (64, 70, 75, 82, 144), <name>@ply for the composite plies (column PLY - 1) and <name>@station for CBEAM's 11 stations and CBAR's station entries (stations not output stay NaN). The grid point force tables (OGPFB*) give GRID_FORCE:F1...M3 per element node and GRID_FORCE:<label>:<M> point data for the rows of no element (*TOTALS*, APP-LOAD, F-OF-SPC..., named without blanks and asterisks: GRID_FORCE:TOTALS:F1), rotated from each GRID's CD to basic. MSC writes a different header word 5 in OGPFB1 than in the other tables of the subcase, so grid point forces join the step the subcase's other tables made instead of starting their own.

Not read, and named in one warning: complex grid point forces, heat-transfer temperature rates and loads, nonlinear stress tables (OESNLX*), energy loss (OEDE), every other element type, and every other table (contact and bolt tables...).

Writing ​

python
import meshioplusplus

meshioplusplus.write("model.op2", mesh)                            # one step
meshioplusplus.write("model.bin", mesh, file_format="nastran_op2")

with meshioplusplus.nastran_op2.SeriesWriter("run.op2") as w:     # one file, a block per step
    for time, mesh in steps:
        w.write(time, mesh)
meshioplusplus.write_sequence("run.op2", steps)                    # the same
bash
meshioplusplus convert result.vtu model.op2

The writer is the reader's inverse: an MSC-dialect file of 32-bit little-endian words and SORT1 tables, the one a Nastran post-processor reads with no flags. It writes this one dialect: 64-bit words, big-endian files and NX/Simcenter's interlaced table names and double-precision GRIDs are what Nastran builds and versions write for their own platforms, the reader accepts all of them and detects them from the content, and a mesh read from one is written back in the MSC dialect (whether NX/Simcenter post-processors accept that is a vendor check). The model is written the way the reader builds it:

  • Points are the GEOM1 GRIDs, numbered from 1 in point order unless point_data["nastran:nid"] gives their ids (positive and unique), in the basic system (CP = CD = 0); a 2-D point set gets z = 0. The elements, the nodal tables and a solid's corner GRIDs use those ids; GEOM1 holds the GRIDs by id, as Nastran writes it, so a file read back has its points in id order.
  • Cells are the GEOM2 element cards, one record per card in card-name order and each card's elements by id: triangle is CTRIA3, quad CQUAD4 (or CSHEAR for a cell whose nastran:card is 31, so one quad block can hold both), tetra CTETRA, pyramid CPYRAM, wedge CPENTA, hexahedron CHEXA, triangle6 CTRIA6, quad8 CQUAD8, quad9 CQUAD, the quadratic solids with their mid-side nodes in Nastran's order (the inverse of the permutation the reader applies to hexahedron20 and wedge15), a vertex CONM2 and a line CROD, CTUBE (when its property id belongs to a region named PTUBE_<pid>) or CONROD (when nastran:pid is zero or negative). With nastran:card (a mesh read from an OP2 has it) a line or vertex takes the card that number names: CROD, CONROD, CTUBE, CBAR, CONM2, CELAS1, CDAMP1, CELAS2, CDAMP2, CVISC PLOTEL (a plot element has two nodes and no property, so its nastran:pid is not written), CMASS1, CMASS2 or, on a vertex, CONM1, and a spring, damper or mass vertex (CELAS1, CDAMP1, CELAS2, CDAMP2, CMASS1, CMASS2) is grounded at its second end. A CMASS2 takes its mass from nastran:m, and a CONM1 its mass matrix from nastran:conm1 ((n, 21) rows in the record's order, in the basic system); both are required. A CELAS2 takes its stiffness from nastran:k, its damping coefficient from nastran:ge and its stress coefficient from nastran:s (zero when absent), and a CDAMP2 its damping from nastran:b, which the write needs. A CQUADR is written as a CQUAD4 and a CTRIAR as a CTRIA3, which have the same nodes. Element ids are nastran:eid, else 1, 2, ... over the written blocks; they must be positive and unique. A cell block of another type (a polygon...) is not written, with a warning naming it.
  • Properties. nastran:pid gives each cell's property id, else 1 for shells, 2 for solids and 3 for rods. One EPT card is written per id, by the cell family it belongs to: PSHELL, PSOLID, PROD, PTUBE, PBAR (for a CBAR), PELAS (for a CELAS1), PDAMP (for a CDAMP1), PSHEAR (for a CSHEAR, from nastran:PSHEAR_<id>:MID, T, NSM, F1 and F2: material 1, thickness 1 and zeros when absent) or PVISC (for a CVISC, from nastran:PVISC_<id>:CE and CR, 1 and 0 when absent), PMASS (for a CMASS1, from nastran:PMASS_<id>:M, 1 when absent); a shell whose id has nastran:PCOMP_<id>:PLY is a PCOMP instead of a PSHELL, and one with nastran:PCOMPG_<id>:PLY ((n, 5) rows of GPLYID, MID, T, THETA and SOUT, with the PCOMP scalars and LAMOPT) a PCOMPG; a CBAR whose id has nastran:PBARL_<id>:TYPE is on a PBARL (TYPE and GROUP as text, MID, DIM, NSM) and a CBEAM with nastran:PBEAML_<id>:TYPE on a PBEAML (the same with DIM as (stations, n) rows and SO, XXB, NSM per station; the dimensions must match the section type's count, 22 types), in place of the PBAR and PBEAM, and the materials of its plies are MAT1 cards, so that the reader names the regions PSHELL_<pid> and so on. A property id shared by two families is a WriteError. The shell, solid and rod cards are placeholders: a thickness, an area or an outer diameter of 1, and material 1, which one unit MAT1 (E = 1, nu = 0.3) defines, so the file loads and cross-references; they are not the model's, and only the id and the family carry over unless field_data gives the values (see below). A CBEAM goes out with the PBEAM of its id, from the nastran:PBEAM_<id>:* field data; a PBEAM with no field data gets one unit segment of constant section. Its stations go in the slots the file uses: the first in slot 0, the last in slot 10, and any between them in the slots after the first. A CGAP (nastran:card 19) goes out with the PGAP of its id, from the nastran:PGAP_<id>:* field data; its orientation is nastran:x or nastran:g0, as for a CBUSH, and its coordinate system (CID) is written as 0 and not read. A CBUSH goes out with the PBUSH of its id, from the nastran:PBUSH_<id>:* field data (K of 1, B of 0, GE of 0 and stress and strain coefficients of 1 by default), as the 23-word MSC record. Its orientation is nastran:x or nastran:g0, as for a CBAR; a bush oriented by its element system (flag -1) is read with a zero X vector. Cards the writer holds no cell type for (the scalar-point elements CELAS3/4, CDAMP3/4 and CMASS3/4, CBUSH1D and CBEND, the axisymmetric elements, and a CSHEAR number on a cell that is not a quad) are left out of the file, with one warning naming them, so a mesh read from an OP2 writes the cards above and nothing else of its model. A CBAR needs its orientation (nastran:x, or nastran:g0 for a G0), a CGAP the same, a CBEAM its nastran:x, or nastran:g0 for a G0 (flag 2, the grid at word 6; the reader gives a beam oriented by a local system a zero X vector, and pyNastran takes a beam's flag from word 10 and so reads a G0 only when PA is 2), a CELAS2 its nastran:k and a CDAMP2 its nastran:b, else the write is refused. A PBEAM needs one to ten segments (NSEG). A CBUSH needs its orientation (nastran:x or nastran:g0).
  • Coordinate systems. A GRID with a non-zero nastran:cp is written in its system's local coordinates. Each system a GRID names is written as its CORD2R (type 1), CORD2C (type 2) or CORD2S (type 3) record, from the nastran:CORD2_<id>:TYPE, A, X, Y and Z field data, with the points B on +z and C in the xz plane. A GRID whose system has no such field data is refused, and a system the file reads as CORD1 is written back as the equivalent CORD2 record. A GRID with a non-zero nastran:cd is written with that CD (its system also needs the nastran:CORD2_<id>:* field data), and the nodal vectors (displacements and the other point tables, real and complex, with their rotations) and the grid point forces at it are rotated from basic into that system, point by point for cylindrical and spherical ones, which the reader rotates back.

The results are the arrays the reader returns, under the same names:

  • Point data: DISPLACEMENT and EIGENVECTOR (OUGV1, table codes 1 and 7), VELOCITY and ACCELERATION (10, 11), TEMPERATURE (OUGV1, a thermal table), SPC_FORCE (OQG1), MPC_FORCE (OQMG1) and APPLIED_LOAD (OPG1), each (n, 3) with a _ROT twin for the rotations (zeros when absent) and TEMPERATURE (n,). A point whose components are all NaN is left out of the table; the reader gives it NaN back.
  • Cell data are the arrays the reader returns for the element tables, under the same names, written through the reader's own layout table inverted: for a card, the first of its element types whose arrays the mesh holds is written, with the layout (and word count) that holds the most of them. STRESS:<M> (OES1X1) and STRAIN:<M> (OSTR1X) are written for the rods CROD, CONROD (A, MSA, T, MST) and CTUBE (AS, MSA, TS, MST), the linear shells CQUAD4 and CTRIA3 (FD1...VON_MISES2, or MAX_SHEAR1 and MAX_SHEAR2 when the mesh has those and no von Mises), the solids CTETRA, CHEXA, CPENTA and CPYRAM (X...TZX, PRINCIPAL_A...C, PRESSURE, VON_MISES or OCT_SHEAR; quadratic solids write the same table, for their corner nodes), the quadratic shells CQUAD8 and CTRIA6, the bars CBAR and CBEAM, the bushes CBUSH, the shear panels CSHEAR, and the springs, dampers and viscous dampers CELAS1, CELAS2, CDAMP1, CDAMP2 and CVISC, whichever layout the reader decodes for that type. ELEMENT_FORCE:<M> (OEF1X, table code 4) is written for the same cards (AF and TRQ for a rod, MX...TY for a plate, FX...MZ for a bush, the station forces SD, BM1... of a bar or beam...). HEAT_FLUX:<M> (HOEF1, a heat-transfer table: header word 23 is 1) is written for the rods, shells and solids (XGRAD...ZFLUX), and ENERGY:ENERGY, PCT, DEN (ONRGY1, table code 18, whose header word 3 holds the record's total energy as a single-precision float) and KINETIC_ENERGY:<M> (OEKE1, 36) for any element. The values beyond an element's centre are written from the location arrays: <M>@corner (a solid's corner nodes and the corner output of CQUAD4 type 144, CQUAD8, CTRIA6; the centre value is repeated for a corner the mesh leaves NaN, so a read of the file gains the @corner arrays), <M>@ply (the plies of a composite shell, PCOMP) and <M>@station (the stations of a CBAR and CBEAM); an element type whose location arrays are what make it that type (the composite shells, the bar with stations) is written only when the mesh has them. A member the mesh lacks, and a NaN inside a written row, are written as 0; a cell with every member NaN is left out.
  • Property and material values. field_data["nastran:<CARD>_<id>:<MEMBER>"] (one value each, float64, the MID members int64) sets the values the writer put in its PSHELL (MID1, T, NSM), PROD (MID, A, J, C, NSM), PTUBE (MID, OD, T, NSM), PSOLID (MID), PBAR (MID, A, I1, I2, J, NSM, C1, C2, D1, D2, E1, E2, F1, F2, K1, K2, I12), PELAS (K, GE, S), PDAMP (B), PVISC (CE, CR), PGAP (U0, F0, KA, KB, KT, MU1, MU2, TMAX, MAR, TRMIN; defaults 0, 0, 1e8, 1e-6, 0, 0, 0, 0, 100 and 0.001), PSHEAR (MID, T, NSM, F1, F2), PCOMP (Z0, NSM, SB, FT, TREF, GE, SYM, and the plies as PLY), PMASS (M), MAT1 (E, G, NU, RHO, A, TREF, GE), MAT2 (G11, G12, G13, G22, G23, G33, RHO, A1, A2, A3, TREF, GE, ST, SC, SS, and the integer MCSID) MAT4 (K, CP, RHO, H, MU, HGEN, REFENTH, TCH, TDELTA, QLAT), MAT5 (K1...K6, CP, RHO, HGEN), MAT8 (E1, E2, NU12, G12, G1Z, G2Z, RHO, A1, A2, TREF, XT, XC, YT, YC, S, GE, F12, STRN) and MAT9 (G11, G12...G66, RHO, A1...A6, TREF, GE) records, and the reader returns them under the same names (the first record of an id wins). A property with no value keeps the old placeholder (material 1, thickness, area and outer diameter 1, a spring's stiffness and a damper's coefficient 1); one MAT1 is written for every material a property names and every nastran:MAT1_<id>:* given, unit modulus and Poisson's ratio 0.3 unless set (since v17.4.0). A material id with nastran:MAT2_<id>:* or nastran:MAT8_<id>:* field data is written as that card instead of a MAT1, so a ply of a PCOMP can be a MAT8 and a PSHELL material a MAT2; the members left out are zero, except a MAT2's G11, G22 and G33 (1) and a MAT8's E1, E2 (1) and NU12 (0.3). An id given as two of the cards is a WriteError.
  • Complex nodal tables. <name>_real and <name>_imag (and <name>_ROT_real, <name>_ROT_imag, all (points, 3)) of DISPLACEMENT, EIGENVECTOR, VELOCITY, ACCELERATION, SPC_FORCE, MPC_FORCE and APPLIED_LOAD are written as a complex table beside the real one: table code 1000 plus the real code, format code 2 (real and imaginary parts), 14 words a node. Set nastran:analysis to 5 (frequency) or 9 (complex modes) as the solution would. pyNastran reads them (since v17.4.0).
  • Complex element tables. <M>_real and <M>_imag cell data (and @corner, @ply, @station twins) of any of the groups above are written as a complex table: table code 1000 plus the real one, format code 2, 14 words a node or the reader's complex layout of the element type; members that have no imaginary part (the fibre distances) are shared with the real table. Set nastran:analysis to 5 (frequency) or 9 (complex modes). pyNastran reads them where it supports the layout.
  • Random tables. <M>_RMS, <M>_NO, <M>_PSD, <M>_CRM and <M>_ATO cell data of the stresses, strains and forces are written as a table of their own (OESRMS1, OESNO1, OESPSD1, OESCRM1, OESATO1, and the OSTR and OEF ones) with MSC's table codes (random bit 4 in the sort code, 100 times 5 for CRM, 6 PSD, 7 ATO, 8 RMS, 9 NO added to the quantity's code: 4805 is an RMS stress). The tables are SORT1 and hold magnitudes only, as the reader reads them. MSC writes PSD, CRM and ATO as SORT2 (one record per element, a row per frequency): pyNastran reads RMS and NO SORT1 tables and not the SORT1 PSD, CRM and ATO ones, so those are read by meshio++ (both engines) and by readers that accept either sort; the writer does not pivot to SORT2, which holds the same numbers.
  • Grid point forces. GRID_FORCE:F1...M3 cell data ((cells, nodes) per block, a column per node of the connectivity, NaN where a node has no force) is written as one OGPFB1 row per node of each element (table code 19, 10 words: the GRID, the element, its card name and the six components), and each GRID_FORCE:<label>:<M> point array (TOTALS, APP-LOAD, F-OF-SPC...) as a row of element 0 with the label as its text (*TOTALS* for TOTALS). The forces are in basic coordinates (every GRID has CD 0). A reader attaches grid point forces to a step another table of the subcase made, so write them beside a nodal or element table.
  • Not written, and named in one warning: every other array (SORT2 tables, complex grid point forces, the results of elements the writer holds no card for), field data other than nastran:* and meshio:time, regions, and nastran:cp and nastran:cd.

A step is nastran:subcase (default 1), nastran:analysis (default 1, or 6 when meshio:time is not 0) and nastran:mode (default 1) of the field data, and meshio:time (0 when absent). The reader's own rules are inverted: a statics, buckling or other subcase is told apart by its subcase alone, a mode (analyses 2, 8 and 9) by its mode number with the eigenvalue as its time, and a time or frequency step (5, 6, 10 and 12) by its time, stored in single precision. A series is the model once and a block per step in each result table, so the file is assembled when the writer closes. Every step must have the first step's points count, cell blocks and result arrays, and a subcase, analysis and mode or time of its own; a step that does not is a WriteError naming it, and {step} in the path writes one file per step instead. A step's time is the argument of write (or the pair's first item in write_sequence), not its meshio:time. One step is write's bytes.

Values are stored as float32 and ids as int32, so a read of a written file returns the values rounded to single precision. The bytes depend on the mesh only (the header's date is fixed): the C++ write_nastran_op2 and NastranOp2SeriesWriter and their Python twins write the same bytes, which the test suite compares, and a hand-framed oracle spells out. A repeated element id, a property id shared by shells, solids and rods, an array of the wrong size, a subcase or analysis that is not a positive code and a step that breaks the series rules are a WriteError.

The files were read back by pyNastran 1.3.4 (read_op2, and read_op2_geom with cross-referencing): the displacements and rotations of every step, the plate, solid and rod stress and strain, and the nodes, elements, properties and material, for the one-step, mode, transient and static files and for a mesh of every cell type. They were not opened in Femap or Patran.

Verification ​

The test suite reads eleven files from the pyNastran test models (BSD 3-Clause, credited in tests/python/meshes/nastran_op2/README.md): MSC and NX output, 32- and 64-bit, SOL 101 statics (among them static_solid_shell_bar, a solid, shell and bar model), modes, buckling, transient heat transfer, an NX 2412 file, and one with its geometry tables removed next to its deck. On every step of every file the nodal vectors, the centre stress and strain, and the corner, ply, station and grid point force values equal pyNastran 1.4.1's reading to float precision, and the two engines agree exactly. The nodal vectors and centre values also match on 40 further pyNastran models checked outside the suite (before the corner, ply, station and grid point force values were read). No file from an MSC or NX version newer than those samples, and no big-endian file Nastran wrote, has been read.

Since v16.12.0 three more pyNastran models are in the suite: freq_elements2 (frequency response, magnitude and phase, every element family's complex forces, stresses and strains, NX's von Mises variants), modes_complex_elements (complex modes) and test_vba (random: PSD, RMS and NO displacements, accelerations and SPC forces, and acoustic GRIDs). Their complex, random and SORT2 nodal values, and every step's element forces, heat fluxes and strain energies across the suite, equal pyNastran's to float precision (pyNastran keeps complex values in single precision). Left out of that comparison where pyNastran misreads: complex CSHEAR forces, CBAR station forces (it files them with CBAR 34's), complex CTRIA3 from OESVM1 and complex composite von Mises. Derived files check the rest: time_thermal_elements_sort2_only.op2 (the SORT2 tables alone read as their SORT1 twins), static_elements_bgpdt.op2 and sol401_tstep1_bgpdt.op2 (GEOM1 removed: the BGPDTS/EQEXINS and NX BGPDT points equal the GRIDs), and a header-less copy of static_solid_shell_bar.op2 for PARAM,POST,-2. Five more (cbush, cbush_2021, sdbush01, cc188b, rms_tri_oesrmx1) check CBUSH forces, stresses and strains (real and complex), CGAP forces, random element tables and complex strain energies the same way (pyNastran labels its four complex energy columns with three names: the reference names them itself); GRIDs whose output system is not basic are left out of the comparison, since pyNastran keeps their results in that system. No sample has complex SORT2 nodal tables or NX solids 300-303.

The coordinate systems are checked on static_solid_shell_bar_cord.op2, derived from static_solid_shell_bar.op2 by pointing every GRID's CP and CD at the file's own CORD2R, CORD2C and CORD2S systems (tools/gen_nastran_cord_reference.py): points match pyNastran's to 1e-12 and displacements to the file's single precision. They are 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.