Use Rust
Rust: The get-run CLI
One command, one config file, three output files. No Python interpreter and no compiled module to install. This is the route for reproducing a run from a pinned copy of the source and a document.
Build It
This is the one route that starts at the repository. get-run is built from a
checkout rather than installed from a package, and that is deliberate: this route serves someone
who wants a frozen copy of the code behind a set of numbers, so the source you built from is part
of the result. Take a tagged release for a run you intend to cite, or main to track
development.
git clone https://github.com/md12ol/GraphEvolutionTool
cd GraphEvolutionTool
git checkout <tag> # a release tag; omit to build main instead
cargo build --release -p graph-evolution-tool --features cli --bin get-run
./target/release/get-run --help
Releases are listed at
github.com/md12ol/GraphEvolutionTool/releases;
use v0.9.0 for this release. Record the resolved commit as well as the tag. The Rust
routes start from a checkout because the crate is not published to crates.io; see
Rust: As a Library for an application dependency.
PATH to build it, even though you never write any
Python and the binary never runs an interpreter. The cli feature pulls in the
Python bindings' auto-initialize support; without a Python present the build fails, and a stale
binary can die at startup rather than printing anything useful.
libpython must be findable at run time too, which is a
separate failure from the one above and looks nothing like it: the build succeeds and the binary
dies before printing a word. It bites when Python was installed somewhere the dynamic loader does
not search: a pyenv or CI interpreter rather than the system one.
Point the loader at it:
export LD_LIBRARY_PATH="$(python3 -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))'):$LD_LIBRARY_PATH"
Derive the directory rather than hardcoding a version, so upgrading Python does not break it. On
macOS the variable is DYLD_LIBRARY_PATH.
Run It
get-run config.toml 7
The first argument is the config document, the second an optional master seed. Omit the seed and one is drawn at random, which is fine for a look around and useless for anything you want to repeat.
| Argument | Meaning |
|---|---|
<config.toml> | Required. The run's configuration. |
[seed] | Master seed. Random if omitted. |
--runs N | Replicates from that master seed. Each gets its own run_<n>/. |
--out DIR | Put the timestamped folder under DIR instead of here. It chooses the root, nothing else. |
--help | Print the usage above and exit. -h works too. |
The Config Document
population_size = 200
network_size = 100
max_edge_multiplicity = 1
crossover_rate = 0.9
mutation_rate = 0.2
max_mutations = 1
[evolution] # generational | steady_state
type = "generational"
num_generations = 500
elite_count = 1
[scope] # who is eligible to breed
type = "global"
[selection] # best | tournament
type = "tournament"
tournament_size = 5
[genome] # edge_edit | sda
type = "edge_edit"
gene_length = 256
[fitness] # epi_spread | epi_length | epi_prof_match | struct_match
type = "epi_spread"
infection_rate = 0.5
num_epidemics = 30
[section] header. Once a table opens, every
following key belongs to it, so a population_size written under
[evolution] becomes evolution.population_size, and the error you get is
a missing population_size.
type = "python" is the one objective this route cannot use. There is no
interpreter here to call, so a config selecting it is rejected up front rather than failing
part-way through a run. Every other objective works exactly as it does elsewhere.
Every key the document can carry is in the Configuration Reference.
Where the Files Go
Every invocation makes a <timestamp>-<seed>/ folder and writes three files
into it. With no --out that folder is made where you are standing:
$ get-run config.toml 7
20260821-141233-7/run_log.csv # one row per logged iteration
20260821-141233-7/best_individual.txt # the winner as a loadable edge list
20260821-141233-7/config.toml # the exact config this run used
--out chooses where that folder is made and changes nothing else. Either way a second
invocation gets a folder of its own, so nothing is ever overwritten, and the config you
passed in is never written over, because the run's copy is one level down.
$ get-run config.toml 7 --out output
output/20260821-141233-7/run_log.csv
output/20260821-141233-7/best_individual.txt
output/20260821-141233-7/config.toml
Several Replicates
get-run config.toml 7 --runs 4
20260821-141233-7/config.toml # once, shared by all four
20260821-141233-7/run_1/run_log.csv
20260821-141233-7/run_2/run_log.csv
20260821-141233-7/run_3/run_log.csv
20260821-141233-7/run_4/run_log.csv
--runs needs nothing else. Each replicate gets its own
run_<n>/ inside the timestamped folder, so they cannot overwrite one another
and --out is a choice about where to put the folder rather than a requirement.
One master seed goes in and GET derives one seed per replicate. Reproduce a replicate by
re-running with the same master seed and reading the same run folder. The derived seed is not
something you pass back in. Run folders count from one and are zero-padded to the width of
--runs, so ten replicates sort as run_01 through run_10;
the run_index recorded in the log stays zero-based.
Reading the Log
run_log.csv has a header row and one row per logged iteration:
iteration, best_fitness, mean_fitness,
std_dev, ci_95, and the seed and replicate index the row came from.
# best-of-run per replicate, across a --runs sweep
for f in output/*/run_*/run_log.csv; do
printf "%s " "$f"
tail -n1 "$f" | cut -d, -f2
done
What This Route Promises
Configurations are portable; the command line is not. A config document this accepts is one every other route accepts, because all of them converge on the same parser and the same validator before anything is constructed. The binary's own surface (argument order, the names of the three files, the shape of the folder they land in) may change between versions. Pin a commit if you are depending on it, which is what this route is for.
When You Want More Than a Config File
A config document can only select the objectives GET ships. To score graphs with your own function and still stay in Rust, use the crate directly. See Rust: As a Library.