Use Python
Python: TOML File
Keep the parameters in a document instead of in code. Point GraphEvolver at the file,
run it, read the results back as Python values. The configuration is something you can archive,
diff and send to someone else.
Start Here: The Example Bundle
Download get-examples.zip: a script that runs the examples, a script that plots what they produced, and five complete configurations, laid out so every path inside resolves. It is the fastest way to see this route working, and the rest of this page explains what the files in it do.
get-examples/
python_from_config.py runs one configuration; the only file you edit
run_all.py runs all five, then plots every one of them
analyze_output.py plots the folders the runs produced
graph_to_png.py draws one result, standard library only
01_edge_edit_generational.toml
02_sda_steady_state.toml
03_edge_edit_base_graph.toml
04_sda_profile_match.toml
05_exercise_degree_match.toml
base_graph_ring.csv base_graph_empty.csv base_graph_powerlaw.csv
output/ every run writes here
The archive is flat, so unpack it into a folder of its own: it fills whatever directory you point
the extractor at rather than making one. Install GET with the commands in the script's own header,
then work through the configurations in order. Each one adds to the shape of the one before it, so
01 is the smallest complete document and 04 the richest:
python python_from_config.py 01_edge_edit_generational.toml
python python_from_config.py 02_sda_steady_state.toml
python python_from_config.py 03_edge_edit_base_graph.toml
python python_from_config.py 04_sda_profile_match.toml
05 is an exercise, not a fifth run. It selects
[fitness] type = "python", which names a callable the configuration cannot supply, so
a run with nothing registered stops and tells you so. Open python_from_config.py and
uncomment the one commented-out block in register_objective before you run it. That
block is the only commented-out code in the bundle, and uncommenting it is the only edit needed.
Every file is also readable here without downloading it. Results
land in output/example_N/, N counting up as you go, with one folder per replicate
inside, so a second run never overwrites the first, and each example folder holds enough
replicates to compare.
Install
Version 0.9.0 is published on PyPI. The distribution is
graph-evolution-tool, while Python imports get.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install graph-evolution-tool
python -c "import get; print(get.__name__)"
py -m venv .venv,
skip the activate line entirely, because a stock machine refuses to run it, and
write .venv\Scripts\python.exe wherever a command on this page says
python or python3. pip becomes
.venv\Scripts\python.exe -m pip. That substitution is the whole Windows route;
nothing else on this page differs. If you would rather have an activated shell, see
PowerShell refuses to run
Activate.ps1.
graph-evolution-tool==0.9.0 when an experiment must
resolve the same package later.
A Rust toolchain is needed only when building a development checkout from source.
The Config Document
Five required tagged sections plus a handful of top-level keys. Each type selects a variant,
and the rest of that section's keys depend on which one you chose.
population_size = 200
network_size = 100
max_edge_multiplicity = 1 # 1 = unweighted (the default)
crossover_rate = 0.9
mutation_rate = 0.2 # is this child mutated at all?
max_mutations = 1 # if so, how many
[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 | python
type = "epi_spread"
infection_rate = 0.5
num_epidemics = 30
[section] header. Once a table header opens, every following key belongs to
that table, so a population_size written below [evolution] becomes
evolution.population_size, and the error you get is a missing
population_size.
Every key this document can carry, with its default and what rejects it, is in the Configuration Reference.
Running It
import get
evolver = get.GraphEvolver("config.toml")
results = evolver.run(seed=7, n_runs=1)
best = results[0]
print(best.best_fitness, len(best.best_edges))
best.save_logs("run_log.csv")
best.save_results("best_individual.txt")
run always returns a list, one result per replicate, even for a single run, so code
written for one does not need rewriting for twenty.
[genome], set
base_graph = "base_graph.csv". A relative path is resolved beside the TOML file, the
graph is read as 0-indexed, and its required # nodes = N header must equal
network_size. Use
evolver.set_base_graph_from_file("base_graph.csv", min_node_index=1) after construction
instead when your file is 1-indexed or selected dynamically. See
Data & Inputs.
Reading a Result
| Attribute | What it holds |
|---|---|
best_fitness | The winning score, in the direction the objective asked for. |
best_edges | (u, v, multiplicity) triples, u < v. |
num_nodes | Node count of the winning graph. |
history | Per logged iteration: iteration, best_fitness, mean_fitness, std_dev, ci_95. |
config_toml | The configuration this run used, as text. |
Validation Happens Once, Up Front
The document is parsed and checked when you construct the evolver, not part-way through a run. In
field values are validated, but unknown-key handling is not uniformly strict. Only
[genome] and [genome.operation_weights] reject every unknown key. A typo
such as elite_cuont can be ignored and leave a default in use.
[fitness] has an additional
flattening limitation: a typo there can also be ignored, leaving the default in place. Two keys are
singled out and rejected by name because they are the mistakes that change a run silently:
seed, which belongs on run, and target_profile under a
variant that has no use for it. Anything else misspelled under [fitness] passes
quietly, so check that section by eye.
try:
evolver = get.GraphEvolver("config.toml")
except ValueError as err:
print("bad config:", err) # names the field and the constraint
Several Replicates
from pathlib import Path
results = evolver.run(seed=7, n_runs=8, max_cores=4)
for run_index, result in enumerate(results):
run_dir = Path("output") / f"run_{run_index}"
run_dir.mkdir(parents=True, exist_ok=True)
result.save_logs(str(run_dir / "run_log.csv"))
result.save_results(str(run_dir / "best_individual.txt"))
save_results writes only the named
network. Preserve the original TOML once at the experiment root, as the bundle runner does.
One master seed in, one derived seed per replicate. Asking for more replicates never changes the ones you already had. Reproduce a replicate with the same master seed and the same index. Its derived seed is not a handle you can pass back in.
Plotting What Came Out
Ten replicates is ten folders of CSV, which is not a result you can look at. The bundle's second
script reads the folders python_from_config.py wrote and turns them into pictures:
pip install matplotlib networkx scipy
python analyze_output.py output/example_1 output/example_2
python analyze_output.py output/example_*
Give it two or more example_N/ folders and it writes three things beside them: a
boxplot of final fitness with one box per folder, a convergence plot with every
replicate in grey and one average per folder, and one rendered PNG of each folder's
best network. Give it a single folder and the boxplot is skipped with a note, since one box compares
nothing.
{example_1, example_2} and then {example_1, example_3} into the same
directory leaves four plots rather than silently overwriting two.
Each configuration gets its own panel on the convergence plot and its own colour on the boxplot, because configurations can optimize different objectives and use different iteration counts. The average line uses whichever replicates reached each iteration; when that supporting count falls, the plot marks it.
The network render colours nodes by degree and edges by multiplicity, so a hub and a doubled edge are visible rather than inferred. Both codes carry a key, and the two are different shapes because the numbers are: degree is open-ended and gets a colourbar, while a multiplicity capped at 5 or fewer gets a legend naming each value outright. Nodes with no edges at all are drawn in grey on a ring outside the graph. A layout that solves for them puts them in the middle, which is the one place a disconnected node should never appear, and they get their own legend entry, because grey is not a point on the degree scale.
Only the analysis script needs extra packages. It is a workshop tool rather than part of
GET. For a picture with no installation at all, graph_to_png.py draws a single result
using only the standard library.
An Objective Written in Python
Set type = "python" in the document and register the callable before running. The
section names no objective of its own. It says "one will be registered", and a run that reaches
scoring without one is rejected rather than silently scoring nothing.
[fitness]
type = "python"
def total_edges(batch):
return [float(len(edges)) for (num_nodes, edges) in batch]
evolver = get.GraphEvolver("config.toml")
evolver.set_fitness_function(total_edges, "maximize")
results = evolver.run(seed=7, n_runs=1)
The function takes the whole population at once and returns one number per individual in
the same order. Direction is declared here, never inferred. NaN stops the run.
Infinity is allowed, but which one means "invalid" follows the direction: +inf
under minimize, -inf under maximize; the opposite one is the best possible score.
Building the Document From Python
If you would rather generate configurations than hand-write them, the same typed objects the other
Python route uses will serialize to this format, which is useful for sweeping a parameter across many
files. See Python: Config Objects, and
config_builder.py, which builds configurations
without running anything.
A Complete Example
The example bundle at the top of this page is this route end to end: a script that takes a config path and a seed, and five configurations covering the options described above. Every file in it is readable here without downloading.
For a single document showing every key at once rather than five that build up to it, see
config.example.toml.