FMI build once simulate many - #297
Merged
Merged
Conversation
First step of OpenModelica#78: the FMI jobs come in pairs - v1.26-fmi and v1.26-fmi-fmpy, v1.27-fmi and v1.27-fmi-fmpy, master-fmi and master-fmi-fmpy - and both jobs of a pair build the same Model Exchange FMUs, some 19500 of them, only to simulate them with a different tool. Measured on ExternData here: 164 seconds building the FMUs and 0.6 simulating them with OMSimulator, then another 167 seconds building the very same FMUs to simulate them for 3 seconds with FMPy. --fmisimulator can now be repeated, as "name=command" or as the bare command it has always been, so that one job can simulate every FMU with several tools after building it once. The FMU can only be shared when everything that goes into it is the same - the same compiler, the same library and the same build flags - so what a merged job varies is the simulator and nothing else. That is why the three cs-fmu-cvode jobs stay as they are: their FMUs are Co-Simulation ones built with --fmiFlags=s:cvode, not the same artifact at all. --branch now names the job rather than the table, and every simulator derives its own from it: OMSimulator keeps the plain v1.27-fmi it has always had, FMPy fills v1.27-fmi-fmpy, PyFMI would fill v1.27-fmi-pyfmi. The mapping is keyed by the simulator and not by the order it was given in, so that a job asked for FMPy alone still writes to v1.27-fmi-fmpy instead of taking over v1.27-fmi. No report and no history has to move. A job that passes a single simulator behaves as it always did, and the version of every tool is now read rather than only the first one's. This only adds the option and the naming rules; testmodel.py still simulates with one tool, which the next commit changes. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
Adding a simulator to the testing was a case in testmodel.py, one in the
version check and one in the choice of result file. It is now an entry in
configs/fmi-simulators.json and nothing else:
"fmusim": {
"untested": true,
"resultExtension": "csv",
"versionArgument": "--version",
"stepSizeArgument": " --output-interval {stepSize:g}",
"arguments": "--interface-type ModelExchange --output-file {result} ..."
}
An entry says how the tool is invoked, what it writes, how it prints its
version, how it spells a step size and which table it fills. The arguments are
a template formatted with the model and the experiment, so a tool with
different flags needs no code, and a Python package without a command line
needs a driver script that the entry then points "command" at.
stepSizeArgument is a key of its own because the tools disagree about an
experiment that has no step size: OMSimulator leaves the flag out, while FMPy
passes --output-interval 0 all the same. A tool of the second kind writes
{stepSize:g} into its arguments and needs no stepSizeArgument at all.
The entry for fmusim is there because it is the next one we expect to want. It
is marked untested, and asking for a simulator nobody has run yet says so
rather than failing every model with a puzzling error.
Checked against the format strings testmodel.py uses today: both tools are
given exactly the command line they were given before, with a step size and
without one.
---
Generated by Claude Code.
Signed-off-by: Adrian Pop <adrian.pop@liu.se>
The heart of OpenModelica#78. testmodel.py builds the FMU as before and then runs every simulator the job was given on it, and test.py writes one row per simulator into the branch of that simulator. A job given OMSimulator and FMPy on --branch=v1.27-fmi therefore fills v1.27-fmi and v1.27-fmi-fmpy from a single build, which is what the two jobs of a pair used to do twice. Measured on ExternData, 12 models of which 9 build an FMU: separate 164s building + 0.60s simulating OMSimulator 167s building + 3.09s simulating FMPy merged 178s building + 0.74s + 2.58s both so the FMUs are built once instead of twice and the run costs about half. The results are the same either way: every model's phase, verification time, number of variables compared and number that differ is identical to what the two separate runs produced, for both tables. The verification of a result against its reference file became a function that returns rather than one that ends the run, so that the next simulator of the same FMU can be verified too, and omc is now quit once all of them are done rather than after the first. A simulator that fails or times out records its own failure and leaves the others alone; the first one keeps ending the model as it always has, so put the tool you trust most first. A model that fails before its FMU exists reports the phase the build stopped at for every simulator, rather than pretending each of them failed. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
The .err and .sim files of a run are collected per library and rsynced to a directory named after the branch under libraries.openmodelica.org/branches. A job that runs several simulators has one set of results per simulator, so it now does that once per branch: v1.27-fmi and v1.27-fmi-fmpy each get their own directory, their own library page and their own files, as they did when two jobs produced them. A branch directory looks exactly as it did before, down to the file names. A simulator writes <model>_<simulator>.sim in the workspace so that the tools of one job do not overwrite each other, but each branch is published from a directory of its own where those appear under the plain <model>.sim that has always been there. The links are hard, not symbolic, so rsync sees ordinary files and nothing is copied twice on disk; where that is not possible, across file systems, the file is copied. The .err is written by the build and is therefore the same for every simulator of a model, and each branch gets a copy rather than a link into another branch: a job asked for FMPy alone publishes no v1.27-fmi to link into, and a branch that is re-run must not break the pages of another. It costs about 66 MB of 3.4 kB files against the 58 GB such a directory already holds. The report of a library is generated from the results of the simulator whose branch it belongs to, so its phases, times and links describe that simulator and not the first one. The two ways of publishing disagreed about --output and still do: the rsync path is given the directory of the branch, the --noSync path the directory the branches live in. Both now derive the directory of each simulator the way they already derived the one of the job. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
The last part of OpenModelica#78. The three pairs of FMI jobs - v1.26-fmi and v1.26-fmi-fmpy, v1.27-fmi and v1.27-fmi-fmpy, master-fmi and master-fmi-fmpy - are one job each now. Six stages become three, and the FMUs of a release are built once instead of twice. The parameters keep their meaning: fmi_v1_27 asks for OMSimulator, fmpy_fmi_v1_27 for FMPy, and ticking both runs one job that builds every FMU once and simulates it with both, filling v1.27-fmi and v1.27-fmi-fmpy as the two jobs did. Ticking one runs that tool alone and fills only its table, and its results and files still go where they always went: a job given FMPy alone stores them in v1.27-fmi-fmpy and publishes them to branches/v1.27-fmi-fmpy, not to the branch it was started with. Which simulators a job runs is now something it is told rather than something guessed from its name, but a job that says nothing still gets the tool its name implies, so the cs-fmu-cvode jobs and every job that does not test FMI are untouched. OMSimulator is only cloned and built when a job actually asks for it. Checked here on ExternData, for all three ways of asking: OMSimulator alone fills v1.27-fmi, FMPy alone fills v1.27-fmi-fmpy, both fill the two of them from a single build, and the results of a merged run are the same as those of the two separate runs. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
Two sections in the README: how to run a job with more than one FMI simulator, what each of them fills and where it is published, and what may differ between results that share an FMU; and how to add a simulator, which is an entry in configs/fmi-simulators.json and no change to any script. The description of every key of an entry is there, including why a step size flag may have to disappear rather than be passed empty, and what a Python package without a command line needs. --- Generated by Claude Code. Signed-off-by: Adrian Pop <adrian.pop@liu.se>
adrpo
added a commit
that referenced
this pull request
Aug 12, 2026
A library whose reference files are already prepared by another library took a
`continue` that skipped the rest of the configuration loop - and the only thing
after it is the line that turns FMI on:
if allTestsFmi:
c["fmi"] = "2.0"
So four libraries of configs/conf.json have been running as ordinary native
models inside the FMI jobs: PowerGrids_symb_jac, PowerGrids_dev, ClaRa_dev and
ScalableTestSuite_noopt, each of them the second library to want a reference
directory that PowerGrids, ClaRa and ScalableTestSuite had already cloned. They
verify identically in master and in master-fmi - 66, 19, 72 and 244 models -
while every library that prepares its own directory drops as an FMI job should
(242 to 176 for ScalableTestSuite, 65 to 31 for PowerGrids).
The reuse also looked the directory up under a key it had not computed yet, so
it took whatever `destination` the previous library left behind. Both are fixed
by computing the normalised destination first and putting the preparation in an
else branch, which leaves nothing after the loop's body unreachable.
Since #297 this crashed the run rather than quietly mistesting it. Such a model
has no per-simulator results, so the second simulator's branch reported the
phase the *first* simulator reached - 7, verified - with no comparison to go
with it, and building the report died on it after seven hours:
("%s (%d verified)" % (timeSeconds(diff.get("time")), ...)) if s[3]["phase"]>=7
AttributeError: 'NoneType' object has no attribute 'get'
A simulator that never ran a model now reports the phase the shared build
reached and never a phase another simulator went on to reach with the same FMU,
in the report and in the database row alike; and a model with nothing to compare
renders as an empty cell whatever phase it claims, so a mismatch can never again
throw away a finished run.
Fixes the master-fmi failure of build 11369.
adrpo
added a commit
that referenced
this pull request
Aug 12, 2026
An FMI job that runs several simulators over one FMU (#297, #309) gives the first of them a veto over the rest. When it fails, the TimeoutError handler calls writeResultAndExit, the process is gone, and the loop that runs fmisimulators[1:] never starts -- but a row is still written for every simulator, recording the phase the first one stopped at. The other tools are marked as having failed at a model they were never given. checkOutputTimeout raises TimeoutError for a command that exits non-zero as well as for one that runs out of time, so this covers an ordinary failure and not just a slow one. In the master-fmi run of 2026-08-12 this cost 433 models. They had never simulated under OMSimulator -- phase 5 in every master-fmi run back to 2026-08-08 -- but FMPy ran them, and the previous master-fmi-fmpy run has them at phase 6 or 7. In the first combined run they are phase 5 in both tables, with FMPy's simulation time recorded as exactly 0: OMSimulator failed in under a second for 344 of them, and FMPy never started. They show up as 433 of the 653 regressions on the comparison page, next to the 213 that PR #309 really did find. It is also why that page reports OMSimulator's version, and its JSON parse warning, in place of "FMPy version 0.3.29": FMPy never ran to say what it was. The first simulator now records its failure and falls through to the others, which is what every simulator after it already did. Nothing changes when only one simulator is configured: that case still ends the model where it always has, so the non-FMI paths and the single-simulator FMI jobs are untouched. Verified on MyLibrary.Blocks.Examples.PID_Controller with the first simulator replaced by /bin/false: before phase 5, "simulators": {} after phase 5, "simulators": {"fmpy": {"sim": 0.64, "phase": 7}} and, unchanged in both: one failing simulator phase 5, os._exit as before both simulators working OMSimulator phase 7, fmpy phase 7 Claude-Session: https://claude.ai/code/session_01MYvMaAotMy425H7nvKzWX7 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #78, the first task of #295: each FMU is built once and simulated with every tool the job asks for, instead of once per tool.
The duplication
The FMI jobs come in pairs. Both jobs of a pair build the same Model Exchange FMUs — about 19500 of them — and differ only in what simulates them:
v1.26-fmiv1.26-fmi-fmpyv1.27-fmiv1.27-fmi-fmpymaster-fmimaster-fmi-fmpyBuilding is what costs. Measured here on the twelve models of
ExternData, nine of which build an FMU:48% of the run, three times over.
The three
*-cs-fmu-cvodejobs are not part of this: they build Co-Simulation FMUs with--fmiFlags=s:cvode --fmuRuntimeDepends=modelica, which are different artifacts. Only the simulator may differ between results that share an FMU; anything that changes the FMU is a different job.Using it
--fmisimulatorcan be given once per tool:./test.py --branch=v1.27-fmi --fmi=true \ --fmisimulator=/path/to/OMSimulator \ --fmisimulator='python3 -m fmpy' \ configs/conf.json--branchnames the job, and every simulator derives its own branch from it:v1.27-fmibranches/v1.27-fmiv1.27-fmi-fmpybranches/v1.27-fmi-fmpywhich is where those results have always been, so no report and no history has to move. The branch follows the tool, not the order, so a job given FMPy alone still fills
v1.27-fmi-fmpyand leavesv1.27-fmiuntouched — including where it publishes.A branch directory looks exactly as it did before, file names included. A simulator writes
<model>_<simulator>.simin the workspace so that the tools of one job do not overwrite each other, but each branch is published from a directory where those appear under the plain<model>.sim. The.erris written by the build, so every simulator of a model publishes the same one; a copy rather than a link into another branch, since a job may fill only one of them, and 66 MB of 3.4 kB files against the 58 GB such a directory already holds is not worth the coupling.Adding a simulator
The simulators are described in
configs/fmi-simulators.json. Adding one is an entry there and no change to any script:argumentsis a template over the model and the experiment;optionalArgumentsare the flags that have to disappear when there is nothing to put in them. That last one is not hypothetical: OMSimulator hangs on--stepSize=0rather than ignoring it (OMSimulator#1622, found while doing this), while FMPy wants--output-interval 0all the same. The entry forfmusimis included because it is the next tool we expect to want; it is markeduntested, and asking for a simulator nobody has run yet says so rather than failing every model with a puzzling error. A Python package without a command line needs a small driver script that the entry pointscommandat.The README has both: how to run several simulators, and how to add one.
Jenkins
Six stages become three. The parameters keep their meaning:
fmi_v1_27asks for OMSimulator,fmpy_fmi_v1_27for FMPyWhich simulators a job runs is now something it is told rather than something guessed from its name, but a job that says nothing still gets the tool its name implies, so the
cs-fmu-cvodejobs and every job that does not test FMI are untouched. OMSimulator is only cloned and built when a job actually asks for it.How it was checked
Everything below was run here against real FMUs, with
omc 1.28.0~dev-284,OMSimulator 3.0.0~154andFMPy 0.3.30.v1.27-fmi, FMPy alone fillsv1.27-fmi-fmpy, both fill the two of them from a single build — results and published directories alike.testmodel.pypassed the two tools before, with a step size and without one..sim.That testing earned its keep: it caught FMPy's
--output-filebeing blanked by OMSimulator's omit-the-flag rule, models failing before the FMU exists being reported as phase 0 instead of the phase the build stopped at,omc_newcaptured as a local when the verification became a function, and the first simulator's branch being the name of the job rather than of the tool. Each of those would have looked like a mass regression in one of the tables.Before it runs for real
test.pyis also touched by the PostgreSQL branch (Several improvements to LibraryTesting needed: network SQLDB and docker images to run the tests #295) in the same region. Whichever merges second needs a small merge.Generated by Claude Code.