3. 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.
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, notExtractStress.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 thanjohn_test_2024/. Dates belong in metadata, not paths. - Templates carry a
.tmplsuffix 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.
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.
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?