Documentation menu

Simulation as code: from architecture to a verified verdict

Release. The physics: ports, simulate: and expects:, arclang verify, the verdict cache and the Modelica export are in the 4.5.0 binaries. ArcLang 4.6.0 adds the drawn export (Placement, Line, Icon and Diagram annotations, --split, --no-placement), arclang verify --check-tools, and the Simulation status item, menu and guided setup in VS Code. Each of these is marked below.

ArcLang exports the physical architecture of a model to a Modelica package, and verifies requirements by simulating it in OpenModelica. The verdict of each simulation goes where every other verdict goes: the production gate and the requirement coverage table. The chain is:

  1. Architecture as code: nodes, ports that say what they carry, links, configurations.
  2. Modelica as code: arclang export -f modelica writes the structure; the physics is a Modelica package you write, extending it.
  3. A verdict: a test_case says what to simulate and what must hold; arclang verify runs OpenModelica and evaluates it.
  4. The gate: a failing simulation is a blocker, and a requirement is verified only if its simulations pass.
  5. CI: the pipeline runs the same commands and caches the verdicts (CI/CD).

The worked example is examples/automotive/ev-range/ev_range_simulation.arc of the compiler repository, with its physics in examples/automotive/ev-range/physics/EVPhysics.mo: a winter-range requirement that the long range pack meets and the standard pack does not. Its sources are copied here, and every command below was run for real when this page was built.

Ports say what they carry

A port declares its physics with physics:, on the port itself, on a physical port, or on the type the port is: (a port definition):

type "HV plus" { physics: electrical }
type "HV minus" { physics: electrical pole: negative }
type "Signal" { physics: signal }

node "Drive unit" {
    id: "PN-DRIVE"
    efficiency: 90 %
    gear_ratio: 9.0
    deploys "LC-DRIVE"
    port "hv_p" { is: "HV plus" }
    port "hv_n" { is: "HV minus" }
    port "shaft" { physics: mechanical_rotational flange: b }
    port in "torque_cmd" { is: "Signal" }
}
physics: Modelica Standard Library connector
signal Modelica.Blocks.Interfaces.RealInput (port in) or RealOutput (port out)
electrical Modelica.Electrical.Analog.Interfaces.PositivePin; NegativePin with pole: negative
mechanical_translational Modelica.Mechanics.Translational.Interfaces.Flange_a, Flange_b
mechanical_rotational Modelica.Mechanics.Rotational.Interfaces.Flange_a, Flange_b
thermal Modelica.Thermal.HeatTransfer.Interfaces.HeatPort_a, HeatPort_b
fluid not exported yet, and the export says so

A flange or heat port is _b with flange: b (or, for an out port, when it says nothing), _a otherwise. A port without physics: is not exported, and the export notes it: ArcLang does not guess. A physical_link or physical_exchange between ports becomes a connect(...). Its two ends must have the same physics, and a signal must go from an output to an input, otherwise the model does not compile, whether you export or not.

The generated structure

$ arclang export ev_range_simulation.arc -f modelica -o modelica --split
Exporting ev_range_simulation.arc to Modelica format...
✓ Export successful — package EVRangeSimulation (2 system model(s))
  system model: EVRangeSimulation.Systems.City
  system model: EVRangeSimulation.Systems.Touring
  Output: modelica/EVRangeSimulation
  The physics goes in a package of its own extending these models (see docs/modelica.md).

--split (4.6.0) writes one file per class in a <Package>/ folder, the layout of a Modelica library. Without it the package is one <Package>.mo file.

$ grep -n 'Interfaces' modelica/EVRangeSimulation/Physical/BatteryPack.mo | cut -c1-118
7:  Modelica.Electrical.Analog.Interfaces.PositivePin hv_p "electrical port hv_p" annotation(Placement(transformation(
8:  Modelica.Electrical.Analog.Interfaces.NegativePin hv_n "electrical port hv_n" annotation(Placement(transformation(

The package holds the structure and only the structure. Every model in it is partial: a node's model has its connectors and parameters, a configuration's model has its replaceable nodes and its connect equations. The physics lives in a package of its own, which extends the generated models and redeclares their parts, so regenerating never overwrites a line of it.

EVRangeSimulation                      generated, never edited
├── Behavior.<Node>.<Component>       partial model per behavior component
├── Physical.<Node>                    partial model per physical node
└── Systems.<Configuration>            partial model per configuration

EVPhysics                              hand-written
├── model Battery extends EVRangeSimulation.Physical.BatteryPack; equations
└── model TouringRange extends EVRangeSimulation.Systems.Touring(
          redeclare EVPhysics.Battery batteryPack, ...);
  • Names. The package is the model name in UpperCamelCase, classes are element names in UpperCamelCase and instances in lowerCamelCase (Battery pack gives Physical.BatteryPack and batteryPack). Ports and parameters keep their ArcLang names.
  • Traceability. Every class, component, connector and connect carries annotation(__ArcLang(id="...")), the ArcLang id it comes from; every class has a Documentation(info=...) from its description.
  • Parameters. A typed quantity becomes a parameter of the SI type of its dimension, in SI units: energy: 82 kWh gives parameter Modelica.Units.SI.Energy energy = 2.952e8.
  • Configurations. A node that deploys only variants is in the system models of the configurations selecting one of them. A node that deploys a variation declares the parameters its variants carry, and each system model sets them to the values of the variant its configuration selects.
  • Deterministic. The same model gives the same text, with no date.

A test case that simulates

test_case "Winter range, standard pack" {
    id: "TC-RANGE-STD"
    verifies: ["REQ-RANGE"]
    method: "analysis"
    simulate: { model: "EVPhysics.CityRange" configuration: "CFG-CITY" stop_time: 3.2 h sources: ["physics/EVPhysics.mo"] }
    expects: ["final(vehicleBody.distance) >= 300 km", "min(\"PN-BATT\".soc) >= 0.1"]
}

simulate: takes model (required: the hand-written class, which must extend the system model of configuration), stop_time (required) and start_time as times, sources (Modelica files, relative to the model file), tolerance (default 1e-6), intervals (output intervals, default 500), and backend.

Each entry of expects: compares two expressions over the result. A variable is a Modelica name of the result (vehicleBody.distance) or an ArcLang element in quotes followed by the rest of the path ("PN-BATT".soc is batteryPack.soc). A variable is a time series, read through min(v), max(v), final(v), initial(v), mean(v) or at(t, v). Numbers may carry units (300 km, 100 km/h) and are compared in SI. When OpenModelica gives the unit of a variable, the comparison is dimension-checked: final(distance) >= 300 s is an error, not a fail.

Until a simulation has been run, check says so, and a missing verdict is never a pass:

$ arclang check ev_range_simulation.arc | sed -n 1,6p
Checking ev_range_simulation.arc...
✓ No compilation errors

⚠ Compilation warnings:
  verification: test_case 'Winter range, standard pack' is inconclusive: never simulated: run `arclang verify ev_range_simulation.arc --test TC-RANGE-STD`
  verification: test_case 'Winter range, long range pack' is inconclusive: never simulated: run `arclang verify ev_range_simulation.arc --test TC-RANGE-LR`

arclang verify

$ arclang verify ev_range_simulation.arc
Verification by simulation — ev_range_simulation.arc
  Tool: docker image openmodelica/openmodelica:v1.27.1-minimal, OpenModelica 1.27.1

✗ TC-RANGE-STD "Winter range, standard pack": FAIL
    verifies REQ-RANGE · configuration City (CFG-CITY) · model EVPhysics.CityRange
    ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
    FAIL min(batteryPack.soc) = 0.08693   (min("PN-BATT".soc) >= 0.1, short by 0.01307)

✓ TC-RANGE-LR "Winter range, long range pack": PASS
    verifies REQ-RANGE · configuration Touring (CFG-TOURING) · model EVPhysics.TouringRange
    ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
    ok   min(batteryPack.soc) = 0.2874   (min("PN-BATT".soc) >= 0.1, 0.1874 to spare)

✓ TC-RANGE-ENVELOPE "Range envelope, long range pack": PASS
    verifies REQ-RANGE-ENVELOPE · configuration Touring (CFG-TOURING) · model EVPhysics.TouringRange
    sweep: 9 points, 9 pass, 0 do not
    ok   ambient = -20 °C, target_speed = 90 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.335   (min("PN-BATT".soc) >= 0.1, 0.235 to spare)
    ok   ambient = -20 °C, target_speed = 100 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.273   (min("PN-BATT".soc) >= 0.1, 0.173 to spare)
    ok   ambient = -20 °C, target_speed = 110 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.2016   (min("PN-BATT".soc) >= 0.1, 0.1016 to spare)
    ok   ambient = 0 °C, target_speed = 90 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.3573   (min("PN-BATT".soc) >= 0.1, 0.2573 to spare)
    ok   ambient = 0 °C, target_speed = 100 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.3006   (min("PN-BATT".soc) >= 0.1, 0.2006 to spare)
    ok   ambient = 0 °C, target_speed = 110 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.235   (min("PN-BATT".soc) >= 0.1, 0.135 to spare)
    ok   ambient = 20 °C, target_speed = 90 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.3766   (min("PN-BATT".soc) >= 0.1, 0.2766 to spare)
    ok   ambient = 20 °C, target_speed = 100 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.3244   (min("PN-BATT".soc) >= 0.1, 0.2244 to spare)
    ok   ambient = 20 °C, target_speed = 110 km/h
        ok   final(vehicleBody.distance) = 300 km   (final(vehicleBody.distance) >= 300 km, at the bound)
        ok   min(batteryPack.soc) = 0.2638   (min("PN-BATT".soc) >= 0.1, 0.1638 to spare)

  Simulated in 10.7 s (-v prints the OpenModelica log)

  3 verdict(s) cached in .arclang/sim-cache
Error: 1 of 3 simulated case(s) do not pass

arclang verify model.arc [--test TC-ID] [--json] exports to a temporary directory, runs OpenModelica on every case that simulate:s (or one), evaluates expects:, prints the values, and caches the verdicts. It exits non-zero unless every case passes. Both configurations drive 319.9 km in 3.2 h; only the standard pack, with 64 kWh instead of 82, runs the battery down to 2.7 % of its charge and so breaks the 10 % reserve.

The gate and the tables read the cached verdicts and never simulate:

$ arclang gate ev_range_simulation.arc
Production-readiness gate (ISO26262) — ev_range_simulation.arc
  Requirements: 2 total, 2 satisfied, 1 verified

✗ 1 blocker(s):
  [requirements.verification] requirement 'REQ-RANGE': verification cases fail (Winter range, standard pack: min(batteryPack.soc) = 0.08693 (expected min("PN-BATT".soc) >= 0.1))

⚠ 6 warning(s):
  [methodology] system analysis without an operational analysis: no operational need is captured
  [methodology] logical component 'Standard pack' performs no functions — Arcadia expects logical components to realize behavior
  [methodology] logical component 'Long range pack' performs no functions — Arcadia expects logical components to realize behavior
  [methodology] logical component 'Drive' performs no functions — Arcadia expects logical components to realize behavior
  [methodology] logical component 'Speed control' performs no functions — Arcadia expects logical components to realize behavior
  [methodology] logical component 'Body' performs no functions — Arcadia expects logical components to realize behavior

PRODUCTION GATE: FAIL ✗ (1 blockers)
Error: Compilation error: production gate failed with 1 blocker(s)
$ arclang table ev_range_simulation.arc --kind requirements_coverage --columns id,verified_by,verdict,evidence -f md
## Requirement coverage

2 rows: 1 ok, 1 failing. Gate ISO26262: blocked (1 blocker about these rows).

| Req id | Verified by | Verdict | Key values |
| --- | --- | --- | --- |
| REQ-RANGE | Winter range, standard pack (fail), Winter range, long range pack (pass) | fail | TC-RANGE-STD: final(vehicleBody.distance) = 300 km; min(batteryPack.soc) = 0.08693 (fails) \| TC-RANGE-LR: final(vehicleBody.distance) = 300 km; min(batteryPack.soc) = 0.2874 |
| REQ-RANGE-ENVELOPE | Range envelope, long range pack (pass) | pass | TC-RANGE-ENVELOPE: final(vehicleBody.distance) = 300 km; min(batteryPack.soc) = 0.335 |
Verdict When Cached Gate
pass every expectation holds yes verified
fail one does not; the values are reported yes blocker
error the model does not load, does not extend its system, does not simulate, or an expectation is ill-typed yes blocker
inconclusive no OpenModelica, a timeout; or, when compiling, a verdict never computed or stale no blocker

The verdict cache

The cache is .arclang/sim-cache/<test case id>.json beside the model. It holds the verdict, the values, the OpenModelica log and version, and the key: the SHA-256 of the generated package, every source file, and the case (backend, model, configuration, times, tolerance, intervals, expectations). Any change to one makes the cached verdict stale, which counts as inconclusive. The cache may be committed as evidence: arclang gate in CI then needs no OpenModelica, and fails when the evidence is stale.

Backends

backend: names what runs the simulation. openmodelica is the default and the only one built. fmu (any FMU, through FMPy) and command (an external script that writes a results CSV) are accepted by the language and answer inconclusive: backend 'fmu' is declared but not implemented yet.

Finding OpenModelica

OpenModelica is an external tool, looked for in this order:

  1. ARCLANG_OMC: the path of a local omc. When it is set and does not run, nothing else is tried.
  2. ARCLANG_OMC_DOCKER: a Docker image to run omc from.
  3. An omc on the PATH.
  4. Docker, with the default image openmodelica/openmodelica:v1.27.1-minimal (about 290 MB compressed).

With Docker, ArcLang runs docker run --rm -v <work dir>:/work -w /work -v arclang-openmodelica-libraries:/root/.openmodelica/libraries <image> omc run.mos. The model, the sources and the script are files of the working directory, so no model text goes through a shell. The minimal image has no Modelica Standard Library: the first run installs MSL 4.1.0 into the arclang-openmodelica-libraries volume, and later runs reuse it. A run is limited to 900 s (ARCLANG_OMC_TIMEOUT, in seconds), and a container that outlives it is removed. When no tool is found the verdict is inconclusive with the reason, and is not cached.

From 4.6.0, arclang verify --check-tools [--json] simulates nothing and reports what the machine has. Its output below is the one of the machine that built this page:

$ arclang verify --check-tools
OpenModelica: docker image openmodelica/openmodelica:v1.27.1-minimal
  local omc: no
  Docker installed: yes
  Docker running: yes
  image openmodelica/openmodelica:v1.27.1-minimal: pulled yes
  Modelica library (arclang-openmodelica-libraries): installed yes

In VS Code

From 4.6.0. Open a folder with a model that simulates (a test case with simulate:, or a physics: port) or a .mo file: a Simulation item appears in the status bar. Its text says where things stand: OpenModelica (Docker) or (local) when a tool is found, needs setup when none is, Simulating… during a run, then the verdicts of the last run (1 fail, 1 pass). Click it for the menu (also ArcLang: Simulation in the command palette):

  • Run simulations on this model (also the play button in the editor title and Simulate in the workbench) runs arclang verify under a progress notification you can cancel. The result offers Show the verdicts (the requirement coverage table) and Show the log.
  • Check my setup is a checklist: Docker installed, Docker running, the OpenModelica image, the Modelica library, a local omc, and the Modex extension. Each missing item has a button: open the Docker Desktop download page, start Docker Desktop, download the image (about 1 GB, with progress), install the library, install Modex, or show the exact command in a terminal (typed, not run).
  • Set up OpenModelica (guided) does the same fixes one step at a time.
  • Export Modelica, Open the Modelica drawing and Open the documentation.

When you run simulations and nothing is available, the checklist opens instead of an error. The settings arclang.simulation.image, arclang.simulation.omc and arclang.simulation.timeout are the three environment variables above; the first two name a program and are user settings only. Nothing runs in an untrusted workspace. In the browser the item is hidden, and the commands say that simulation needs the desktop extension. Canceling a run stops arclang; a Docker container already started may finish on its own.

Drawing the architecture in VS Code and OMEdit

From 4.6.0. Graphical Modelica editors draw from annotations, and without them the canvas is blank. The export therefore writes, by default:

  • Placement on every component instance, on a layered layout: inputs and upstream parts on the left, outputs and downstream parts on the right, boxes that do not overlap;
  • a Line on every connect, in right angles and colored by what it carries: electrical blue, signal dark blue, mechanical gray, thermal red;
  • a Diagram(coordinateSystem(extent=...)) on every model, sized to its boxes and wires;
  • an Icon on every model that is itself a component, with its connectors on the left (inputs, positive pins, _a flanges) or the right (outputs, negative pins, _b flanges).
$ grep -c 'Placement' modelica/EVRangeSimulation/Systems/Touring.mo
4

$ grep -o 'annotation(Line([^)]*)' modelica/EVRangeSimulation/Systems/Touring.mo | head -2
annotation(Line(points={{46.8,5.7},{67.8,5.7},{67.8,7.6},{88.9,7.6}}, color={0,0,255})
annotation(Line(points={{46.8,-5.7},{70.8,-5.7},{70.8,0},{88.9,0}}, color={0,0,255})

The __ArcLang(id=...) annotations stay beside them. --no-placement writes the structure alone, with no drawing. arclang verify always simulates that text, never the drawn one: a drawing change can neither alter a verdict nor invalidate the cache. If <model>.layout.json (the workbench layout file) sits next to the model, the offsets it holds for the physical view move the boxes from where the automatic layout puts them.

  • VS Code (next release; already on arclang.fr): ArcLang: Open the Modelica drawing offers the model's simulated scenarios and its configurations and draws the chosen one as OMEdit does, the Standard Library's icons included; a part opens its own diagram. OpenModelica gives the drawing (arclang verify model.arc --drawings lists what there is to draw, --drawing CLASS prints OpenModelica's instance of a class as JSON); on arclang.fr, signed in, it runs online. No other extension is needed.
  • Modex (VS Code, optional): use --split and add the folder that holds the package folders to modex.libraryRoots in the settings. Modex browses a package by its folders and resolves the icons of a model's components only for packages it knows as libraries. A single <Package>.mo file opens, but its nested models cannot be reached, and a component whose class Modex does not know is a plain box with the class name. Open <Package>/Systems/<Configuration>.mo for the whole architecture, and a file of Physical/ for the inside of a node. The first start offers to install the Modelica Standard Library (network needed once); accept it, since the connectors are Modelica.* classes.
  • OMEdit (OpenModelica) reads the same annotations from either layout, with MSL 4.1.0 installed. The compiler's tests load the package in omc, which OMEdit uses to read the annotations, and read the placements, lines and diagram extent back. OMEdit itself was not run for this.

Known limits of the drawing: the layout is layered and simple, so a model with many unconnected parts is wrapped into columns of four and a dense model has long wires routed round the outside; the colors cover the physics ArcLang exports (fluid ports are not exported); a long name is shrunk to its box by the editor; the size changes and bent edges of a layout file are ignored.

SysML v2

physics:, pole: and flange: are written as String attributes of the ports and types, and expects: as a String [*] attribute. simulate: is ArcLang-only: SysML v2 has no value for a block of settings, so it is written as its ArcLang text in a String attribute, which the importer reads back.

Limits

  • Signals are causal (RealInput, RealOutput); electrical, mechanical and thermal ports are acausal connectors. ArcLang does not check that the physics balances: OpenModelica does, when it checks the hand-written models.
  • Fluid ports are not exported: Modelica.Fluid connectors need a medium, which ArcLang does not model yet.
  • The logical architecture is not exported as models: it gives the variants and their parameters only.
  • Nodes do not nest in ArcLang; behavior components are one level inside their node. A port holding ports is not exported.
  • Simulating compiles C code: about 20 s a case with Docker on a laptop, the first run longer.
  • With Docker on Linux, files the container writes in the working directory belong to root; ArcLang reads the result and removes the directory as far as it can.
  • A model compiled from text (the language server, the API) has no file, hence no cache: its simulated cases are inconclusive.
  • Not available in the browser playground: it has no OpenModelica or Docker.

Every ArcLang sample on this page is compiled, and every command is run, with the released compiler when the site is built.