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
6 changes: 6 additions & 0 deletions Readme_Files/Vida HOWTO.txt
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,12 @@ BASIC USAGE

Runs the same setup three times, giving Output-repeatTest, Output-repeatTest1 and Output-repeatTest2. Because seeds, dispersal and death are random, each run turns out differently, which lets you look at the spread of outcomes.

Example 9: running several simulations at once

python tools/run_many.py -rngstarts 1-8 -jobs 4 -- -n example9 -w 100 -s 10 -t 200 -m 550

The -x repeats of example 8 run one after another. To use every core of the computer instead, tools/run_many.py runs several simulations at once, each as its own copy of Vida, so each gives exactly the same results as it would on its own. This runs example 1 (without the images) eight times, with -rngstart 1 to 8, four at a time, in folders named "Output-example9-rng1" and so on, with what each prints saved in "Output-example9-rng1.log". It can also run a list of different simulations from a file; see python tools/run_many.py -h.


GRAPHICS, VIDEOS AND 3D FILES
-----------------------------
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/test_run_many.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Tests for tools/run_many.py: working out which runs to start."""

import importlib.util
from pathlib import Path

import pytest

TOOL = Path(__file__).resolve().parents[2] / "tools" / "run_many.py"
spec = importlib.util.spec_from_file_location("run_many", TOOL)
run_many = importlib.util.module_from_spec(spec)
spec.loader.exec_module(run_many)


def test_reading_rng_starts():
assert run_many.readRngStarts("1-4") == [1, 2, 3, 4]
assert run_many.readRngStarts("1,3,5") == [1, 3, 5]
assert run_many.readRngStarts("1-3,10") == [1, 2, 3, 10]


def test_one_run_per_rng_start_each_with_its_own_name():
runs = run_many.runsForRngStarts([1, 2], ["-n", "forest", "-w", "100", "-t", "50"])
assert runs == [
["-n", "forest-rng1", "-rngstart", "1", "-w", "100", "-t", "50"],
["-n", "forest-rng2", "-rngstart", "2", "-w", "100", "-t", "50"],
]


def test_runs_without_a_name_use_vidas_default_name():
assert run_many.runsForRngStarts([7], ["-w", "50"]) == [["-n", "default-rng7", "-rngstart", "7", "-w", "50"]]


def test_rngstart_in_the_options_is_refused():
with pytest.raises(SystemExit):
run_many.runsForRngStarts([1], ["-n", "forest", "-rngstart", "3"])


def test_runs_from_a_file(tmp_path):
runs = tmp_path / "runs.txt"
runs.write_text("# dry and wet\n-n dry -w 100 -rngstart 1\n\n-n wet -w 100 -rngstart 1\n")
assert run_many.runsFromFile(str(runs)) == [["-n", "dry", "-w", "100", "-rngstart", "1"], ["-n", "wet", "-w", "100", "-rngstart", "1"]]
assert run_many.nameOf(["-n", "dry", "-w", "100"]) == "dry"
146 changes: 146 additions & 0 deletions tools/run_many.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Run several Vida simulations at once, one on each processor core.

Within one simulation everything happens in a set order, drawing on the
same stream of random numbers, so one simulation can't be split across
cores without changing its results. But separate simulations don't share
anything: this runs each one as its own copy of Vida, side by side. Each
gives exactly the same results as it would if run on its own; only the
order they finish in changes.

Run it from the folder that has Vida.py in it. Examples:

# the same simulation with -rngstart 1 to 8, four at a time
python tools/run_many.py -rngstarts 1-8 -jobs 4 -- -n forest -w 100 -s 400 -t 50

# every line of runs.txt is the options for one run, e.g.
# -n dry -w 100 -s 400 -t 50 -rngstart 1
# -n wet -w 100 -s 400 -t 50 -rngstart 1
python tools/run_many.py -file runs.txt

With -rngstarts, each run is named after the -n name and its -rngstart
(forest-rng1, forest-rng2, ...). Each run's output goes where Vida puts it
(Output-forest-rng1/ and so on), and what it prints goes to a log file next
to it (Output-forest-rng1.log). -jobs is how many run at once; it starts
at the number of cores.
"""

import argparse
import os
import subprocess
import sys
import time
from concurrent.futures import ThreadPoolExecutor


def readRngStarts(text):
# "1-8" or "1,3,5" or "1-4,10" -> [1, 2, ...]
rngStarts = []
for part in text.split(","):
part = part.strip()
if "-" in part:
first, last = part.split("-")
for rngStart in range(int(first), int(last) + 1):
rngStarts.append(rngStart)
elif part:
rngStarts.append(int(part))
return rngStarts


def nameOf(options):
# the value after -n in a run's options, or None
for i in range(len(options) - 1):
if options[i] == "-n":
return options[i + 1]
return None


def runsForRngStarts(rngStarts, options):
# One run per starting value: the same options, with its own -rngstart and name.
if "-rngstart" in options:
sys.exit("run_many: leave -rngstart out of the options; -rngstarts sets it for each run")
baseName = nameOf(options) or "default"
rest = []
skipNext = False
for option in options:
if skipNext:
skipNext = False
elif option == "-n":
skipNext = True
else:
rest.append(option)
runs = []
for rngStart in rngStarts:
name = "%s-rng%d" % (baseName, rngStart)
runs.append(["-n", name, "-rngstart", str(rngStart)] + rest)
return runs


def runsFromFile(fileName):
# One run for each line that isn't empty or a comment (#).
runs = []
with open(fileName) as theFile:
for line in theFile:
line = line.strip()
if line and not line.startswith("#"):
runs.append(line.split())
return runs


def runOne(options):
# Run Vida once, with what it prints going to a log file.
name = nameOf(options) or "run"
logName = "Output-%s.log" % name
start = time.time()
with open(logName, "w") as log:
result = subprocess.run([sys.executable, "Vida.py"] + options,
stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT)
seconds = time.time() - start
if result.returncode == 0:
print("finished %s in %.1f s" % (name, seconds))
else:
print("FAILED %s (exit code %d): see %s" % (name, result.returncode, logName))
return seconds, result.returncode


def main():
parser = argparse.ArgumentParser(description="Run several Vida simulations at once.")
parser.add_argument("-rngstarts", help="a run for each of these -rngstart values, e.g. 1-8 or 1,3,5")
parser.add_argument("-file", help="a file with the options for one run on each line")
parser.add_argument("-jobs", type=int, default=os.cpu_count() or 1, help="how many to run at once (default: the number of cores)")
parser.add_argument("options", nargs=argparse.REMAINDER, help="after --, Vida's options for every run (with -rngstarts)")
arguments = parser.parse_args()
options = arguments.options
if options and options[0] == "--":
options = options[1:]

if not os.path.exists("Vida.py"):
sys.exit("run_many: run this from the folder that has Vida.py in it")
if arguments.rngstarts and arguments.file:
sys.exit("run_many: use -rngstarts or -file, not both")
if arguments.rngstarts:
runs = runsForRngStarts(readRngStarts(arguments.rngstarts), options)
elif arguments.file:
runs = runsFromFile(arguments.file)
else:
sys.exit("run_many: say which runs, with -rngstarts or -file (see python tools/run_many.py -h)")

jobs = max(1, arguments.jobs)
print("%d runs, %d at a time" % (len(runs), jobs))
start = time.time()
with ThreadPoolExecutor(max_workers=jobs) as pool:
# each thread just starts one copy of Vida and waits for it
results = list(pool.map(runOne, runs))
wallClock = time.time() - start
total = 0.0
failed = 0
for seconds, returnCode in results:
total += seconds
if returnCode != 0:
failed += 1
print("all done in %.1f s (the runs took %.1f s between them)" % (wallClock, total))
if failed:
sys.exit("%d of the runs failed" % failed)


if __name__ == "__main__":
main()