Skip to content

Commit 5b24b0b

Browse files
authored
Give each runner a working directory of its own (#360)
One build is simulated by several runners -- an FMU by several FMI tools, a wasm FMU as me and cs, a model by several solvers -- and they all shared the build directory. A file the model writes on its first run and reads back on the next is then computed by whichever runner goes first and found already cached by the rest, which reads as the later ones being faster. Buildings' ground temperature-response matrix is the big one: it lands in `tmp/temperatureResponseMatrix/<sha>TStep.mat` relative to the working directory, and locally costs 83.8 s against 24.0 s once it is there -- more than the whole me-vs-cs difference on those rows. The wasm-jit lane does not have this (its job runs one simulation in a fresh directory), so the warm numbers were being compared against cold ones. Each runner now simulates in `run_<name>/`. The build stays where it was and every runner reaches it as `..`, so the result files land where the report already expects them; that also means naming them explicitly, since the defaults are relative to the working directory (`-r=` for the result, `-f=` for the executable's setup XML). Assisted-by: Claude Opus 5 (1M context)
1 parent ef783c4 commit 5b24b0b

1 file changed

Lines changed: 58 additions & 14 deletions

File tree

‎testmodel.py‎

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,31 @@ def runnerSuffix(name):
869869
publishes the workspace itself, so its files keep the plain names."""
870870
return "_%s" % name if name and runners and name != runners[0][0] else ""
871871

872+
def runnerDir(name):
873+
"""A working directory of this runner's own, or None when there is only one.
874+
875+
A file the model writes on its first run and reads back on the next --
876+
Buildings' ground temperature-response matrix under `tmp/`, an EnergyPlus
877+
scratch directory -- is otherwise computed by whichever runner goes first and
878+
found already cached by the rest, which reads as the later ones being faster.
879+
The build stays in the directory above and every runner reaches it as `..`, so
880+
the result files stay where the report expects them.
881+
"""
882+
if len(runners) < 2 or not name:
883+
return None
884+
d = "run_%s" % name
885+
if not os.path.isdir(d):
886+
os.mkdir(d)
887+
return d
888+
889+
def fromRunnerDir(runDir, path):
890+
"""`path`, which names something in the build directory, as a runner sees it."""
891+
return path if not runDir or os.path.isabs(path) else "../%s" % path
892+
893+
def inRunnerDir(runDir, cmd):
894+
"""`cmd` run in the runner's directory. The pipe and the log stay outside it."""
895+
return "(cd %s && %s)" % (runDir, cmd) if runDir else cmd
896+
872897
def resultFile(name=None):
873898
"""Where a simulator writes its results."""
874899
# Only an FMI tool decides the format; the others write what the model was
@@ -885,23 +910,25 @@ def artifactPrefix(name=None):
885910
def simulateFmu(name, command, resFile, simFile):
886911
"""Run the FMU with one simulator, writing what it says to simFile."""
887912
suffix = runnerSuffix(name)
913+
runDir = runnerDir(name)
888914
fmitmpdir = "temp_%s%s_fmu" % (conf["fileName"].replace(".","_"), suffix)
889915
with open("%s.tmpfiles" % conf["fileName"], "a+") as fp:
890-
fp.write("%s\n" % fmitmpdir)
916+
fp.write("%s\n" % (os.path.join(runDir, fmitmpdir) if runDir else fmitmpdir))
891917
cmd = shared.fmiSimulatorCommand(name, command,
892-
fmu="%s.fmu" % conf["fileName"].replace(".","_"),
893-
result=resFile,
894-
requestedResult=resFile if outputFormat != "empty" else "",
918+
fmu=fromRunnerDir(runDir, "%s.fmu" % conf["fileName"].replace(".","_")),
919+
result=fromRunnerDir(runDir, resFile),
920+
requestedResult=fromRunnerDir(runDir, resFile) if outputFormat != "empty" else "",
895921
tempDir=fmitmpdir, startTime=startTime, stopTime=stopTime,
896922
tolerance=tolerance, timeout=conf["ulimitExe"],
897923
stepSize=stepSize)
924+
cmd = inRunnerDir(runDir, cmd)
898925
with open(simFile,"w") as fp:
899926
fp.write("%s\n" % cmd)
900927
pipe = "%s%s" % (conf["fileName"], suffix)
901928
return checkOutputTimeout("(rm -f %s.pipe ; mkfifo %s.pipe ; head -c 1048576 < %s.pipe >> %s & %s > %s.pipe 2>&1)"
902929
% (pipe,pipe,pipe,simFile,cmd,pipe), 1.05*conf["ulimitExe"], conf)
903930

904-
def wasmFmuCmd(runnerFlags, resFile):
931+
def wasmFmuCmd(runnerFlags, resFile, runDir):
905932
"""The simulate() that runs the exported FMU one way.
906933
907934
Nothing is translated: `resimulateExecutable` points at the FMU, `-s
@@ -910,43 +937,60 @@ def wasmFmuCmd(runnerFlags, resFile):
910937
"""
911938
# An empty output format is a run with nothing to compare against, so it is
912939
# asked for no result file at all rather than one nobody reads.
913-
resultArgument = "-noemit" if outputFormat == "empty" else "-r=%s" % resFile
940+
resultArgument = "-noemit" if outputFormat == "empty" else "-r=%s" % fromRunnerDir(runDir, resFile)
914941
# The export baked in no filter, and the variableFilter argument below only
915942
# reaches a model through the build this run skips.
916943
filterArgument = "" if variableFilter in ("", ".*") else "-variableFilter=%s" % variableFilter
917944
simflags = " ".join(x for x in (annotationSimFlags, conf["simFlags"], emit_protected,
918945
"-lv LOG_STATS",
919946
"-startTime=%g -stopTime=%g -tolerance=%g -stepSize=%g" % (startTime,stopTime,tolerance,stepSize),
920947
filterArgument, resultArgument, runnerFlags) if x.strip())
921-
return 'simulate(%s,startTime=%g,stopTime=%g,tolerance=%g,numberOfIntervals=%d,outputFormat="%s",variableFilter="%s",fileNamePrefix="%s",simflags="%s",resimulateExecutable="%s.fmu")' % (
922-
conf["modelName"],startTime,stopTime,tolerance,numberOfIntervals,outputFormat,variableFilter,conf["fileName"],simflags,conf["fileName"].replace(".","_"))
948+
return 'simulate(%s,startTime=%g,stopTime=%g,tolerance=%g,numberOfIntervals=%d,outputFormat="%s",variableFilter="%s",fileNamePrefix="%s",simflags="%s",resimulateExecutable="%s")' % (
949+
conf["modelName"],startTime,stopTime,tolerance,numberOfIntervals,outputFormat,variableFilter,conf["fileName"],simflags,fromRunnerDir(runDir, "%s.fmu" % conf["fileName"].replace(".","_")))
923950

924951
def simulateWasmFmu(name, runnerFlags, resFile, simFile):
925952
"""Run the FMU one way, writing what omc says to simFile.
926953
927954
Returns what simulate() answered; an empty resultFile is a failed run.
928955
"""
929-
cmd = wasmFmuCmd(runnerFlags, resFile)
956+
runDir = runnerDir(name)
957+
cmd = wasmFmuCmd(runnerFlags, resFile, runDir)
930958
# The export is a directory, and a zipped one unpacks itself beside the .fmu on
931959
# the first run; the cleanup removes what this file names.
932960
with open("%s.tmpfiles" % conf["fileName"], "a+") as fp:
933961
fp.write("%s.fmu\n%s_artifact\n" % (conf["fileName"].replace(".","_"), conf["fileName"].replace(".","_")))
934962
with open(simFile, "w") as fp:
935963
fp.write("startTime=%g\nstopTime=%g\ntolerance=%g\nnumberOfIntervals=%d\nstepSize=%g\n" % (startTime,stopTime,tolerance,numberOfIntervals,stepSize))
936964
fp.write("wasm FMU (%s: %s): %s\n" % (name, shared.wasmFmuRunner(name).get("description") or "", cmd))
937-
res = sendExpressionTimeout(omc, cmd, conf["ulimitExe"]) or {}
965+
try:
966+
if runDir:
967+
sendExpressionTimeout(omc, 'cd("%s")' % runDir, conf["ulimitOmc"])
968+
res = sendExpressionTimeout(omc, cmd, conf["ulimitExe"]) or {}
969+
finally:
970+
# A run that timed out took omc with it; there is then nothing to change back.
971+
if runDir:
972+
try:
973+
sendExpressionTimeout(omc, 'cd("..")', conf["ulimitOmc"])
974+
except Exception:
975+
pass
938976
with open(simFile, "a+") as fp:
939977
fp.write(res.get("messages") or "")
940978
return res
941979

942980
def simulateExecutable(name, solverFlags, resFile, simFile):
943981
"""Run the built executable once, with the flags of one solver."""
944-
exe = ".\\%s.bat" % conf["fileName"] if isWin else "./%s" % conf["fileName"]
982+
runDir = runnerDir(name)
983+
exeName = "%s.bat" % conf["fileName"] if isWin else conf["fileName"]
984+
exe = fromRunnerDir(runDir, exeName) if runDir else (".\\%s" % exeName if isWin else "./%s" % exeName)
945985
# A run sharing the directory with other solvers needs a result file of its own.
946-
resultArgument = "-r=%s" % resFile if runnerSuffix(name) and outputFormat != "empty" else ""
947-
cmd = " ".join(x for x in (exe, annotationSimFlags, conf["simFlags"], emit_protected,
986+
# A run in a directory of its own needs one named explicitly either way: the
987+
# default lands beside the executable, which is no longer the working directory.
988+
resultArgument = "-r=%s" % fromRunnerDir(runDir, resFile) if (runDir or runnerSuffix(name)) and outputFormat != "empty" else ""
989+
# `-f` because the setup XML the executable reads by default is beside it too.
990+
setupArgument = "-f=%s" % fromRunnerDir(runDir, "%s_init.xml" % conf["fileName"]) if runDir else ""
991+
cmd = inRunnerDir(runDir, " ".join(x for x in (exe, annotationSimFlags, conf["simFlags"], emit_protected,
948992
"-lv LOG_STATS" if conf["simCodeTarget"] in ("C","C+Rust") else "",
949-
resultArgument, solverFlags) if x.strip())
993+
setupArgument, resultArgument, solverFlags) if x.strip()))
950994
with open(simFile,"w") as fp:
951995
fp.write("Environment - simulationEnvironment:\n")
952996
for e in conf["environmentSimulation"]:

0 commit comments

Comments
 (0)