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__)"
On Windows, in PowerShell: create the environment with 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.
Pin recorded work. Use 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
The ordering rule that bites everyone once. Top-level keys must appear before the first [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.

A TOML config can name its starting graph. Under an edge-edit [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

AttributeWhat it holds
best_fitnessThe winning score, in the direction the objective asked for.
best_edges(u, v, multiplicity) triples, u < v.
num_nodesNode count of the winning graph.
historyPer logged iteration: iteration, best_fitness, mean_fitness, std_dev, ci_95.
config_tomlThe 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.

Check all config keys against the reference. [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"))
Keep one configuration per experiment. 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.

Every generated filename names the folders it came from, so analysing {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.