Skip to content
John Hodge

← Blog

sysml2kit 0.4.0: multi-fidelity verification on a compute budget

A 32 by 32 phased-array terminal carries two requirements: at least 31 dBi of realized gain at broadside, and at least 30 dBi scanned to 60 degrees. A back-of-envelope aperture model says both pass. A tapered array-factor integration says one of them does not. Here is a verification run that starts with the cheap model and spends a ten-second compute budget only where the margin is thin:

$ sysml2kit verify terminal.json --policy escalate --budget-s 10 \
    --engine af-analytic=terminal_engines:analytic \
    --engine af-pattern=terminal_engines:pattern
analysis PhasedArrayTerminal::gainStudy [af-analytic]: 2 metrics
analysis PhasedArrayTerminal::gainStudy [af-pattern]: 2 metrics
PASS  REQ-GAIN  gain_dbi       >= 31.0  actual=35.0745  margin=+4.0745   [must] @analytic
PASS  REQ-GAIN  gain_dbi       >= 31.0  actual=32.192   margin=+1.192    [must] @pattern (from analytic)
PASS  REQ-SCAN  scan_gain_dbi  >= 30.0  actual=31.1611  margin=+1.16111  [must] @analytic
FAIL  REQ-SCAN  scan_gain_dbi  >= 30.0  actual=29.2553  margin=-0.744653 [must] @pattern (from analytic)
seconds by fidelity: analytic=0.00661s, pattern=3.47s
passed: False

The scanned-gain requirement fails by 0.7 dB, and the process exits nonzero, so a CI pipeline fails with it. The requirement, the architecture that satisfies it, the analysis that verifies it, and the verdict all live in one SysML v2 model. The tool that ran this is sysml2kit, a Python SDK I published this week. In a companion post I walked through the binding convention that makes a SysML v2 analysis executable, and closed by calling budget-driven fidelity allocation the next project. Release 0.4.0 ships a first, deterministic version of that loop. This post is the package tour that ends there. Every command output shown here was produced by the released 0.4.0 package.

What it is

sysml2kit is API-first Python tooling for building, querying, validating, and automating SysML v2 models. It is Apache-2.0, needs Python 3.11+, and installs from PyPI:

pip install sysml2kit            # build, write, query, validate, diff
pip install "sysml2kit[parse]"   # + read .sysml files (sysmlpy backend)
pip install "sysml2kit[mcp]"     # + MCP server for agents
pip install "sysml2kit[verify]"  # + YAML verification-binding configs

One caveat up front: the current release is 0.4.0 and the classifier says pre-alpha. The full loop works, from model construction through executed verification, but the API may still move between minor versions. Pin an exact version if you depend on it.

The timing is deliberate. SysML v1 was a graphical notation locked inside modeling tools, and programmatic access meant vendor plugins. SysML v2 changed the ground rules: the language has a textual notation alongside the graphical one, and the standard ships with an API. OMG approved SysML 2.0, KerML 1.0, and the Systems Modeling API and Services 1.0 for final adoption on July 21, 2025, with the formal specifications published in September 2025. A system model is now something ordinary software can read, write, and check, if the tooling exists. sysml2kit is my attempt at that tooling for Python.

Models as code

The object model is a documented subset of SysML v2 as pydantic classes: packages, parts, ports, attributes with units, requirements, analysis cases, and the satisfy/verify/derive/allocate relationships between them. A builder API assembles the terminal from the opening example in a few lines (abridged here; the pattern follows the package docs):

from sysml2kit import Model
from sysml2kit.model import builder

model = Model()
pkg = builder.pkg(model, "PhasedArrayTerminal")
terminal = builder.part(model, "terminal", owner=pkg)
aperture = builder.part(model, "aperture", owner=terminal)

req_scan = builder.req(
    model, "REQ-SCAN", "ScannedGain", owner=pkg,
    text="Realized gain scanned to 60 degrees shall be at least 30 dBi.",
)
builder.attr(model, "metricKey", "scan_gain_dbi", owner=req_scan)
builder.attr(model, "threshold", 30.0, owner=req_scan, unit="dBi")
builder.attr(model, "op", ">=", owner=req_scan)

builder.satisfy(model, source=aperture, target=req_scan, owner=pkg)
study = builder.analysis(model, "gainStudy", owner=pkg, subject=terminal)
builder.verify(model, source=study, target=req_scan, owner=pkg)

The metricKey, threshold, and op attributes are a convention the kit documents: a requirement that owns them is machine-checkable, and the extraction API turns it into a typed spec that downstream tools consume.

What comes out is standard notation. write_model(model) emits deterministic SysML v2 text:

requirement <'REQ-SCAN'> ScannedGain {
    doc /* Realized gain scanned to 60 degrees shall be at least 30 dBi. */
    attribute metricKey = "scan_gain_dbi";
    attribute threshold = 30.0 [dBi];
    attribute op = ">=";
    attribute severity = "must";
}
satisfy ScannedGain by terminal.aperture;

The same model also writes Systems Modeling API JSON, the serialization the standard REST API speaks, with elements sorted by qualified name so committed files diff cleanly. The vocabulary is pinned to the OMG SysML-v2-Release tag 2026-05, and the spec document in the repo lists exactly which elements are in the subset. Everything outside it survives a round trip as an opaque passthrough rather than being dropped. Text output is deterministic but not lossless for those elements; the JSON interchange is the lossless format. Reading .sysml text back is delegated to a pluggable backend (sysmlpy) instead of reimplementing the KerML grammar inside the kit.

Traceability you can query

Once requirements and architecture live in one model, the questions systems engineers maintain in spreadsheets become queries: which requirements are unsatisfied, which are unverified, what satisfies REQ-SCAN. The CLI prints the trace matrix:

$ sysml2kit show terminal.json --traceability
          terminal    aperture    beamformer  rfChain
REQ-GAIN  .           S           .           .
REQ-SCAN  .           S           A           .
(S=satisfy, A=allocate; '.'=no link)

Validation applies ten rules, S2K001 through S2K010, covering dangling references, duplicate requirement ids, sibling-name collisions, and malformed fidelity ladders. Diff works at the element level. Here is the diff after a design revision that adds a radome and relaxes the scan threshold, using stable ids so the comparison keys on names instead of random UUIDs:

$ sysml2kit diff stable_v1.json stable_v2.json
+ PhasedArrayTerminal::terminal::radome
~ PhasedArrayTerminal::ScannedGain  (text: '...at least 30 dBi.' -> '...at least 29 dBi.')
~ PhasedArrayTerminal::ScannedGain::threshold  (value: 30.0 [dBi] -> 29.0 [dBi])

That is a requirements change you can review in a pull request.

Verification that runs

This is the part I care most about. A verify link in most MBSE practice is a promise that someone will check the requirement. In sysml2kit it is executable: an analysis case carries a verificationBinding metadata that names an engine, and sysml2kit verify runs it.

An engine is a callable with a narrow contract: payload dict in, flat metrics mapping out. Packages register engines through the sysml2kit.engines entry-point group, and the CLI also accepts operator-supplied ones. The analytic rung from the opening example is the aperture formula plus a cosine-exponent scan loss:

def analytic(payload):
    area_wl2 = payload["nx"] * payload["ny"] * payload["spacing_wl"] ** 2
    g0 = 10 * math.log10(4 * math.pi * area_wl2)
    scan = math.radians(payload["scan_deg"])
    loss = 10 * payload["scan_exp"] * math.log10(math.cos(scan))
    return {"gain_dbi": g0, "scan_gain_dbi": g0 + loss}

The second rung integrates a Hamming-tapered array factor over the visible sphere, which takes about 3.5 seconds instead of microseconds and reads 2.9 dB lower at broadside and 1.9 dB lower at 60 degrees, because the taper the design actually uses for sidelobe control costs gain the cheap model ignores. Both rungs bind to the same analysis with declared fidelity labels and costs:

binding = builder.metadata_def(model, "verificationBinding", owner=pkg)
builder.metadata(
    model, study,
    {"engine": "af-analytic", "configRef": "study.yaml",
     "fidelity": "analytic", "costSeconds": 0.001},
    name="analyticRung", definition=binding,
)
builder.metadata(
    model, study,
    {"engine": "af-pattern", "configRef": "study.yaml",
     "fidelity": "pattern", "costSeconds": 4.0},
    name="patternRung", definition=binding,
)

The runner’s policy decides which rungs execute. cheapest runs only the lowest-cost rung, and on this model it reports both requirements passing. all runs every rung and reports the cross-rung spread as an error bar. escalate, shown at the top of the post, runs the cheap rungs first, ranks requirements by margin thinness (|margin| / |threshold|), and spends the remaining budget escalating the thinnest ones. REQ-SCAN’s analytic margin was 1.16 dB against a 30 dBi threshold, the thinnest in the model, so it got the 3.5-second pattern run, which is how the failure surfaced without paying the expensive analysis for every requirement on every run. Every run records measured seconds per fidelity rung, so the allocation claim is auditable.

Results can be written back into the model. Each verdict lands as metadata on its requirement, carrying status, actual value, margin, engine, fidelity, and timestamp, and each checked metric is recorded with a provenance source string; the broadside gain above came back as sysml2kit.verify PhasedArrayTerminal::gainStudy af-analytic==unknown fidelity=analytic 2026-08-22T00:24:06+00:00 (entry-point engines report their package version in place of unknown). Reruns replace prior results instead of accumulating.

The same loop works with real domain engines registered through entry points: with phased-array-systems and the sysml2kit-rf-library model library installed, one command verifies the packaged SATCOM terminal model through a full phased-array trade study, 62 metrics checked against five requirement thresholds in 17 seconds. The companion post walks through that run and the binding convention behind it in detail, so I will not repeat it here.

Nine tools for agents

The other half of the package is agent access. sysml2kit mcp serve exposes the model over the Model Context Protocol: model_show, model_validate, model_diff, model_export, model_diagram, library_load, requirements_trace, requirements_extract, and requirements_verify. An agent asking “which requirements lack verification evidence” calls a typed tool and gets a structured answer instead of parsing a model file as text, and requirements_verify runs the same policy-driven verification loop with entry-point engines.

I have written before about agents running electromagnetic design loops with AEDL, and about why structured provenance matters in those loops. This is the same thesis applied to the systems model: give the agent a typed interface to the engineering semantics, and keep the physics in engines whose inputs and outputs are recorded. AEDL connects through a deliberately dependency-free bridge that converts extracted requirement specs into its own bound form, so neither package imports the other.

Adjacent tools

sysml2kit is not the first Python code to touch SysML v2, and it leans on some of the earlier work. The official SysML-v2-API-Python-Client is a generated REST client covering projects, branches, commits, elements, and relationships, but it has not been updated since 2021, four years before the spec reached final adoption. sysml2py parses and constructs SysML v2 structures with a textX grammar, and its ANTLR-based fork sysmlpy is the parse backend sysml2kit delegates to. The OMG pilot implementation ships Eclipse editors and a Jupyter kernel, and Syside is a SysML v2 language server for editors. What I wanted and could not find was the layer above these: traceability queries, validation, diff, and executed verification against one model, which is the layer sysml2kit adds. It talks to the standard API too, with a thin HTTP client and a docker-compose harness that runs the pilot implementation server for live round-trip testing.

Limits

The element subset is a pragmatic slice of the language, documented in the repo’s spec file, and the full SysML v2 metamodel is far larger. Textual output has documented round-trip losses for a handful of constructs (the interchange JSON is the lossless path). Verification engines only cover analyses someone has bound to code; a verify link with no binding is still just a promise. And the project is pre-alpha at 0.4.0: I expect the API to move, and the right way to depend on it today is with a pinned version and low expectations of stability, on a project where you can afford both.

Try it

pip install "sysml2kit[parse,verify]==0.4.0"
sysml2kit show model.json --traceability

Code is at github.com/jman4162/sysml2kit, docs at jman4162.github.io/sysml2kit, and the RF model library at github.com/jman4162/sysml2kit-rf-library. If you work on requirements-driven design of physical systems and this loop matches a problem you have, I would like to hear what breaks. The background on how I ended up caring about executable system models is in an earlier post on model-based engineering for phased arrays.

All command outputs above were produced with sysml2kit 0.4.0 installed from PyPI while writing this post on 2026-08-21 (timestamps in output are UTC); external claims link to their sources, checked the same day.

sysml2kit is an independent project I build on my own time. The views are my own and do not represent any current or former employer.

Frequently asked questions

What is sysml2kit?

An open-source Python SDK (Apache-2.0, Python 3.11+) for SysML v2: build system models as typed Python objects, emit standard textual notation and Systems Modeling API JSON, run traceability queries, validate, diff, and execute verification analyses bound to registered engines. It also serves the model to AI agents over MCP.

How is it different from sysml2py or the official SysML v2 Python API client?

Those are building blocks at the language and transport layers: sysml2py and its sysmlpy fork parse and construct SysML v2 text, and the official generated Python client wraps the REST API but has not been updated since 2021. sysml2kit sits above both: it delegates parsing to sysmlpy and speaks the standard API, then adds the traceability, validation, diff, and verification layers on top.

Is sysml2kit ready for production use?

No. The current release is 0.4.0 with a pre-alpha classifier, and the README says to pin an exact version. The full loop works, but the API may still move between minor versions, and the element subset is a documented slice of the SysML v2 language rather than a complete implementation.

More in Modeling & simulation