diff --git a/.CI/Jenkinsfile b/.CI/Jenkinsfile index 8c07913..099aa1e 100644 --- a/.CI/Jenkinsfile +++ b/.CI/Jenkinsfile @@ -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)') @@ -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' @@ -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 -fmi and -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 -fmi, -fmi-cs-cvode, -fmi-cs-euler and -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 } @@ -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(""" @@ -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')) { @@ -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" } } diff --git a/README.md b/README.md index 6686277..8c320d9 100644 --- a/README.md +++ b/README.md @@ -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/_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 @@ -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 `-` a tool adds to the branch. Only - OMSimulator needs it, with `""`. +- `branchSuffix` overrides the `-` 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/_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. diff --git a/configs/fmi-simulators.json b/configs/fmi-simulators.json index 56cb39e..4ab7e86 100644 --- a/configs/fmi-simulators.json +++ b/configs/fmi-simulators.json @@ -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", - " -, so OMSimulator is the only one that needs to say anything here.", + " -.", + "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/_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", diff --git a/test.py b/test.py index 2e4efab..e52d72e 100755 --- a/test.py +++ b/test.py @@ -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='') diff --git a/testmodel.py b/testmodel.py index 9ab087a..6f7843b 100755 --- a/testmodel.py +++ b/testmodel.py @@ -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') @@ -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, @@ -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. @@ -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.""" @@ -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,