Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions .CI/Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ pipeline {
booleanParam(name: 'v1_27', defaultValue: false, description: 'maintenance/v1.27 branch (ryzen-5950x-1)')
booleanParam(name: 'master', defaultValue: false, description: 'master branch, simulated with the solver each model asks for, filling master (ryzen-9950x).')

booleanParam(name: 'fmi_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with FMI, simulated by OMSimulator, filling v1.26-fmi (ryzen-5950x-2). Ticking it together with fmpy_fmi_v1_26 runs one job that builds every FMU once and simulates it with both.')
booleanParam(name: 'fmi_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with FMI, simulated by OMSimulator, filling v1.27-fmi (ryzen-5950x-2). Ticking it together with fmpy_fmi_v1_27 runs one job that builds every FMU once and simulates it with both.')
booleanParam(name: 'fmi_master', defaultValue: false, description: 'master branch with FMI, simulated by OMSimulator, filling master-fmi (ryzen-5950x-2). Ticking it together with fmpy_fmi_master runs one job that builds every FMU once and simulates it with both.')
booleanParam(name: 'fmi_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with FMI, simulated by OMSimulator: Model Exchange filling v1.26-fmi, Co-Simulation with CVODE filling v1.26-fmi-cs-cvode and with Euler filling v1.26-fmi-cs-euler (ryzen-5950x-2). Ticking it together with fmpy_fmi_v1_26 runs one job that builds every FMU once and simulates it with all of them.')
booleanParam(name: 'fmi_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with FMI, simulated by OMSimulator: Model Exchange filling v1.27-fmi, Co-Simulation with CVODE filling v1.27-fmi-cs-cvode and with Euler filling v1.27-fmi-cs-euler (ryzen-5950x-2). Ticking it together with fmpy_fmi_v1_27 runs one job that builds every FMU once and simulates it with all of them.')
booleanParam(name: 'fmi_master', defaultValue: false, description: 'master branch with FMI, simulated by OMSimulator: Model Exchange filling master-fmi, Co-Simulation with CVODE filling master-fmi-cs-cvode and with Euler filling master-fmi-cs-euler (ryzen-5950x-2). Ticking it together with fmpy_fmi_master runs one job that builds every FMU once and simulates it with all of them.')

booleanParam(name: 'cs_fmu_cvode_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with CVODE CS FMUs running OMSimulator (ryzen-5950x-2)')
booleanParam(name: 'cs_fmu_cvode_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with CVODE CS FMUs running OMSimulator (ryzen-5950x-2)')
booleanParam(name: 'cs_fmu_cvode_master', defaultValue: false, description: 'master branch with CVODE CS FMUs running OMSimulator (ryzen-5950x-2)')

booleanParam(name: 'fmpy_fmi_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with FMI, simulated by FMPy, filling v1.26-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_v1_26 runs one job that builds every FMU once and simulates it with both.')
booleanParam(name: 'fmpy_fmi_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with FMI, simulated by FMPy, filling v1.27-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_v1_27 runs one job that builds every FMU once and simulates it with both.')
booleanParam(name: 'fmpy_fmi_master', defaultValue: false, description: 'master branch with FMI, simulated by FMPy, filling master-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_master runs one job that builds every FMU once and simulates it with both.')
booleanParam(name: 'fmpy_fmi_v1_26', defaultValue: false, description: 'maintenance/v1.26 branch with FMI, simulated by FMPy, filling v1.26-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_v1_26 runs one job that builds every FMU once and simulates it with all of them.')
booleanParam(name: 'fmpy_fmi_v1_27', defaultValue: false, description: 'maintenance/v1.27 branch with FMI, simulated by FMPy, filling v1.27-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_v1_27 runs one job that builds every FMU once and simulates it with all of them.')
booleanParam(name: 'fmpy_fmi_master', defaultValue: false, description: 'master branch with FMI, simulated by FMPy, filling master-fmi-fmpy (ryzen-5950x-2). Ticking it together with fmi_master runs one job that builds every FMU once and simulates it with all of them.')

booleanParam(name: 'newInst_daeMode', defaultValue: false, description: 'master branch, --daeMode with -d=newInst (ryzen-5950x-2)')
booleanParam(name: 'newInst_newBackend', defaultValue: false, description: 'master branch, -d=newInst --newBackend, (ryzen-5950x-1)')
Expand Down Expand Up @@ -549,7 +549,7 @@ pipeline {
}
environment {
GITBRANCHES = 'maintenance/v1.20 maintenance/v1.21 maintenance/v1.22 maintenance/v1.23 maintenance/v1.24 maintenance/v1.25 maintenance/v1.26 maintenance/v1.27 master newInst-newBackend'
GITBRANCHES_FMI = 'maintenance/v1.20-fmi maintenance/v1.21-fmi maintenance/v1.22-fmi maintenance/v1.23-fmi maintenance/v1.24-fmi maintenance/v1.25-fmi maintenance/v1.26-fmi maintenance/v1.27-fmi maintenance/v1.22-fmi-fmpy maintenance/v1.23-fmi-fmpy maintenance/v1.24-fmi-fmpy maintenance/v1.25-fmi-fmpy maintenance/v1.26-fmi-fmpy maintenance/v1.27-fmi-fmpy master-fmi master-fmi-fmpy'
GITBRANCHES_FMI = 'maintenance/v1.20-fmi maintenance/v1.21-fmi maintenance/v1.22-fmi maintenance/v1.23-fmi maintenance/v1.24-fmi maintenance/v1.25-fmi maintenance/v1.26-fmi maintenance/v1.27-fmi maintenance/v1.22-fmi-fmpy maintenance/v1.23-fmi-fmpy maintenance/v1.24-fmi-fmpy maintenance/v1.25-fmi-fmpy maintenance/v1.26-fmi-fmpy maintenance/v1.27-fmi-fmpy master-fmi master-fmi-fmpy maintenance/v1.26-fmi-cs-cvode maintenance/v1.27-fmi-cs-cvode master-fmi-cs-cvode maintenance/v1.26-fmi-cs-euler maintenance/v1.27-fmi-cs-euler master-fmi-cs-euler'
GITBRANCHES_NEWINST = 'oldInst'
GITBRANCHES_DAE = 'newInst-daeMode'
GITBRANCHES_NEWBACKEND_DAE = 'newBackend-daeMode'
Expand Down Expand Up @@ -917,11 +917,11 @@ def buildsWithCMake(branch) {
* omc stay outside. Passing `''` runs the job on the node itself.
*/
/* The FMI simulators a job runs, from the parameters that used to start one job each.
* Both ticked is one job that builds every FMU once and simulates it with both,
* filling <branch>-fmi and <branch>-fmi-fmpy exactly as the two jobs did. */
* Both ticked is one job that builds every FMU once and simulates it with all of them,
* filling <branch>-fmi, <branch>-fmi-cs-cvode, <branch>-fmi-cs-euler and <branch>-fmi-fmpy. */
def fmiSimulators(boolean omsimulator, boolean fmpy) {
def simulators = []
if (omsimulator) simulators << 'OMSimulator'
if (omsimulator) simulators += ['OMSimulator', 'OMSimulator-cs-cvode', 'OMSimulator-cs-euler']
if (fmpy) simulators << 'fmpy'
return simulators
}
Expand Down Expand Up @@ -1010,7 +1010,8 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla
}
FMI_TESTING_FLAG = ""
SOLVER_FLAG = (solvers ?: []).collect { " --solver=${it}" }.join('')
if (simulators.contains('OMSimulator') && omsHash) {
def omsSimulators = simulators.findAll { it.startsWith('OMSimulator') }
if (omsSimulators && omsHash) {
// In the image rather than on the node: test.py runs this binary from inside it, and one built
// against the node's libraries need not load there.
runSh("""
Expand Down Expand Up @@ -1048,7 +1049,7 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla
echo OMSimulator version:
${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator --version
""")
FMI_TESTING_FLAG = " --fmisimulator=${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator"
FMI_TESTING_FLAG = omsSimulators.collect { " --fmisimulator=${it}=${env.HOME}/saved_omc/OMSimulator/install/bin/OMSimulator" }.join('')
}

if (simulators.contains('fmpy')) {
Expand All @@ -1062,6 +1063,8 @@ def runRegressiontest(branch, name, extraFlags, omsHash, omcompiler, extrasimfla
FMI_TESTING_FLAG = "--fmi=true${FMI_TESTING_FLAG} --default=ulimitExe=50"
if (name.contains('cvode')) {
FMI_TESTING_FLAG += " --fmuType=cs"
} else if (fmiSimulators != null) {
FMI_TESTING_FLAG += " --fmuType=me_cs"
}
}

Expand Down
42 changes: 35 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,33 @@ are the ones that simulator produced.

Only the simulator may differ between results that share an FMU. Anything that
changes the FMU itself - a different compiler, a different library, a different
`--fmuType` or `--fmiFlags` - is a different job, which is why the Co-Simulation
jobs with CVODE are not merged with the Model Exchange ones.
`--fmuType` or `--fmiFlags` - is a different job.

In Jenkins the parameters keep their meaning: `fmi_v1_27` asks for OMSimulator
and `fmpy_fmi_v1_27` for FMPy. Ticking both runs one job that builds every FMU
once and simulates it with both; ticking one runs that tool alone.
The one exception is the Co-Simulation solver of the C runtime. An FMU reads it
from `resources/<prefix>_flags.json` when it is instantiated, so an entry with
`fmiFlags` runs a copy of the FMU with that file replaced. Build the FMU with
`--fmuType=me_cs` and one build covers both interfaces and both solvers:

```bash
./test.py --branch=v1.27-fmi --fmi=true --fmuType=me_cs \
--fmisimulator=OMSimulator=/path/to/OMSimulator \
--fmisimulator=OMSimulator-cs-cvode=/path/to/OMSimulator \
--fmisimulator=OMSimulator-cs-euler=/path/to/OMSimulator \
--fmisimulator='python3 -m fmpy' \
configs/myConf.json
```

| simulator | interface | branch |
| --- | --- | --- |
| `OMSimulator` | Model Exchange | `v1.27-fmi` |
| `OMSimulator-cs-cvode` | Co-Simulation, CVODE | `v1.27-fmi-cs-cvode` |
| `OMSimulator-cs-euler` | Co-Simulation, Euler | `v1.27-fmi-cs-euler` |
| `fmpy` | Model Exchange | `v1.27-fmi-fmpy` |

In Jenkins the parameters keep their meaning: `fmi_v1_27` asks for the three
OMSimulator runs and `fmpy_fmi_v1_27` for FMPy, and either builds `me_cs` FMUs.
Ticking both runs one job that builds every FMU once and simulates it with all
four; ticking one runs those tools alone.

### Adding an FMI simulator

Expand Down Expand Up @@ -302,8 +323,15 @@ Adding one is an entry there and no change to any script:
- `resultExtension` is what the tool writes, so that the comparison against the
reference file knows what to read.
- `versionArgument` prints the version, which is recorded with the results.
- `branchSuffix` overrides the `-<name>` a tool adds to the branch. Only
OMSimulator needs it, with `""`.
- `branchSuffix` overrides the `-<name>` a tool adds to the branch. OMSimulator
has `""`.
- `interface`, `me` or `cs`, is the interface the tool runs of an `me_cs` FMU,
available to the templates as `{interface}`. It is empty for an FMU with one
interface, which the tool then runs.
- `fmiFlags` makes the tool run a copy of the FMU whose
`resources/<prefix>_flags.json` holds these flags. `"s": "cvode"` in any entry
of the job builds the FMU with `--fmiFlags=s:cvode`, which links CVODE into it;
the Euler solver needs nothing linked.
- `untested` marks an entry nobody has run yet; the run then says so instead of
failing every model with a puzzling error. Remove it once it works.

Expand Down
42 changes: 39 additions & 3 deletions configs/fmi-simulators.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,56 @@
"resultExtension what the tool writes, so the verification knows what to compare.",
"versionArgument the flag that prints the version, recorded with the results.",
"branchSuffix appended to --branch to get the table of this simulator; the default is",
" -<name>, so OMSimulator is the only one that needs to say anything here.",
" -<name>.",
"interface me or cs: which interface the tool runs of an FMU built with --fmuType=me_cs,",
" available to the templates as {interface}. It is empty for an FMU exporting",
" one interface, which the tool then runs; OMSimulator refuses --mode for it.",
"fmiFlags what the tool runs is a copy of the FMU whose resources/<prefix>_flags.json",
" holds these flags instead, which is how the Co-Simulation solver of the C",
" runtime is picked without building the FMU again. s:cvode in any of them",
" builds the FMU with --fmiFlags=s:cvode, which links CVODE into it.",
"untested set it while nobody has run the entry yet; the run then says so."
],
"OMSimulator": {
"branchSuffix": "",
"interface": "me",
"resultExtension": "mat",
"versionArgument": "--version",
"arguments": "{resultArgument} --stripRoot --tempDir={tempDir} --startTime={startTime:g} --stopTime={stopTime:g}{stepSizeArgument} --timeout={timeout:.0f} --tolerance={tolerance:g} {fmu}",
"arguments": "{resultArgument} --stripRoot --tempDir={tempDir} --startTime={startTime:g} --stopTime={stopTime:g}{stepSizeArgument}{modeArgument} --timeout={timeout:.0f} --tolerance={tolerance:g} {fmu}",
"optionalArguments": {
"resultArgument": "--resultFile={requestedResult}",
"stepSizeArgument": " --stepSize={stepSize:g}"
"stepSizeArgument": " --stepSize={stepSize:g}",
"modeArgument": " --mode={interface}"
}
},
"OMSimulator-cs-cvode": {
"branchSuffix": "-cs-cvode",
"interface": "cs",
"fmiFlags": {"s": "cvode"},
"resultExtension": "mat",
"versionArgument": "--version",
"arguments": "{resultArgument} --stripRoot --tempDir={tempDir} --startTime={startTime:g} --stopTime={stopTime:g}{stepSizeArgument}{modeArgument} --timeout={timeout:.0f} --tolerance={tolerance:g} {fmu}",
"optionalArguments": {
"resultArgument": "--resultFile={requestedResult}",
"stepSizeArgument": " --stepSize={stepSize:g}",
"modeArgument": " --mode={interface}"
}
},
"OMSimulator-cs-euler": {
"branchSuffix": "-cs-euler",
"interface": "cs",
"fmiFlags": {"s": "euler"},
"resultExtension": "mat",
"versionArgument": "--version",
"arguments": "{resultArgument} --stripRoot --tempDir={tempDir} --startTime={startTime:g} --stopTime={stopTime:g}{stepSizeArgument}{modeArgument} --timeout={timeout:.0f} --tolerance={tolerance:g} {fmu}",
"optionalArguments": {
"resultArgument": "--resultFile={requestedResult}",
"stepSizeArgument": " --stepSize={stepSize:g}",
"modeArgument": " --mode={interface}"
}
},
"fmpy": {
"interface": "me",
"resultExtension": "csv",
"versionArgument": "--version",
"command": "{simulator} simulate",
Expand Down
2 changes: 1 addition & 1 deletion test.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
parser.add_argument('configs', nargs='*')
parser.add_argument('--branch', default='master')
parser.add_argument('--fmi', default=False)
parser.add_argument('--fmuType', default='me', help="FMU type: 'me' for Model Exchange, 'cs' for Co-Simulation.")
parser.add_argument('--fmuType', default='me', help="FMU type: 'me' for Model Exchange, 'cs' for Co-Simulation, 'me_cs' for both; the interface of configs/fmi-simulators.json then says which one a simulator runs.")
parser.add_argument('--output', default='')
parser.add_argument('--docker', default='')
parser.add_argument('--libraries', help="Directory omc will search in to load system libraries/libraries to test.", default='')
Expand Down
45 changes: 36 additions & 9 deletions testmodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from monotonic import monotonic
import OMPython
from OMPython import FindBestOMCSession, OMCSession, OMCSessionZMQ
import shared, glob
import shared, glob, zipfile

parser = argparse.ArgumentParser(description='OpenModelica library testing tool helper (single model)')
parser.add_argument('config')
Expand Down Expand Up @@ -713,6 +713,20 @@ def simulateCmd(resimulate):

# TODO: Detect and handle the case where RT_CLOCK is not available in OMC
total_before = omc.sendExpression("OpenModelica.Scripting.Internal.Time.timerTock(OpenModelica.Scripting.Internal.Time.RT_CLOCK_SIMULATE_TOTAL)")

# The FMU is built once and simulated with every tool the job asked for, so
# that testing FMPy no longer means building the same FMU a second time. The
# tools are described in configs/fmi-simulators.json.
fmisimulator = conf.get("fmisimulator")
fmisimulators = shared.parseFmiSimulators(conf.get("fmisimulators")) if conf.get("fmi") else []
if conf.get("fmi") and not fmisimulators and fmisimulator:
fmisimulators = shared.parseFmiSimulators([fmisimulator])

if conf.get("fmi") and any(shared.fmiSimulator(name).get("fmiFlags", {}).get("s") == "cvode" for (name, _) in fmisimulators):
with open(errFile, 'a+') as fp:
fp.write("Running command: setCommandLineOptions(\"--fmiFlags=s:cvode\")\n")
sendExpressionOldOrNew('setCommandLineOptions("--fmiFlags=s:cvode")')

start=monotonic()
timeout = conf["ulimitOmc"]
# If the command has to be killed there is no way to tell which phase it was in,
Expand Down Expand Up @@ -847,13 +861,6 @@ def simulateCmd(resimulate):
writeResult()
# Do the simulation

# The FMU is built once and simulated with every tool the job asked for, so
# that testing FMPy no longer means building the same FMU a second time. The
# tools are described in configs/fmi-simulators.json.
fmisimulator = conf.get("fmisimulator")
fmisimulators = shared.parseFmiSimulators(conf.get("fmisimulators")) if conf.get("fmi") else []
if conf.get("fmi") and not fmisimulators and fmisimulator:
fmisimulators = shared.parseFmiSimulators([fmisimulator])
wasmfmurunners = shared.parseWasmFmuRunners(conf.get("wasmfmurunners")) if useWasmFmu else []
# The model is built once and its executable run once per solver; the solvers are
# described in configs/solvers.json.
Expand All @@ -864,6 +871,25 @@ def simulateCmd(resimulate):
# several ways and a model run by several solvers fan out the same way.
runners = fmisimulators or wasmfmurunners or solverRunners

def fmuWithFlags(name):
"""The FMU a simulator runs: the one built, or a copy whose flags file holds
the fmiFlags of its entry, from which the C runtime picks its CS solver."""
prefix = conf["fileName"].replace(".","_")
fmu = "%s.fmu" % prefix
flags = shared.fmiSimulator(name).get("fmiFlags")
if not flags:
return fmu
copy = "%s_%s.fmu" % (prefix, name)
flagsFile = "resources/%s_flags.json" % prefix
with zipfile.ZipFile(fmu) as src, zipfile.ZipFile(copy, "w") as dst:
for item in src.infolist():
if item.filename != flagsFile:
dst.writestr(item, src.read(item))
dst.writestr(flagsFile, "{\n%s\n}\n" % ",\n".join(' "%s" : "%s"' % kv for kv in flags.items()), zipfile.ZIP_DEFLATED)
return copy

fmus = dict((name, fmuWithFlags(name)) for (name, _) in fmisimulators)

def runnerSuffix(name):
"""What tells the files of one runner from those of another. The first one
publishes the workspace itself, so its files keep the plain names."""
Expand Down Expand Up @@ -915,7 +941,8 @@ def simulateFmu(name, command, resFile, simFile):
with open("%s.tmpfiles" % conf["fileName"], "a+") as fp:
fp.write("%s\n" % (os.path.join(runDir, fmitmpdir) if runDir else fmitmpdir))
cmd = shared.fmiSimulatorCommand(name, command,
fmu=fromRunnerDir(runDir, "%s.fmu" % conf["fileName"].replace(".","_")),
fmu=fromRunnerDir(runDir, fmus[name]),
interface=shared.fmiSimulator(name).get("interface", "") if conf["fmuType"] == "me_cs" else "",
result=fromRunnerDir(runDir, resFile),
requestedResult=fromRunnerDir(runDir, resFile) if outputFormat != "empty" else "",
tempDir=fmitmpdir, startTime=startTime, stopTime=stopTime,
Expand Down
Loading