Architecture

This page describes the design rationale and internals of ABQflow (v0.5.0).

Design Principles

Data + Service = Strategy

The core insight of v0.3 is splitting the old AbaqusCalculation God-object into two narrow contracts:

  • JobContextfrozen data: paths, job name, CPU count. Strategies see data, not implementation.

  • AbaqusRunner — a service with three public methods: run_solver(), run_hook(), and the internal _base_command().

Strategies depend only on (ctx, runner, logger) — not on AbaqusCalculation private methods. This eliminates the circular import that required TYPE_CHECKING hacks, makes strategies independently testable (mock the runner), and gives each layer a clear responsibility.

┌──────────────────────────────────────────────────┐
│ User Layer                                        │
│   JobSpec (dataclass, validate + deep-copy)       │
│   BatchSpec = list[JobSpec]                       │
└────────────────┬─────────────────────────────────┘
                 │ StrategyRegistry.build(spec)
┌────────────────▼─────────────────────────────────┐
│ Orchestration Layer  BatchAbaqusProcessor          │
│   plan()      — conflict detection (no side fx)   │
│   prepare()   — apply decisions, build calcs      │
│   run_batch() — ProcessPoolExecutor + fault-tol   │
│   ResourcePlanner — CPU / license constraints     │
└────────────────┬─────────────────────────────────┘
                 │ JobContext (frozen) + AbaqusRunner
┌────────────────▼─────────────────────────────────┐
│ Execution Layer                                    │
│   JobContext   — paths, name, resources            │
│   AbaqusRunner — run_solver / run_hook             │
│   Strategy     — depends on (ctx, runner, logger)  │
└──────────────────────────────────────────────────┘

Strategy Pattern

Workflows are composed from three strategy types:

PreparationStrategy

Generates an INP file. Built-in implementations:

  • InpModifyStrategy — replace {{placeholders}} in a base INP file. Validates coverage at prepare-time; missing parameters produce a clear error, not a silently broken input file.

  • ModelGenerationStrategy — run a Python script (with CAE kernel) that builds the model and exports an INP.

  • Custom — register via register_preparation().

ExtractionStrategy

Extracts data from simulation outputs. Built-in:

JobWorkflowStrategy

Orchestrates the full pipeline:

Execution Environments

AbaqusRunner selects the correct Python interpreter based on what the script needs:

Condition

Command

Use Case

abqpy installed

python script.py

Any script (recommended)

Needs CAE kernel (mdb)

abaqus cae noGUI=script.py --

Model generation, INP extraction

Needs odbAccess only

abaqus python script.py

ODB post-processing

The -- separator after noGUI= prevents Abaqus from consuming custom arguments.

Resource Planning

The framework automatically caps parallelism to avoid oversubscribing Abaqus license tokens, since a job that cannot obtain a license will simply fail to start. CPU cores are not hard-capped — small jobs rarely saturate a full core, so requesting more parallel jobs than physical cores support (CPU oversubscription) is allowed, but it is flagged with a warning so the allocation stays visible.

Abaqus license token formula (official): a job using n CPU cores consumes

\[T(n) = \lceil 5 \cdot n^{0.422} \rceil\]

Example token counts: 1→5, 2→7, 4→9, 8→12, 16→16.

Parallelism limits:

\[\begin{split}P_{cpu} &= \lfloor (C - R) / c \rfloor \\ P_{license} &= \lfloor L / T(c) \rfloor \\ P_{actual} &= \max(1, \min(P_{req}, P_{license}))\end{split}\]

where C = physical cores, R = reserved cores (default 1), c = cores per job, and L = available tokens. \(P_{cpu}\) is computed only to decide whether to emit the CPU-oversubscription warning — it no longer bounds \(P_{actual}\).

Use plan_parallelism() to compute this directly.

Fault Tolerance

run_batch uses concurrent.futures.ProcessPoolExecutor with these guarantees:

  • Single-job isolation: an exception in one worker returns as an error JobOutcome — it does not kill the batch.

  • Clean process lifecycle: the executor context-manager guarantees worker cleanup on completion or error.

  • Pickle-safe workers: the top-level _worker function (not a lambda or closure) is the entry point, ensuring Windows spawn compatibility.

JSON Protocol

Hook scripts and monolithic scripts communicate results via stdout. The framework uses a sentinel-marker approach to reliably extract JSON even when Abaqus prints banner text or warnings to stdout.

import json, sys

results = {"max_stress": 4525.3, "status": "COMPLETED"}
sys.__stdout__.write("===ABQ_RESULT_BEGIN===\n")
sys.__stdout__.write(json.dumps(results) + "\n")
sys.__stdout__.write("===ABQ_RESULT_END===\n")

When sentinel markers are absent, the framework falls back to scanning from the end of stdout for the last complete JSON object (Abaqus banner precedes script output, so the last { is most likely the result).

ABQflow.hookkit (staged into the job’s working directory automatically) implements this protocol for hook scripts so authors never write sentinel markers or argparse plumbing by hand. It is single-file and stdlib-only — never imports ABQflow, odbAccess, abaqus, or numpy — so it runs unmodified under the Abaqus Python interpreter (Py2.7 or Py3). It also adds a field-output mode (hookkit.field()) that spills large result sets (>10k rows or >1MB) to a CSV sidecar instead of inlining them in the JSON payload, keeping stdout small for bulky field quantities.

Configuration Validation

JobSpec validates at construction time:

  • workflow='modular' requires a preparation field.

  • workflow='monolithic' requires a monolithic_script field.

  • Unknown workflow values raise ValueError immediately.

This means misconfiguration surfaces before any Abaqus process is launched — no silent KeyError at job initialization.

Migration from v0.2

v0.3 introduced breaking changes to fix structural defects (see the design document for the full analysis).

Dict config → JobSpec:

Old:

jobs = [{'job_name': 'x', 'type': 'inp_based', 'base_inp_path': '...', 'params': {...}}]

New (compatible — from_dict bridge):

spec = JobSpec.from_dict({'job_name': 'x', 'type': 'inp_based', 'base_inp_path': '...', 'params': {...}})

New (native):

spec = JobSpec(job_name='x', preparation=PreparationSpec(kind='inp_based', source_path='...', params={...}))

Batch result format:

Old: run_batch() returned list[dict] or dict[str, dict]. New: returns list[JobOutcome]. Use outcomes_to_list() or outcomes_to_dict() for the old format.

Strategy signatures:

Custom strategies that subclasses PreparationStrategy / ExtractionStrategy / JobWorkflowStrategy must change their method signatures from (self, context: AbaqusCalculation) to (self, ctx: JobContext, runner: AbaqusRunner, logger: Logger).

Constructor side-effects:

BatchAbaqusProcessor.__init__ no longer deletes directories or prompts for input. Call plan() / prepare() explicitly, or let run_batch() auto-call them. The default duplicate_mode is now 'fail' (was 'interactive').