3. Structuring a CAE automation repository

Structuring a CAE automation repository 学习卡片

A CAE automation repository is not a generic software project. It sits between two very different worlds: the solver-centric world of decks, include files, and binary result databases, and the software world of version control, packaging, and continuous integration. A layout that works for a web service — a flat src/ tree plus a tests/ folder — will collapse quickly once you add Nastran bulk data files, Abaqus keyword decks, Python post-processing scripts, and per-project YAML configuration. The goal of this chapter is to define a directory structure that keeps those artifacts separated by role and lifecycle, so that Git diffs stay readable, CI pipelines stay fast, and engineers can find the right file without guessing.

Separate by role, not by file extension

The most common mistake is organizing by file type: *.py in one folder, *.inp in another, *.yaml in a third. This breaks down because a Python file can be a driver, a post-processor, or a test fixture, and each has a different change frequency and review requirement. A more durable principle is to group by role in the automation pipeline:

  • Executable logic — drivers, orchestrators, and utilities that run on every job.
  • Inputs — solver decks, templates, and parameter files that define a specific analysis.
  • Outputs and derived data — results, logs, and extracted metrics, which should almost never be committed.
  • Configuration — environment-specific settings, solver paths, and credentials references.
  • Tests and fixtures — small, deterministic cases that validate the pipeline itself.

This separation maps directly onto how CI behaves. Executable logic changes trigger full test runs; template changes trigger a narrower validation; configuration changes may require a manual approval gate. If everything lives in one folder, every commit looks equally risky and you lose the ability to reason about impact.

A reference layout

The following structure has proven workable for teams running Nastran, Abaqus, and OpenFOAM automation from a single repository. It is a starting point, not a mandate — the important part is the reasoning behind each boundary.

cae-automation/
├── src/
│   ├── drivers/            # job submission, solver invocation, queue adapters
│   ├── preprocess/         # deck generation, parameter substitution, meshing hooks
│   ├── postprocess/        # result extraction, plotting, report generation
│   └── common/             # shared I/O, logging, unit conversion, error types
├── templates/
│   ├── nastran/            # .bdf skeletons with ${PLACEHOLDER} tokens
│   ├── abaqus/             # .inp skeletons
│   └── openfoam/           # case dictionaries
├── decks/
│   ├── wing_static/        # one directory per analysis case
│   │   ├── case.yaml       # parameters, solver, boundary conditions
│   │   └── mesh/           # committed mesh or a fetch script
│   └── bracket_modal/
├── config/
│   ├── default.yaml        # baseline settings
│   ├── ci.yaml             # overrides for CI runners
│   └── local.example.yaml  # template for developer machines
├── tests/
│   ├── unit/               # pure Python, no solver required
│   └── integration/        # small decks that run in seconds
├── tools/                  # one-off migration or inspection scripts
├── docs/                   # architecture notes, onboarding, runbooks
├── .gitignore
├── .gitattributes
└── pyproject.toml

Two boundaries deserve emphasis. First, templates/ and decks/ are deliberately separate. A template is a reusable skeleton with placeholders; a deck is a concrete, parameterized case. Mixing them means every new analysis either copies a template and drifts, or edits a shared file and breaks other cases. Second, config/ is separate from src/. Configuration changes far more often than code and often differs between a developer laptop and a CI runner. Keeping it isolated lets you use .gitignore to exclude local.yaml while committing default.yaml and ci.yaml.

CAE Automation Repository — Role Boundaries and Data Flow repository root Inputs templates/ decks/ config/ low churn · reviewed committed Executable Logic src/drivers/ src/preprocess/ src/postprocess/ high churn · tested committed Outputs / Derived results/ logs/ reports/ ephemeral · ignored never committed Tests tests/unit/ tests/integration/ deterministic committed CI Pipeline Stages lint + unit tests template render integration solve post-process check report artifact Lifecycle rules Inputs: commit, review on change Logic: commit, full test on change Outputs: gitignore, archive externally

Naming conventions that survive scale

Directory structure alone is not enough; file naming determines whether a diff is reviewable. Adopt a small set of rules and enforce them in CI with a linter:

  • Lowercase with underscores for Python modules and directories: extract_stress.py, not ExtractStress.py. This avoids case-sensitivity surprises between Linux CI runners and Windows developer machines.
  • Case directories describe the physical configuration, not the date or engineer: wing_static_m3/ rather than john_test_2024/. Dates belong in metadata, not paths.
  • Templates carry a .tmpl suffix before the solver extension: static_analysis.bdf.tmpl. This makes it obvious that the file is not directly solvable and prevents accidental submission.
  • Configuration files are named by environment: default.yaml, ci.yaml, local.yaml. The loader merges them in that order, with later files overriding earlier keys.

What must never be committed

CAE repositories accumulate large binary artifacts faster than almost any other domain. A single Nastran .f06 or Abaqus .odb can exceed hundreds of megabytes, and once committed, it bloats every clone forever. A working .gitignore is therefore a first-class deliverable, not an afterthought:

# Solver outputs
*.f06
*.op2
*.odb
*.sim
*.log

# Post-processing artifacts
results/
reports/
*.png
*.pdf

# Local configuration and credentials
config/local.yaml
.env

# Python
__pycache__/
*.pyc
.venv/

For results that genuinely need to be retained — qualification evidence, for example — push them to an artifact store (S3, Artifactory, or a CI artifact bucket) and commit only a small manifest that records the run ID, solver version, and checksum. This keeps the repository cloneable in seconds while preserving traceability.

Configuration layering in practice

The config/ directory deserves a concrete example because it is where most teams introduce accidental coupling. A three-layer merge works well:

# config/default.yaml
solver:
  name: nastran
  executable: /opt/msc/nastran/bin/nastran
  memory: 8GB
postprocess:
  metrics: [max_stress, mass, first_mode]
  output_format: csv
# config/ci.yaml
solver:
  executable: /usr/local/bin/nastran
  memory: 4GB
postprocess:
  output_format: json

The loader reads default.yaml first, then overlays ci.yaml when the environment variable CAE_ENV=ci is set. A developer running locally gets the default solver path; a CI runner gets the containerized path and a smaller memory ceiling. No code changes, no branching logic scattered through the drivers. The key discipline is that default.yaml must always be runnable on its own — if it requires a local override to work, the baseline is wrong.

Practical check: after cloning the repository on a clean machine, running python -m src.drivers.run --case decks/wing_static --dry-run should resolve all paths and print the merged configuration without invoking the solver. If it fails, the layout has a hidden dependency on a developer's local state.

Where tests and fixtures belong

Integration tests for CAE automation are unusual because they may invoke a real solver. Keep them small and fast: a 200-element cantilever that solves in under ten seconds is far more valuable than a full wing model that takes an hour. Place the deck under tests/integration/fixtures/ and assert on extracted scalar metrics with a tolerance, not on binary result files. Unit tests under tests/unit/ should never touch a solver — they test parameter substitution, path resolution, and unit conversion in isolation. This split lets CI run unit tests on every push and integration tests on a nightly schedule or a labeled branch.

Common failure: committing a results/ directory "just for reference" during a debugging session. Once merged, removing it requires a history rewrite. Add results/ to .gitignore before the first commit, and use git status in the CI lint stage to fail any build that stages a file matching the ignore patterns.

Summary of the layout contract

Directory Contents Change frequency Committed?
src/ Drivers, pre/post-processing logic High Yes
templates/ Parameterized solver deck skeletons Low Yes
decks/ Concrete analysis cases with case.yaml Medium Yes
config/ Layered environment settings Medium Yes (except local.yaml)
tests/ Unit and integration validation Medium Yes
results/, logs/ Solver output and derived data Every run No — ignored

The structure above is not the only valid one, but it encodes a clear contract: inputs are reviewed, logic is tested, outputs are ephemeral, and configuration is layered. Once that contract is in place, CI stages map naturally onto directories, and a new engineer can locate any artifact by asking a single question — is this something we run, something we feed in, or something we produce?