Use Python

Python: Config Objects

Build a run out of typed objects, hand it to GraphEvolver, and read the results back as Python values. Nothing to look up in another file: the whole run reads top to bottom.

Install

Version 0.9.0 is published on PyPI as platform wheels. Install the distribution into a virtual environment; Python imports it as 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.
Why the names differ. The project/distribution name is graph-evolution-tool, while Python imports get. Pin graph-evolution-tool==0.9.0 for an archived environment.
Your editor is the API reference. The wheel ships py.typed and a complete type stub, so an editor or language server shows every signature, argument type and docstring for Config, GraphEvolver and RunResult as you type, offline, and always matching the version you installed. mypy checks your calls against it too.

The Smallest Complete Run

import get

config = get.Config(
    population_size=60,
    network_size=40,
    crossover_rate=0.9,
    mutation_rate=0.2,
    evolution=get.EvolutionConfig.Generational(num_generations=50, elite_count=1),
    scope=get.ScopeConfig.Global(),
    selection=get.SelectionConfig.Tournament(tournament_size=5),
    genome=get.GenomeConfig.EdgeEdit(gene_length=256),
    fitness=get.FitnessConfig.EpiSpread(
        sir=get.SirParams(infection_rate=0.5, num_epidemics=30)
    ),
)

evolver = get.GraphEvolver.from_config(config)
results = evolver.run(seed=7, n_runs=1)

best = results[0]
print(best.best_fitness, len(best.best_edges))

run always returns a list, one result per replicate, even for n_runs=1. That is deliberate: code written against a single run does not have to be rewritten when you ask for twenty.

Reading a Result

AttributeWhat it holds
best_fitnessThe winning score, in the direction you asked for.
best_edges(u, v, multiplicity) triples, u < v. Pairs with no edge are absent.
num_nodesNode count of the winning graph.
historyOne row per logged iteration: iteration, best_fitness, mean_fitness, std_dev, ci_95.
config_tomlThe exact configuration this run used, as text. Archive it beside the results.
first = best.history[0].best_fitness
last  = best.history[-1]
print(f"{first:.3f} -> {last.best_fitness:.3f} over {last.iteration} generations")

best.save_logs("run_log.csv")
best.save_results("best_individual.txt")
best.save_config(".")                       # writes config.toml

Write config_toml out with your results. On this route the parameters live in a program someone will edit, so the file is the only record of what a given set of numbers was actually produced by. Write one config.toml per experiment invocation rather than one per replicate, since every replicate in that invocation came from the same document. save_results writes only the network. save_config(directory) writes one shared config.toml for the experiment.

Several Replicates

One master seed goes in; GET derives a seed per replicate from it. Asking for more replicates does not change the ones you already had, and a replicate is reproduced by re-running with the same master seed and reading the same index, not by feeding its derived seed back in.

import os

results = evolver.run(seed=7, n_runs=8)

for run_index, result in enumerate(results):
    print(run_index, result.best_fitness)
    os.makedirs(f"output/run_{run_index}", exist_ok=True)
    result.save_logs(f"output/run_{run_index}/run_log.csv")
The save methods do not create directories. save_logs, save_results, and save_config write into a folder that must already exist. They raise OSError if it is missing, so make the folder first. The get-run CLI is different: --out builds the whole tree for you.

Replicates are spread across cores. Cap that with max_cores if you are sharing the machine: evolver.run(seed=7, n_runs=8, max_cores=4). Peak memory scales with min(max_cores, n_runs), so lowering it lowers memory too.

Your Own Objective

Score graphs with a Python function instead of a built-in. Select FitnessConfig.Python(), then register the callable and say which direction wins:

def total_edges(batch):
    """One score per individual, in the same order."""
    return [float(len(edges)) for (num_nodes, edges) in batch]

config = get.Config(
    ...,
    fitness=get.FitnessConfig.Python(),
)

evolver = get.GraphEvolver.from_config(config)
evolver.set_fitness_function(total_edges, "maximize")
results = evolver.run(seed=7, n_runs=1)

Three things the contract requires:

Starting From a Graph You Already Have

evolver = get.GraphEvolver.from_config(config)

# Call ONE of these, not both. A second loader that disagrees about the
# numbering raises, and one run has one numbering.

# from memory: (u, v, multiplicity), in your own node numbering
evolver.set_base_graph(40, [(0, 1, 1), (1, 2, 1)], min_node_index=0)

# from a file: one edge per line, with a `# nodes = N` header
evolver.set_base_graph_from_file("base_graph.csv", min_node_index=1)
These loaders warn rather than raise on three things, so a run can start from data you did not mean to supply and nothing will stop it. The file loader raises a Python UserWarning naming the file and line; the in-memory loader identifies the affected edge without a file location. In either case, warnings.simplefilter("error") turns them into failures if you would rather find out loudly.

A weight above max_edge_multiplicity is the opposite case and raises, naming the file, the line and the cap rather than clamping silently.

The first loader you call fixes the run's node numbering, and results come back in it. Load 1-indexed data and best_edges is 1-indexed too. A second loader that disagrees is rejected rather than quietly mixed in: one run has one numbering.

A Complete Example

python_inline.py is this route end to end: several replicates, an output directory per run, and all three files written per replicate. It is not shipped in the wheel (the package carries the module and nothing else), so download it and run python python_inline.py.

When to Use the Other Python Route

If you would rather keep the parameters in a document you can archive, diff and hand to someone else, write them as TOML instead. See Python: TOML File. The two routes accept exactly the same configurations; only where the parameters live differs.