Adding a Solver¶
⚠️ Internal only. The solver contract API is not yet stabilised for external contributors. Solver interfaces, the case.yaml schema, and the MCP gateway dispatch protocol are evolving. This document describes the current internal pattern; do not build against it yet.
New solvers follow the same pattern as the existing OpenFOAM, XLB, and Chrono adapters. A solver is a pure Docker image plus its declarations — it is not an MCP server. The single MCP Gateway launches your container per-call; the solver just reads case.yaml, runs, and prints a JSON result. Each solver lives in the monorepo under solvers/<name>/ and ships as a Docker image.
Directory Structure¶
A solver lives under solvers/<name>/ in the arxynehq/arxyne repo:
solvers/<name>/
├── solver_runner.py / .sh # Entrypoint — runs inside the container
├── arxyne_<name>/ # (optional) solver-side package: case prep, helpers
│ └── case_templates/ # Bundled case configs per product
├── stl-files/ # Bundled geometry (BYOM — the solver owns its STLs)
├── kpi_spec.yaml # KPI output definitions + input bindings + gates
├── contracts/
│ ├── <assessment>.input.yaml # M.7 input contract
│ ├── <assessment>.output.yaml # S.9 output contract
│ └── <assessment>.failure.yaml # S.7b failure contract
├── Dockerfile # Solver container image
├── LICENSE # Solver's own licence (GPL/BSD/MIT as appropriate)
├── README.md
└── tests/ # Contract + kpi_spec validation tests
There is no per-solver mcp_server.py, no per-solver port, and no ARXYNE_MCP_<NAME>_URL. The gateway is the only MCP server.
Step-by-Step¶
1. Create the directory¶
2. Write kpi_spec.yaml¶
Declare which USD attributes the solver reads (inputs) and writes (outputs):
domain: thermal
reads:
- attr: "vehicleMass"
solver_key: "mass_kg"
- attr: "arxyne:solver:thermal_boundary"
solver_key: "boundary_conditions"
type: json
writes:
- attr: "currentMaxTemp"
solver_key: "max_temp"
- attr: "currentHeatRejection"
solver_key: "heat_rejection"
provenance:
solver: "OpenFOAM CHT"
method: "Conjugate heat transfer"
3. Write the entrypoint¶
The entrypoint runs inside the container. It reads /case/case.yaml, loads bundled geometry from /solver/stl-files/, runs the solve, and prints a JSON result to stdout prefixed with the @@ARXYNE_RESULT@@ sentinel so the gateway can isolate it from solver banner noise:
# solver_runner.py — runs inside the Docker container
import json, sys, yaml
def main():
case = yaml.safe_load(open("/case/case.yaml"))
# ... run the solve, compute KPIs ...
result = {
"status": "success",
"max_temp": 82.4,
"heat_rejection": 12_500.0,
"executor_id": "thermal",
# ... S.9-conformant fields ...
}
print("@@ARXYNE_RESULT@@")
print(json.dumps(result))
if __name__ == "__main__":
main()
On failure, print an S.7b record: {status, class, gate_result, message, captures} with class ∈ {configuration, runtime, convergence, data}.
4. Write the Dockerfile¶
The image bundles the solver, its entrypoint, geometry, and declarations under /solver/:
FROM openfoam/openfoam-default:2312
COPY solver_runner.py /solver/
COPY stl-files/ /solver/stl-files/
COPY kpi_spec.yaml /solver/
COPY contracts/ /solver/contracts/
ENTRYPOINT ["python", "/solver/solver_runner.py", "--case", "/case"]
5. Register in the platform¶
Add an entry to the _SOLVERS dict in arxyne_platform/solver.py. This is the only platform wiring needed — the gateway reads it to know which image to launch:
"thermal": {
"name": "OpenFOAM (CHT)",
"version": "v2312",
"domain": "thermal",
"kind": "physics",
"differentiable": False,
"origin": "internal",
"image": "arxynehq/solver-thermal:latest",
"entrypoint": ["python", "/solver/solver_runner.py", "--case", "/case"],
"gpu": False,
"container_type": "per-call",
},
Post-launch this becomes YAML/MCP auto-discovery (BYOS) — solvers advertise themselves and the gateway discovers them with no hardcoded dict. The transport stays the same single gateway.
6. Write the platform-side case prep (if needed)¶
If the domain needs platform-side case.yaml authoring, add a case_prep_<domain>.py in arxyne_platform/ that reads USD product attributes and writes case.yaml into the host case dir. The gateway mounts that dir into your container — no tarball, no bytes over the wire.
7. Add the assessment contracts¶
Drop the three contract YAMLs in solvers/thermal/contracts/. The gateway serves them to the platform at runtime via get_contracts(solver, assessment); the platform validates inputs/outputs against them.
Key Conventions¶
- Monorepo directory, pure Docker image. Each solver is independently buildable from
solvers/<name>/and ships as an image. - No per-solver MCP server. The single gateway launches your container per-call (sibling-container pattern — no docker-in-docker).
- Path-based case dir. The platform writes
case.yamlto a host case dir and the gateway mounts it into your container. No bytes-over-MCP, no tarballs. - Solver owns its geometry. STLs are bundled in the solver Docker image under
/solver/stl-files/. Geometry stays yours; the platform sendscase.yamlonly. - Result sentinel. Print
@@ARXYNE_RESULT@@before the JSON result so the gateway can isolate it from solver/CUDA banner output. - Contract trilogy. M.7 input + S.9 output + S.7b failure — repo-resident YAML, validated by the platform.
- Typed failures. Every error returns
{status, class, gate_result, message, captures}with class ∈{configuration, runtime, convergence, data}. - Provenance is mandatory. Every result carries
executor_id; the platform captures solver name, version, and method from the registry.
Reference Implementations¶
solvers/openfoam— CPU RANS (simplest)solvers/xlb— GPU LBM (GPU container, bundled STLs)solvers/chrono— vehicle dynamics (multi-manoeuvre)
See Also¶
- Solvers Reference — current registry and validated results
-
MCP Gateway Reference — gateway tool surface
-
Multi-solver stack guide — running everything together