4. Extracting model metadata and solution controls

Before a single contour plot can be rendered or a dashboard tile populated, the automation pipeline needs to know what is inside the model. A CAE input deck is a complete, self-describing record of the simulation: it declares the mesh, the materials, the boundary conditions, the load cases, and the solver controls. For a reporting workflow, this file is the single source of truth. Extracting structured metadata from it programmatically is what turns a folder of solver decks into a queryable dataset.

This section focuses on the extraction layer. The goal is a Python routine that reads a deck, parses the relevant keyword blocks, and returns a normalized dictionary or DataFrame row that downstream report generators can consume without re-reading the raw file.

Why Parse the Deck Instead of the Solver Output

Solver output files (for example, .odb, .rst, or .f06) contain results, but they are large, binary or semi-structured, and often require a licensed API to read. The input deck, by contrast, is plain text, version-controllable, and available before the solve even runs. Extracting metadata from the deck means the dashboard can be built and validated independently of solver availability, and it can flag modeling issues (missing material, unconstrained nodes) before compute time is spent.

The trade-off is that input decks vary in dialect. A Nastran bulk data file, an Abaqus .inp, and a CalculiX .inp share concepts but differ in keyword syntax. A robust extractor therefore separates lexing (splitting the file into keyword blocks) from interpretation (mapping each block to a normalized field).

Deck Structure and the Keyword-Block Model

Most decks follow a line-oriented grammar: a keyword line begins a block, and subsequent data lines belong to it until the next keyword. Continuation lines and comment characters differ by solver, so the lexer must be configurable. The diagram below shows how a raw deck is decomposed into blocks and then mapped to normalized metadata fields.

From Raw Input Deck to Normalized Metadata Raw Deck (text) BEGIN BULK GRID,1,0.,0.,0. CTETRA,10,1,1,2,3,4 MAT1,1,2.1E5,,0.3 PSOLID,1,1 SPC1,1,123456,5 FORCE,2,5,,100.,1.,0.,0. EIGRL,3,,,10 SOL 101 SUBCASE 1 LOAD=2 SPC=1 ENDDATA Lexer split keyword blocks handle continuation strip comments normalize case Parser map block to field resolve IDs (mat, prop, set refs) count entities Metadata mesh: nodes, elements materials: E, nu, rho BCs: SPC sets loads: case IDs solver: SOL, EIGRL subcases: load/SPC map → DataFrame row per model

Building the Extractor

A practical extractor is a small class with a solver-specific keyword map. The example below targets a Nastran-style bulk deck but the pattern generalizes. It reads the file once, groups lines by keyword, and exposes typed accessors.

import re
from collections import defaultdict

class DeckExtractor:
    def __init__(self, path, comment_char="$"):
        self.path = path
        self.comment_char = comment_char
        self.blocks = defaultdict(list)
        self._lex()

    def _lex(self):
        current = None
        with open(self.path) as fh:
            for raw in fh:
                line = raw.split(self.comment_char, 1)[0].rstrip()
                if not line.strip():
                    continue
                # continuation: leading '+' or blank first field
                if line[0] in "+ " and current:
                    self.blocks[current].append(line)
                    continue
                token = line.split(",")[0].strip().upper()
                current = token
                self.blocks[current].append(line)

    def mesh_stats(self):
        nodes = len(self.blocks.get("GRID", []))
        elems = sum(len(self.blocks.get(k, []))
                    for k in ("CTETRA", "CHEXA", "CQUAD4", "CTRIA3"))
        return {"nodes": nodes, "elements": elems}

    def materials(self):
        mats = {}
        for line in self.blocks.get("MAT1", []):
            f = [x.strip() for x in line.split(",")]
            mats[f[1]] = {"E": float(f[2]) if f[2] else None,
                          "nu": float(f[4]) if len(f) > 4 and f[4] else None}
        return mats

    def subcases(self):
        cases = {}
        for line in self.blocks.get("SUBCASE", []):
            cid = line.split()[-1]
            cases[cid] = {}
        # associate LOAD / SPC lines that follow each SUBCASE
        # (simplified: assumes ordered blocks)
        return cases

Two details matter for correctness. First, continuation handling: Nastran uses a leading + or a blank field to mark continuation, and ignoring this silently truncates long element or property records. Second, ID resolution: a PSOLID references a material ID, and a SUBCASE references load and SPC set IDs. Counting blocks is not enough; the extractor must resolve these references so the dashboard can answer "which material is on this property?" and "which load sets belong to subcase 1?"

Normalizing Across Solvers

Once extraction works for one dialect, the same normalized schema should be filled from others. The table below shows how equivalent concepts map. Keeping a single output schema means the dashboard code never branches on solver type.

Normalized field Nastran Abaqus CalculiX
nodesGRID*NODE*NODE
solid elementsCTETRA / CHEXA*ELEMENT, TYPE=C3D**ELEMENT, TYPE=C3D*
materialMAT1*MATERIAL + *ELASTIC*MATERIAL + *ELASTIC
propertyPSOLID / PSHELL*SOLID SECTION*SOLID SECTION
constraintsSPC1*BOUNDARY*BOUNDARY
loadsFORCE / PLOAD4*CLOAD / *DLOAD*CLOAD / *DLOAD
load caseSUBCASE*STEP*STEP
solver controlSOL, EIGRL*STEP, NLGEOM*STATIC / *FREQUENCY

Extracting Solution Controls

Solution controls determine what the solver actually computes, and they are frequently the source of mismatched expectations in reports. The extractor should capture at minimum: the analysis type (static, modal, buckling, transient), the number of requested modes or time steps, and any nonlinear or solver-selection flags. For a modal run, the EIGRL card's mode count and frequency range are the key parameters; for a static run, the subcase-to-load mapping is the key parameter.

Practical check: after extraction, assert that every SUBCASE resolves to at least one load set and one constraint set. A subcase with no load is a modeling error that will produce a zero-result report, and catching it at extraction time is far cheaper than debugging an empty contour plot.

Validation and Failure Symptoms

Extraction bugs are quiet. A missed continuation line reduces the element count without raising an error; a case-sensitivity mistake drops a keyword block entirely. Build in cheap assertions: compare the extracted node and element counts against the solver's own echo in the output file, and log any keyword that appears in the deck but is absent from the keyword map. When counts disagree, the usual causes are comment characters not matching the solver's convention, continuation lines being treated as new keywords, or fixed-format fields being split on the wrong delimiter.

The output of this stage is a tidy record — one row per model, with nested fields for materials, boundary conditions, load cases, and solver settings. That record is the input to every subsequent reporting step, so its schema should be fixed early and versioned alongside the extractor code.