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:
JobContext— frozen 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:
PreparationStrategyGenerates 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().
ExtractionStrategyExtracts data from simulation outputs. Built-in:
OdbExtractionStrategy— post-simulation ODB extraction (usesodbAccess, no CAE kernel needed).ModelPropertiesExtractionStrategy— pre-simulation INP extraction (uses CAE kernel /mdb).
JobWorkflowStrategyOrchestrates the full pipeline:
ModularWorkflowStrategy— preparation → pre-extraction → simulation → post-extraction.MonolithicWorkflowStrategy— single script handles everything; results returned as JSON on stdout.
Execution Environments¶
AbaqusRunner selects the correct Python interpreter
based on what the script needs:
Condition |
Command |
Use Case |
|---|---|---|
|
|
Any script (recommended) |
Needs CAE kernel ( |
|
Model generation, INP extraction |
Needs odbAccess only |
|
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
Example token counts: 1→5, 2→7, 4→9, 8→12, 16→16.
Parallelism limits:
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
_workerfunction (not a lambda or closure) is the entry point, ensuring Windowsspawncompatibility.
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 apreparationfield.workflow='monolithic'requires amonolithic_scriptfield.Unknown workflow values raise
ValueErrorimmediately.
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').