Skip to content
John Hodge

← Blog

A SysML v2 model that runs its own verification

This is the output of one command, run on 2026-08-21 against a SysML v2 model file:

analysis SatcomTerminalPAS::pasStudy [phased-array-systems]: 62 metrics
PASS  REQ-MARGIN  link_margin_db  >= 3.0    actual=22.14  margin=+19.14  [must]
PASS  REQ-EIRP    eirp_dbw        >= 40.0   actual=46.04  margin=+6.04   [must]
PASS  REQ-SLL     sll_db          <= -20.0  actual=-25.42 margin=+5.42   [must]
PASS  REQ-POWER   prime_power_w   <= 450.0  actual=368.64 margin=+81.36  [must]
PASS  REQ-COST    total_cost_usd  <= 60000  actual=25600  margin=+34400  [must]
passed: True

The command is sysml2kit verify. It read a model of a 28 GHz satcom terminal, found an analysis case bound to the phased-array-systems engine, executed a 16 by 16 Taylor-tapered array study, and checked five requirements against the 62 metrics that came back. Every threshold, every verify link, and every satisfy relationship lives in the model. The engine knows nothing about SysML. The kit knows nothing about antennas.

Last month I mapped the stack I am building for agent-driven RF design and named its biggest gap: no agent yet takes a requirement and decides to move between layers, or allocates simulation fidelity against a budget. This post is about the substrate half of that gap. Requirements now carry machine-checkable thresholds, analyses now execute, and verdicts now flow back into the model with provenance. The allocation loop where something decides which fidelity to buy remains future work, and I will say more about that at the end.

The missing layer was systems engineering

The stack post described nine repositories that go from requirements to photolithography masks, and a reader could fairly ask where the systems engineering lived. The honest answer was: in flat YAML files and two incompatible requirement dialects. My trade-study tool spelled a requirement as a metric key, an operator, and a value. My benchmark harness spelled it as a metric and a single bound. Nothing modeled parts, ports, allocations, or the question a review board actually asks, which is “show me the requirement, the analysis that verified it, and the number.”

SysML v2 is the standards answer to that question, and it became practical for tool builders in a way v1 never was: the language has a normative textual syntax and a REST API specification, both formal OMG standards since September 2025. So I built sysml2kit, an Apache-2.0 Python toolkit, and sysml2kit-rf-library, the RF vocabulary as model content. The kit stays domain-general. Part definitions like PhasedArrayAntenna and requirement definitions like EirpRequirement are SysML packages in the library, loadable by name, and the two dialects I mentioned are now both generated from one model through small adapters that shipped in phased-array-systems 0.13.0 and the aedl benchmark harness this month.

How an analysis becomes executable

The piece I could not find anywhere is a convention for binding a SysML v2 analysis case to code that runs. The specification defines analysis cases structurally and explicitly leaves evaluation strategy to tools. I surveyed what exists as of August 2026: the official API cookbook has no analysis recipe and last saw a commit in March 2025, Sensmetry’s commercial Automator evaluates expressions inside the model rather than dispatching external tools, pyMBE has been dormant since October 2024, and the closest academic work (PySysML2 from a 2024 Springer paper, and arXiv:2606.29006 from June) published no runnable tooling. If a portable convention exists, I could not find it, and I would genuinely like to be corrected.

So here is mine. An analysis case carries a metadata annotation:

analysis pasStudy : LinkBudgetAnalysis {
    subject terminal;
    objective { doc /* Evaluate the terminal study with phased-array-systems. */ }
}
metadata verificationBinding about pasStudy {
    engine = "phased-array-systems";
    configRef = "satcom_terminal_pas.yaml";
}

Two design decisions matter more than the syntax. First, engine is a name, and names resolve against a registry populated from the sysml2kit.engines entry-point group plus whatever the caller registers explicitly. A model file can never name an importable code path, because models are data that agents and strangers will hand you, and data should not get to choose what executes. Installing phased-array-systems 0.13.0 registers its engine; uninstalling it makes the binding fail loudly with the list of engines that are available. Second, the config payload lives in a separate YAML file next to the model, containment-checked so it cannot reference paths outside the model’s directory. The runner never interprets the payload. Each engine owns its own schema, which is what let the phased-array engine reuse its existing study-config format unchanged.

Block diagram of the verification loop. A SysML v2 model box on the left holds requirements with metricKey thresholds and a verificationBinding annotation. An arrow labeled engine by name leads to an engine registry box that resolves names from the sysml2kit.engines entry-point group. The registry passes a resolved callable to a run_study box representing phased-array-systems 0.13.0, which also receives a payload dict from a committed YAML study payload file stored next to the model. run_study emits 62 flat metrics to a verdicts box that computes pass, fail, or unknown with margins per requirement. An apply_results arrow leads down to a write-back box, and a final arrow labeled attributes and metadata, with provenance returns from write-back to the SysML v2 model, closing the loop.

The verification loop. Every box is released code and every arrow is a call verified in source; the loop closes when verdicts are written back into the model that requested them.

The runner does the obvious thing from there. It executes each bound analysis (engine exceptions are captured per analysis, never raised), looks up which requirements each analysis verifies through the model’s verify links, compares each requirement’s threshold against the returned metric, and reports pass, fail, or unknown with a margin. Unknown means the metric was absent or was not a number, and unknown on a must-severity requirement fails the run, because a check that silently checked nothing is worse than a failure. With --write-back, every checked metric is recorded as an attribute on its analysis with a provenance string naming the engine, its version, and the timestamp, and every requirement gains a verdict annotation. Running it twice replaces the results instead of piling them up.

What is real and what is not

A verification tool that overstates its own fidelity would be a bad joke, so this section is the part I most want to get right. sysml2kit implements a pragmatic profile of about 20 element kinds, pinned to the 2026-05 spec release, with an opaque passthrough that preserves any element it does not model. The three representations do not carry equal information, and the differences are measured, tested, and documented rather than smoothed over:

representationcarries everything in the profile?the part that is not
interchange JSONyes, this is the lossless formnothing known; round trip is property-tested
textual notation (.sysml)names, docs, typing, multiplicity, values with units, satisfy linksverify, derive, and allocate edges vanish in parsing, an upstream defect described below
OMG pilot serverelement kinds, names, requirement textownership, verify and derive kinds, multiplicity; the server also mints its own element ids

Two findings behind that table are worth reporting on their own, because each cost me a day and would cost you one too.

The parser I build on, sysmlpy 0.36.2, has a public loader that destroys part of its own parse tree: the ANTLR visitor captures satisfy statements, docs, and ports inside part bodies, and the wrapper-object layer then rebuilds usage bodies lossily and drops them. sysml2kit works around this by walking the raw visitor output directly, which recovered short names, cross-package typing, attribute values with units, and satisfy traceability from text. Two constructs never reach the visitor output at all, so no downstream workaround exists: dependency statements and the endpoints of allocate X to Y. I filed both upstream and then submitted fixes (#6, #7); the dependency fix also repairs three of that project’s own conformance-suite failures. Until they merge, my test suite pins the losses, so an upstream release that fixes them will show up as a failing test telling me to wire the fidelity through.

The second finding came from running a real Systems Modeling API server instead of mocking one. A docker compose file in the repo starts the OMG pilot implementation, and a weekly job pushes a model at it and pulls it back. The first push failed five different ways: the pilot types relationship endpoints, feature typing, and requirement text as lists where my records carried scalars, it has no type for two of my relationship kinds, it drops multiplicity, it ignores the ownership key I send, and it assigns its own element ids regardless of what the client provides. The client now adapts records on the way out and tolerates the differences on the way back, and the table above is honest about what that costs. Mocked tests would have told me none of this.

There is also a syntax oracle: a weekly job feeds every .sysml file my writer emits to the pilot implementation’s parser, running out of process through a community-built jar, pinned by checksum. The oracle taught me one more lesson in humility, since the pilot crashes internally on unit literals like 5.0 [kg] when its standard library is not loaded, and I had to teach the job to distinguish that crash from a genuine rejection of my output.

Where this goes

The narrow claim: as of 2026-08-21 there is a released, pip-installable path from a SysML v2 requirement to an executed analysis and back, with the thresholds in the model, the engine behind an entry point, and the results carrying provenance. The claim I am explicitly not making: that anything here decides what to run. The satcom example binds one analysis to one engine at one fidelity. The interesting version of this problem has a scan-loss requirement deciding between an analytic pattern model and a full-wave solve, with an agent spending a compute budget where the margin is thinnest. That loop needs exactly the substrate this post described, and building it on top is the next project. A full-wave engine wrapping openEMS through antenna-cad is the natural first candidate.

Update, later the same day: sysml2kit 0.4.0 shipped a deterministic first version of that loop. Sibling bindings labeled with fidelity and cost form a ladder, and an escalate policy spends a wall-clock budget on the requirements with the thinnest margins. The 0.4.0 post demonstrates it catching a scanned-gain failure the cheap rung missed. Agent-chosen allocation is still open.

If you work in MBSE tooling and know of prior art for the binding convention, or you maintain a SysML v2 tool and the fidelity tables above misstate what your side does, tell me and I will correct the record.

Try it

pip install "sysml2kit[parse,verify]" sysml2kit-rf-library phased-array-systems
MODEL=$(python -c 'import sysml2kit_rf_library as m; print(m.models_dir())')
sysml2kit verify "$MODEL/interchange/satcom_terminal_pas.json" --report run.json
sysml2kit show "$MODEL/interchange/satcom_terminal_pas.json" --traceability

Every version number, upstream issue, and fidelity claim above was checked against source, PyPI, or a live run on 2026-08-21. The packages are pre-alpha; pin exact versions if you build on them.

These are independent projects 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 Apache-2.0 Python toolkit for building, querying, validating, and executing SysML v2 models. Version 0.3.0, current when this post was written, covers an object model for a documented subset of the language, a textual-notation writer, Systems Modeling API JSON interchange, traceability queries, validation rules, diffs, mermaid diagrams, an MCP server with nine tools, an HTTP client for the standard API, and a verification runner. It is on PyPI as sysml2kit.

How does a SysML v2 model bind to an executable analysis?

A metadata annotation named verificationBinding on an analysis case names an engine and a config file. Engines are resolved by name from the sysml2kit.engines entry-point group, so installing a package like phased-array-systems 0.13.0 makes its engine available. The model file itself never names importable code paths, because models are data and should not be able to run arbitrary code.

Can I run the demo without the RF stack?

You need three pip installs: sysml2kit with the parse and verify extras, sysml2kit-rf-library for the example model, and phased-array-systems for the engine. The whole loop runs in a few seconds on a laptop. The engine executes an analytic 16 by 16 array study at 28 GHz, so no full-wave solver or GPU is involved.

Does this conform to the full SysML v2 specification?

No, and the documentation says so. sysml2kit implements a pragmatic profile of roughly 20 element kinds with an opaque passthrough for everything else, pinned to the 2026-05 spec release. Writer output is checked against the OMG pilot implementation's parser out of process, and fidelity tables in SPEC.md state exactly what survives textual notation, interchange JSON, and a round trip through the pilot server.

Is the requirement-driven fidelity allocation problem solved?

Partly. This post describes the substrate: requirements with machine-checkable thresholds, verify links to analyses, engines that execute, and results recorded back with provenance. Release 0.4.0, published later the same day, added a deterministic first version of the allocation loop: fidelity ladders with an escalate policy that spends a wall-clock budget on the requirements with the thinnest margins. The version where an agent chooses what to spend remains future work.

More in AI agents