graph-evolution-tool 0.9.0__cp38-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- get/__init__.py +12 -0
- get/__init__.pyi +680 -0
- get/get.pyd +0 -0
- get/py.typed +0 -0
- graph_evolution_tool-0.9.0.dist-info/METADATA +75 -0
- graph_evolution_tool-0.9.0.dist-info/RECORD +9 -0
- graph_evolution_tool-0.9.0.dist-info/WHEEL +4 -0
- graph_evolution_tool-0.9.0.dist-info/licenses/LICENSE +21 -0
- graph_evolution_tool-0.9.0.dist-info/sboms/graph-evolution-tool.cyclonedx.json +1640 -0
get/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Re-exports the compiled extension module so `import get` gives the classes
|
|
2
|
+
# directly rather than `get.get.GraphEvolver`.
|
|
3
|
+
#
|
|
4
|
+
# maturin generates exactly this file when the package has no Python source of
|
|
5
|
+
# its own. It is written out here because the package now *does* have Python
|
|
6
|
+
# source — `py.typed` and `__init__.pyi`, which an editor needs beside the
|
|
7
|
+
# module — and declaring `python-source` means maturin stops generating it.
|
|
8
|
+
from .get import *
|
|
9
|
+
|
|
10
|
+
__doc__ = get.__doc__
|
|
11
|
+
if hasattr(get, "__all__"):
|
|
12
|
+
__all__ = get.__all__
|
get/__init__.pyi
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
"""Type stubs for `get`, generated by `tools/generate_stubs.py`.
|
|
2
|
+
|
|
3
|
+
Regenerate with `maturin develop --release && python3 tools/generate_stubs.py`.
|
|
4
|
+
Type annotations here are written by hand and survive regeneration; everything
|
|
5
|
+
else comes from the built module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
__all__ = ['GraphEvolver', 'Config', 'EvolutionConfig', 'ReplacementConfig', 'ScopeConfig', 'SelectionConfig', 'CrossoverConfig', 'EdgeEditMutationConfig', 'SdaMutationConfig', 'GenomeConfig', 'FitnessConfig', 'SirParams', 'OperationWeights', 'RunResult', 'GenerationStats']
|
|
11
|
+
|
|
12
|
+
class GraphEvolver:
|
|
13
|
+
"""Python-facing entry point to the graph-evolution engine.
|
|
14
|
+
|
|
15
|
+
Constructed from a `config.toml` path; `run` dispatches on the configured
|
|
16
|
+
evolution strategy, genome representation and fitness objective.
|
|
17
|
+
|
|
18
|
+
**It holds no results.** A run's state lives in the `RunResult` that `run`
|
|
19
|
+
returns, so one evolver drives repeated runs with nothing stale from the
|
|
20
|
+
previous one hanging off it.
|
|
21
|
+
"""
|
|
22
|
+
def __init__(self, config_path: str) -> None:
|
|
23
|
+
...
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_config(config: Config) -> GraphEvolver:
|
|
26
|
+
"""Build from configuration assembled in Python, rather than from a file.
|
|
27
|
+
|
|
28
|
+
Accepts exactly what a `config.toml` would, and reports a rejection in
|
|
29
|
+
the same words. Every block is its own object — `EvolutionConfig`,
|
|
30
|
+
`ScopeConfig`, `SelectionConfig`, `GenomeConfig`, `FitnessConfig` — and
|
|
31
|
+
all of them are required.
|
|
32
|
+
|
|
33
|
+
A field too large for a TOML integer is rejected here, which a
|
|
34
|
+
`config.toml` writer never meets.
|
|
35
|
+
"""
|
|
36
|
+
...
|
|
37
|
+
def load_reference_graphs(self, folder: str, min_node_index: int=0) -> list[tuple[str, int, list[tuple[int, int, int]]]]:
|
|
38
|
+
"""Read a folder of graphs, one file per graph, and hand them back.
|
|
39
|
+
|
|
40
|
+
The bulk counterpart to `set_base_graph_from_file`, for reference data an
|
|
41
|
+
objective matches against. Each file is one edge per line,
|
|
42
|
+
`start,end,weight`, and every file in the folder shares this run's node
|
|
43
|
+
numbering.
|
|
44
|
+
|
|
45
|
+
**A reference graph may be larger than the network being evolved**, and
|
|
46
|
+
usually is — the only ceiling is a sanity bound against a file indexed
|
|
47
|
+
the wrong way.
|
|
48
|
+
|
|
49
|
+
Each graph comes back as `(name, num_nodes, edges)`, **sorted by file
|
|
50
|
+
name**, because a reference set is consumed positionally and filesystem
|
|
51
|
+
order would let a run's numbers depend on how its data was written to
|
|
52
|
+
disk. The node count is stated rather than derived: an isolated node
|
|
53
|
+
appears in no edge, so each file declares its own in a `# nodes = N`
|
|
54
|
+
header. Sub-directories are skipped; every other file is read.
|
|
55
|
+
|
|
56
|
+
**This call declares the run's numbering** as the base-graph setters do.
|
|
57
|
+
The reference graphs themselves come back exactly as supplied.
|
|
58
|
+
|
|
59
|
+
A rejection names the file and the line; the warnings are the base-graph
|
|
60
|
+
loader's, each naming the file it came from.
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
def run(self, seed: int, n_runs: int=1, max_cores: int | None=None) -> list[RunResult]:
|
|
64
|
+
"""Evolve a population `n_runs` times and return what every run produced.
|
|
65
|
+
|
|
66
|
+
**Always a list, one `RunResult` per replicate, in run order** — even at
|
|
67
|
+
the default `n_runs = 1`, so the return type does not change shape with
|
|
68
|
+
an argument.
|
|
69
|
+
|
|
70
|
+
**One master seed, not `n` of them**, so a run's seed does not depend on
|
|
71
|
+
how many were requested: asking for 50 reproduces the first 30 of a
|
|
72
|
+
30-run request exactly.
|
|
73
|
+
|
|
74
|
+
**Whether replicates run concurrently is the engine's call, not yours.**
|
|
75
|
+
A native Rust objective runs them in parallel; `fitness = "python"` runs
|
|
76
|
+
them one at a time, since concurrent runs would contend for a single GIL.
|
|
77
|
+
`max_cores` caps the concurrency; unset means all available.
|
|
78
|
+
|
|
79
|
+
A replicate that fails abandons the remaining runs rather than returning
|
|
80
|
+
half-complete.
|
|
81
|
+
|
|
82
|
+
# Memory: the sizes multiply, they do not add
|
|
83
|
+
|
|
84
|
+
The whole population is materialized before scoring, and a graph is a
|
|
85
|
+
**dense** matrix however sparse it actually is, so peak memory is roughly
|
|
86
|
+
`network_size² × 4 bytes × population_size × min(max_cores, replicates)`.
|
|
87
|
+
Raising any one of them scales the whole product.
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
def set_base_graph(self, num_nodes: int, edges: list[tuple[int, int, int]], min_node_index: int=0) -> None:
|
|
91
|
+
"""Seed an edge-edit run from a graph the caller already has.
|
|
92
|
+
|
|
93
|
+
`edges` is `(u, v, multiplicity)` — the same shape `run` hands back as
|
|
94
|
+
`best_edges`, so one run's output feeds the next without reshaping.
|
|
95
|
+
|
|
96
|
+
**One run has one numbering.** `min_node_index` is where the caller's
|
|
97
|
+
own numbering starts — pass `1` for 1-indexed edges. Every loader on this
|
|
98
|
+
evolver must declare the same one, and it is what results are shifted
|
|
99
|
+
back into, so they return in the numbering the data arrived in.
|
|
100
|
+
|
|
101
|
+
**Left unset, every run starts from an empty graph.** Several edit
|
|
102
|
+
opcodes need existing structure to walk, so early generations do little
|
|
103
|
+
until `Add` and `Toggle` have built some. That is self-correcting.
|
|
104
|
+
|
|
105
|
+
A rejected edge raises `ValueError` naming the index as the caller wrote
|
|
106
|
+
it, not as it would be after shifting. A pair given more than once is a
|
|
107
|
+
`UserWarning` and the **last** occurrence wins, compared canonically, so
|
|
108
|
+
`(2, 5)` and `(5, 2)` are one undirected edge.
|
|
109
|
+
"""
|
|
110
|
+
...
|
|
111
|
+
def set_base_graph_from_file(self, path: str, min_node_index: int=0) -> None:
|
|
112
|
+
"""Seed an edge-edit run from an edge-list file, rather than from a list
|
|
113
|
+
built in Python.
|
|
114
|
+
|
|
115
|
+
One edge per line, `start,end,weight`, comma-delimited, any line ending.
|
|
116
|
+
`min_node_index` is where the caller's own node numbering starts — pass
|
|
117
|
+
`1` for 1-indexed data, which is the common case in graph files.
|
|
118
|
+
1-indexed in is 1-indexed out.
|
|
119
|
+
|
|
120
|
+
A `# nodes = N` header is mandatory and must agree with `network_size`;
|
|
121
|
+
a file with no header is rejected rather than assumed to match it.
|
|
122
|
+
|
|
123
|
+
**Nothing is stored unless the whole file survives**, and a rejection
|
|
124
|
+
names the line it came from. A repeated edge, a zero-weight edge and an
|
|
125
|
+
empty file are each a `UserWarning` rather than an error.
|
|
126
|
+
"""
|
|
127
|
+
...
|
|
128
|
+
def set_fitness_function(self, callable: Callable[..., float], direction: str) -> None:
|
|
129
|
+
"""Register a Python callable as the objective, with the direction it is
|
|
130
|
+
meant to be optimized in.
|
|
131
|
+
|
|
132
|
+
`config.toml` only *selects* Python — `[fitness] type = "python"`. The
|
|
133
|
+
callable itself arrives here, and so does its direction: nothing can
|
|
134
|
+
infer whether a user's function wants its value large or small.
|
|
135
|
+
|
|
136
|
+
The callable takes the **whole batch** and returns one float per graph,
|
|
137
|
+
in the same order:
|
|
138
|
+
|
|
139
|
+
A batch element is `(num_nodes, edges)`, and `direction` is
|
|
140
|
+
`"minimize"` or `"maximize"`.
|
|
141
|
+
|
|
142
|
+
Registering a callable when the config did not select Python is a
|
|
143
|
+
`ValueError`, not a silent no-op.
|
|
144
|
+
"""
|
|
145
|
+
...
|
|
146
|
+
|
|
147
|
+
class Config:
|
|
148
|
+
"""Everything the genetic algorithm needs for a run."""
|
|
149
|
+
def __init__(self, evolution: EvolutionConfig, population_size: int, network_size: int, crossover_rate: float, mutation_rate: float, scope: ScopeConfig, selection: SelectionConfig, genome: GenomeConfig, fitness: FitnessConfig, max_edge_multiplicity: int=1, max_mutations: int=1, crossover: CrossoverConfig | None=None) -> None:
|
|
150
|
+
...
|
|
151
|
+
@property
|
|
152
|
+
def crossover(self) -> CrossoverConfig:
|
|
153
|
+
"""Recombination operator; two-point when unset."""
|
|
154
|
+
...
|
|
155
|
+
@property
|
|
156
|
+
def crossover_rate(self) -> float:
|
|
157
|
+
"""Probability that a selected pair is recombined."""
|
|
158
|
+
...
|
|
159
|
+
@property
|
|
160
|
+
def evolution(self) -> EvolutionConfig:
|
|
161
|
+
"""Which evolution strategy to run."""
|
|
162
|
+
...
|
|
163
|
+
@property
|
|
164
|
+
def fitness(self) -> FitnessConfig:
|
|
165
|
+
"""Fitness objective."""
|
|
166
|
+
...
|
|
167
|
+
@property
|
|
168
|
+
def genome(self) -> GenomeConfig:
|
|
169
|
+
"""Genome representation and its dimensions."""
|
|
170
|
+
...
|
|
171
|
+
@property
|
|
172
|
+
def max_edge_multiplicity(self) -> int:
|
|
173
|
+
"""Edge-weight cap; 1 is unweighted."""
|
|
174
|
+
...
|
|
175
|
+
@property
|
|
176
|
+
def max_mutations(self) -> int:
|
|
177
|
+
"""How many mutations a mutating child takes, drawn uniformly from
|
|
178
|
+
`1..=max_mutations`.
|
|
179
|
+
"""
|
|
180
|
+
...
|
|
181
|
+
@property
|
|
182
|
+
def mutation_rate(self) -> float:
|
|
183
|
+
"""Probability that a child is mutated at all."""
|
|
184
|
+
...
|
|
185
|
+
@property
|
|
186
|
+
def network_size(self) -> int:
|
|
187
|
+
"""Number of nodes in every expressed graph."""
|
|
188
|
+
...
|
|
189
|
+
@property
|
|
190
|
+
def population_size(self) -> int:
|
|
191
|
+
"""Number of individuals in the population."""
|
|
192
|
+
...
|
|
193
|
+
@property
|
|
194
|
+
def scope(self) -> ScopeConfig:
|
|
195
|
+
"""Which slice of the population one breeding event draws from."""
|
|
196
|
+
...
|
|
197
|
+
@property
|
|
198
|
+
def selection(self) -> SelectionConfig:
|
|
199
|
+
"""Parent-selection strategy, applied within that scope."""
|
|
200
|
+
...
|
|
201
|
+
def to_toml(self) -> str:
|
|
202
|
+
"""Render this config as the TOML document GET parses — the record of what
|
|
203
|
+
was run, byte for byte.
|
|
204
|
+
|
|
205
|
+
`ValueError` if a field is too large for a TOML integer, `2**63 - 1`.
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
class EvolutionConfig:
|
|
210
|
+
"""Which evolution strategy to run, and its strategy-specific settings."""
|
|
211
|
+
|
|
212
|
+
class Generational(EvolutionConfig):
|
|
213
|
+
"""Whole-population replacement, run `num_generations` times."""
|
|
214
|
+
def __init__(self, num_generations: int, elite_count: int=1) -> None:
|
|
215
|
+
...
|
|
216
|
+
@property
|
|
217
|
+
def elite_count(self) -> int:
|
|
218
|
+
"""Best individuals carried forward untouched each generation.
|
|
219
|
+
|
|
220
|
+
Must be less than `population_size` — equal would mean nothing ever
|
|
221
|
+
changes.
|
|
222
|
+
"""
|
|
223
|
+
...
|
|
224
|
+
@property
|
|
225
|
+
def num_generations(self) -> int:
|
|
226
|
+
"""Whole-population replacements to run."""
|
|
227
|
+
...
|
|
228
|
+
|
|
229
|
+
class SteadyState(EvolutionConfig):
|
|
230
|
+
"""Single breeding events, `num_mating_events` of them.
|
|
231
|
+
|
|
232
|
+
One event touches one scope, not the whole population.
|
|
233
|
+
"""
|
|
234
|
+
def __init__(self, num_mating_events: int, replacement: ReplacementConfig | None=None) -> None:
|
|
235
|
+
...
|
|
236
|
+
@property
|
|
237
|
+
def num_mating_events(self) -> int:
|
|
238
|
+
"""Single breeding events to run. One event touches one scope, not the whole
|
|
239
|
+
population.
|
|
240
|
+
"""
|
|
241
|
+
...
|
|
242
|
+
@property
|
|
243
|
+
def replacement(self) -> ReplacementConfig:
|
|
244
|
+
"""Who the children overwrite; omitted, the least fit.
|
|
245
|
+
|
|
246
|
+
`Worst` is what makes steady-state self-elitist; `Random` gives that up.
|
|
247
|
+
"""
|
|
248
|
+
...
|
|
249
|
+
|
|
250
|
+
class ReplacementConfig:
|
|
251
|
+
"""Which members of a scope a mating event's children overwrite."""
|
|
252
|
+
|
|
253
|
+
class Random(ReplacementConfig):
|
|
254
|
+
"""Children overwrite individuals drawn uniformly from the scope, giving up
|
|
255
|
+
steady-state's self-elitism.
|
|
256
|
+
"""
|
|
257
|
+
def __init__(self) -> None:
|
|
258
|
+
...
|
|
259
|
+
|
|
260
|
+
class Worst(ReplacementConfig):
|
|
261
|
+
"""Children overwrite the least fit of the scope.
|
|
262
|
+
|
|
263
|
+
This is what makes steady-state self-elitist.
|
|
264
|
+
"""
|
|
265
|
+
def __init__(self) -> None:
|
|
266
|
+
...
|
|
267
|
+
|
|
268
|
+
class ScopeConfig:
|
|
269
|
+
"""The slice of the population one breeding event draws from."""
|
|
270
|
+
|
|
271
|
+
class Global(ScopeConfig):
|
|
272
|
+
"""Every individual is a candidate. Consumes no randomness."""
|
|
273
|
+
def __init__(self) -> None:
|
|
274
|
+
...
|
|
275
|
+
|
|
276
|
+
class RandomSubset(ScopeConfig):
|
|
277
|
+
"""`size` distinct individuals, drawn uniformly without replacement."""
|
|
278
|
+
def __init__(self, size: int) -> None:
|
|
279
|
+
...
|
|
280
|
+
@property
|
|
281
|
+
def size(self) -> int:
|
|
282
|
+
"""At least 1 and at most `population_size` — and at least 4 under steady-
|
|
283
|
+
state, which needs two parents and two distinct individuals for them to
|
|
284
|
+
replace. Generational has no such floor.
|
|
285
|
+
"""
|
|
286
|
+
...
|
|
287
|
+
|
|
288
|
+
class SelectionConfig:
|
|
289
|
+
"""Parent-selection strategy."""
|
|
290
|
+
|
|
291
|
+
class Best(SelectionConfig):
|
|
292
|
+
"""Takes the scope's fittest, in rank order.
|
|
293
|
+
|
|
294
|
+
Consumes no randomness — the scope did the drawing.
|
|
295
|
+
"""
|
|
296
|
+
def __init__(self) -> None:
|
|
297
|
+
...
|
|
298
|
+
|
|
299
|
+
class Tournament(SelectionConfig):
|
|
300
|
+
"""Draws `tournament_size` members of the scope with replacement and takes the
|
|
301
|
+
best, once per parent.
|
|
302
|
+
"""
|
|
303
|
+
def __init__(self, tournament_size: int) -> None:
|
|
304
|
+
...
|
|
305
|
+
@property
|
|
306
|
+
def tournament_size(self) -> int:
|
|
307
|
+
"""At least 1. May exceed the population — the floor is checked against the
|
|
308
|
+
scope's `size` instead.
|
|
309
|
+
"""
|
|
310
|
+
...
|
|
311
|
+
|
|
312
|
+
class CrossoverConfig:
|
|
313
|
+
"""Recombination operator."""
|
|
314
|
+
|
|
315
|
+
class TwoPoint(CrossoverConfig):
|
|
316
|
+
"""Two cut points, the middle segment exchanged. The only operator that
|
|
317
|
+
ships.
|
|
318
|
+
"""
|
|
319
|
+
def __init__(self) -> None:
|
|
320
|
+
...
|
|
321
|
+
|
|
322
|
+
class EdgeEditMutationConfig:
|
|
323
|
+
"""Which mutation an edge-edit genome applies."""
|
|
324
|
+
|
|
325
|
+
class RerollGene(EdgeEditMutationConfig):
|
|
326
|
+
"""Redraws one edit operation in the genome."""
|
|
327
|
+
def __init__(self) -> None:
|
|
328
|
+
...
|
|
329
|
+
|
|
330
|
+
class SdaMutationConfig:
|
|
331
|
+
"""Which mutation an SDA genome applies."""
|
|
332
|
+
|
|
333
|
+
class RedrawOne(SdaMutationConfig):
|
|
334
|
+
"""Redraws one element of the automaton — the initial character, a transition's
|
|
335
|
+
target, or its response.
|
|
336
|
+
"""
|
|
337
|
+
def __init__(self) -> None:
|
|
338
|
+
...
|
|
339
|
+
|
|
340
|
+
class GenomeConfig:
|
|
341
|
+
"""Genome representation and the dimensions used to build random individuals."""
|
|
342
|
+
|
|
343
|
+
class EdgeEdit(GenomeConfig):
|
|
344
|
+
"""A list of edit operations; the graph is what replaying them produces."""
|
|
345
|
+
def __init__(self, gene_length: int, operation_weights: OperationWeights | None=None, mutation: EdgeEditMutationConfig | None=None) -> None:
|
|
346
|
+
...
|
|
347
|
+
@property
|
|
348
|
+
def gene_length(self) -> int:
|
|
349
|
+
"""Number of edit operations in the genome."""
|
|
350
|
+
...
|
|
351
|
+
@property
|
|
352
|
+
def mutation(self) -> EdgeEditMutationConfig | None:
|
|
353
|
+
"""Which mutation the run applies; omitted, the default one."""
|
|
354
|
+
...
|
|
355
|
+
@property
|
|
356
|
+
def operation_weights(self) -> OperationWeights | None:
|
|
357
|
+
"""Relative weight per edit operation; omitted, every operation weighs
|
|
358
|
+
1.0.
|
|
359
|
+
"""
|
|
360
|
+
...
|
|
361
|
+
|
|
362
|
+
class Sda(GenomeConfig):
|
|
363
|
+
"""A self-driving automaton whose output is read as the graph's edges.
|
|
364
|
+
|
|
365
|
+
No `num_chars`: the alphabet is `max_edge_multiplicity + 1`, so every
|
|
366
|
+
character is a legal edge weight.
|
|
367
|
+
"""
|
|
368
|
+
def __init__(self, num_states: int, max_resp_len: int, init_state: int=0, init_char_mutation_rate: float | None=None, transition_vs_response_rate: float | None=None, mutation: SdaMutationConfig | None=None) -> None:
|
|
369
|
+
...
|
|
370
|
+
@property
|
|
371
|
+
def init_char_mutation_rate(self) -> float | None:
|
|
372
|
+
"""Chance a mutation redraws the initial character instead of touching the
|
|
373
|
+
transition table; omitted, the default rate.
|
|
374
|
+
"""
|
|
375
|
+
...
|
|
376
|
+
@property
|
|
377
|
+
def init_state(self) -> int:
|
|
378
|
+
"""State the automaton starts in. Must be less than `num_states`, or
|
|
379
|
+
expression panics.
|
|
380
|
+
"""
|
|
381
|
+
...
|
|
382
|
+
@property
|
|
383
|
+
def max_resp_len(self) -> int:
|
|
384
|
+
"""Longest response string a transition may emit.
|
|
385
|
+
|
|
386
|
+
Responses are drawn from `1..=max_resp_len` and are never empty, which is
|
|
387
|
+
what guarantees the automaton terminates.
|
|
388
|
+
"""
|
|
389
|
+
...
|
|
390
|
+
@property
|
|
391
|
+
def mutation(self) -> SdaMutationConfig | None:
|
|
392
|
+
"""Which mutation the run applies; omitted, the default one."""
|
|
393
|
+
...
|
|
394
|
+
@property
|
|
395
|
+
def num_states(self) -> int:
|
|
396
|
+
"""States in the automaton."""
|
|
397
|
+
...
|
|
398
|
+
@property
|
|
399
|
+
def transition_vs_response_rate(self) -> float | None:
|
|
400
|
+
"""Chance of redrawing a transition's target rather than its response, once
|
|
401
|
+
the initial character was not chosen.
|
|
402
|
+
"""
|
|
403
|
+
...
|
|
404
|
+
|
|
405
|
+
class FitnessConfig:
|
|
406
|
+
"""Fitness objective and its parameters.
|
|
407
|
+
|
|
408
|
+
The epidemic objectives read one simulation differently, so they share a
|
|
409
|
+
single `SirParams` block.
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
class EpiLength(FitnessConfig):
|
|
413
|
+
"""Timesteps to burn out. Maximized."""
|
|
414
|
+
def __init__(self, sir: SirParams) -> None:
|
|
415
|
+
...
|
|
416
|
+
@property
|
|
417
|
+
def sir(self) -> SirParams:
|
|
418
|
+
"""Epidemic sampling parameters, shared by the three epidemic objectives."""
|
|
419
|
+
...
|
|
420
|
+
|
|
421
|
+
class EpiProfMatch(FitnessConfig):
|
|
422
|
+
"""RMSE against a target profile. Minimized."""
|
|
423
|
+
def __init__(self, sir: SirParams, target_profile: list[float]) -> None:
|
|
424
|
+
...
|
|
425
|
+
@property
|
|
426
|
+
def sir(self) -> SirParams:
|
|
427
|
+
"""Epidemic sampling parameters, shared by the three epidemic objectives."""
|
|
428
|
+
...
|
|
429
|
+
@property
|
|
430
|
+
def target_profile(self) -> list[float]:
|
|
431
|
+
"""The profile the run is scored against, compared verbatim — nothing is
|
|
432
|
+
prepended to it and nothing is rescaled.
|
|
433
|
+
"""
|
|
434
|
+
...
|
|
435
|
+
|
|
436
|
+
class EpiSpread(FitnessConfig):
|
|
437
|
+
"""Total ever-infected. Maximized."""
|
|
438
|
+
def __init__(self, sir: SirParams) -> None:
|
|
439
|
+
...
|
|
440
|
+
@property
|
|
441
|
+
def sir(self) -> SirParams:
|
|
442
|
+
"""Epidemic sampling parameters, shared by the three epidemic objectives."""
|
|
443
|
+
...
|
|
444
|
+
|
|
445
|
+
class Python(FitnessConfig):
|
|
446
|
+
"""A Python callable registered before the run, via
|
|
447
|
+
`GraphEvolver.set_fitness_function`. Whether it is maximized or minimized is
|
|
448
|
+
declared at registration, not here.
|
|
449
|
+
"""
|
|
450
|
+
def __init__(self) -> None:
|
|
451
|
+
...
|
|
452
|
+
|
|
453
|
+
class StructMatch(FitnessConfig):
|
|
454
|
+
"""How closely a graph's structure matches a set of reference graphs.
|
|
455
|
+
|
|
456
|
+
Minimized; requires `max_edge_multiplicity = 1`.
|
|
457
|
+
"""
|
|
458
|
+
def __init__(self, reference_folder: str, degree_bins: int=50, clustering_bins: int=50, spectral_bins: int=50, degree_gamma: float=1.0, clustering_gamma: float=1.0, spectral_gamma: float=1.0, degree_weight: float=1.0, clustering_weight: float=1.0, spectral_weight: float=1.0, density_weight: float=1.0) -> None:
|
|
459
|
+
...
|
|
460
|
+
@property
|
|
461
|
+
def clustering_bins(self) -> int:
|
|
462
|
+
"""Histogram bins for the clustering statistics. More bins resolve finer
|
|
463
|
+
differences and need more reference graphs to fill them.
|
|
464
|
+
"""
|
|
465
|
+
...
|
|
466
|
+
@property
|
|
467
|
+
def clustering_gamma(self) -> float:
|
|
468
|
+
"""RBF bandwidth for the clustering statistics. Must be finite and greater
|
|
469
|
+
than zero — the kernel divides by it. Too large is the dangerous
|
|
470
|
+
direction: the kernel collapses to zero for every candidate, the whole
|
|
471
|
+
population scores about the same, and evolution stalls while appearing to
|
|
472
|
+
run normally.
|
|
473
|
+
"""
|
|
474
|
+
...
|
|
475
|
+
@property
|
|
476
|
+
def clustering_weight(self) -> float:
|
|
477
|
+
"""How much the clustering family counts. Finite and non-negative; the three
|
|
478
|
+
family weights cannot all be zero, which would score every candidate
|
|
479
|
+
identically.
|
|
480
|
+
"""
|
|
481
|
+
...
|
|
482
|
+
@property
|
|
483
|
+
def degree_bins(self) -> int:
|
|
484
|
+
"""Histogram bins for the degree statistics. More bins resolve finer
|
|
485
|
+
differences and need more reference graphs to fill them.
|
|
486
|
+
"""
|
|
487
|
+
...
|
|
488
|
+
@property
|
|
489
|
+
def degree_gamma(self) -> float:
|
|
490
|
+
"""RBF bandwidth for the degree statistics. Must be finite and greater than
|
|
491
|
+
zero — the kernel divides by it. Too large is the dangerous direction:
|
|
492
|
+
the kernel collapses to zero for every candidate, the whole population
|
|
493
|
+
scores about the same, and evolution stalls while appearing to run
|
|
494
|
+
normally.
|
|
495
|
+
"""
|
|
496
|
+
...
|
|
497
|
+
@property
|
|
498
|
+
def degree_weight(self) -> float:
|
|
499
|
+
"""How much the degree family counts. Finite and non-negative; the three
|
|
500
|
+
family weights cannot all be zero, which would score every candidate
|
|
501
|
+
identically.
|
|
502
|
+
"""
|
|
503
|
+
...
|
|
504
|
+
@property
|
|
505
|
+
def density_weight(self) -> float:
|
|
506
|
+
"""How much distance from the reference set's mean density counts. Zero
|
|
507
|
+
switches the penalty off.
|
|
508
|
+
"""
|
|
509
|
+
...
|
|
510
|
+
@property
|
|
511
|
+
def reference_folder(self) -> str:
|
|
512
|
+
"""Folder of reference graphs, one edge-list file each.
|
|
513
|
+
|
|
514
|
+
Not checked when the config is parsed — validation does no I/O, so a
|
|
515
|
+
missing or empty folder is reported when the run starts.
|
|
516
|
+
"""
|
|
517
|
+
...
|
|
518
|
+
@property
|
|
519
|
+
def spectral_bins(self) -> int:
|
|
520
|
+
"""Histogram bins for the spectral statistics. More bins resolve finer
|
|
521
|
+
differences and need more reference graphs to fill them.
|
|
522
|
+
"""
|
|
523
|
+
...
|
|
524
|
+
@property
|
|
525
|
+
def spectral_gamma(self) -> float:
|
|
526
|
+
"""RBF bandwidth for the spectral statistics. Must be finite and greater
|
|
527
|
+
than zero — the kernel divides by it. Too large is the dangerous
|
|
528
|
+
direction: the kernel collapses to zero for every candidate, the whole
|
|
529
|
+
population scores about the same, and evolution stalls while appearing to
|
|
530
|
+
run normally.
|
|
531
|
+
"""
|
|
532
|
+
...
|
|
533
|
+
@property
|
|
534
|
+
def spectral_weight(self) -> float:
|
|
535
|
+
"""How much the spectral family counts. Finite and non-negative; the three
|
|
536
|
+
family weights cannot all be zero, which would score every candidate
|
|
537
|
+
identically.
|
|
538
|
+
"""
|
|
539
|
+
...
|
|
540
|
+
|
|
541
|
+
class SirParams:
|
|
542
|
+
"""Epidemic sampling parameters, shared by the epidemic objectives.
|
|
543
|
+
|
|
544
|
+
Nothing is range-checked here; every field is checked when the config is
|
|
545
|
+
handed to an evolver.
|
|
546
|
+
"""
|
|
547
|
+
def __init__(self, infection_rate: float, num_epidemics: int, patient_zero: int | None=None, min_epidemic_length: int=3, max_epidemic_retries: int=5) -> None:
|
|
548
|
+
...
|
|
549
|
+
@property
|
|
550
|
+
def infection_rate(self) -> float:
|
|
551
|
+
"""Per-edge transmission probability per timestep."""
|
|
552
|
+
...
|
|
553
|
+
@property
|
|
554
|
+
def max_epidemic_retries(self) -> int:
|
|
555
|
+
"""Attempts before keeping whatever came out."""
|
|
556
|
+
...
|
|
557
|
+
@property
|
|
558
|
+
def min_epidemic_length(self) -> int:
|
|
559
|
+
"""Outbreaks shorter than this are re-rolled; 1 disables the re-roll."""
|
|
560
|
+
...
|
|
561
|
+
@property
|
|
562
|
+
def num_epidemics(self) -> int:
|
|
563
|
+
"""Outbreaks averaged per evaluation."""
|
|
564
|
+
...
|
|
565
|
+
@property
|
|
566
|
+
def patient_zero(self) -> int | None:
|
|
567
|
+
"""Pinned patient zero; left unset, a fresh node is drawn per epidemic."""
|
|
568
|
+
...
|
|
569
|
+
|
|
570
|
+
class OperationWeights:
|
|
571
|
+
"""Relative probability of each edge-edit operation.
|
|
572
|
+
|
|
573
|
+
Every weight defaults to 1.0, so the operations are equally likely; 0.0
|
|
574
|
+
disables an operation outright.
|
|
575
|
+
"""
|
|
576
|
+
def __init__(self, toggle: float=1.0, hop: float=1.0, add: float=1.0, delete: float=1.0, swap: float=1.0, local_toggle: float=1.0, local_add: float=1.0, local_delete: float=1.0, null: float=1.0) -> None:
|
|
577
|
+
...
|
|
578
|
+
|
|
579
|
+
class RunResult:
|
|
580
|
+
"""Everything one run produced.
|
|
581
|
+
|
|
582
|
+
Returned by `GraphEvolver.run`. The evolver keeps none of it, so it is
|
|
583
|
+
reusable across runs and never reports a previous one's numbers.
|
|
584
|
+
"""
|
|
585
|
+
@property
|
|
586
|
+
def best_edges(self) -> list[tuple[int, int, int]]:
|
|
587
|
+
"""The best individual's expressed network, as `(u, v, multiplicity)`."""
|
|
588
|
+
...
|
|
589
|
+
@property
|
|
590
|
+
def best_fitness(self) -> float:
|
|
591
|
+
"""Best of the **final** population, **as-measured** — the units and sign
|
|
592
|
+
your objective returned. Matches `history`'s last row, which a
|
|
593
|
+
stochastic objective may have scored worse than an earlier one.
|
|
594
|
+
"""
|
|
595
|
+
...
|
|
596
|
+
@property
|
|
597
|
+
def best_genome_repr(self) -> str:
|
|
598
|
+
"""The best individual's genome, via `Genome::print`."""
|
|
599
|
+
...
|
|
600
|
+
@property
|
|
601
|
+
def config_toml(self) -> str:
|
|
602
|
+
"""The TOML document this run's config was parsed from. `save_config`
|
|
603
|
+
writes it into a folder, so the run can be reproduced.
|
|
604
|
+
"""
|
|
605
|
+
...
|
|
606
|
+
@property
|
|
607
|
+
def history(self) -> list[GenerationStats]:
|
|
608
|
+
"""The convergence log, one row per logged iteration."""
|
|
609
|
+
...
|
|
610
|
+
@property
|
|
611
|
+
def num_nodes(self) -> int:
|
|
612
|
+
"""How many nodes that network has, isolated ones included."""
|
|
613
|
+
...
|
|
614
|
+
@property
|
|
615
|
+
def run_index(self) -> int:
|
|
616
|
+
"""Which replicate this is, `0`-based. With `seed`, the pair that
|
|
617
|
+
reproduces this exact run.
|
|
618
|
+
"""
|
|
619
|
+
...
|
|
620
|
+
def save_config(self, directory: str) -> None:
|
|
621
|
+
"""Write the run's config TOML into `directory` as `config.toml`.
|
|
622
|
+
|
|
623
|
+
Called once per invocation rather than once per replicate: every
|
|
624
|
+
replicate of one invocation was produced by the same document, and a
|
|
625
|
+
copy beside each would be the same bytes N times.
|
|
626
|
+
"""
|
|
627
|
+
...
|
|
628
|
+
def save_logs(self, filename: str) -> None:
|
|
629
|
+
"""Write the convergence log to `filename` as CSV.
|
|
630
|
+
|
|
631
|
+
Every row carries `seed` and `run_index`, so logs from several runs
|
|
632
|
+
concatenate into one file and stay separable.
|
|
633
|
+
"""
|
|
634
|
+
...
|
|
635
|
+
def save_results(self, filename: str) -> None:
|
|
636
|
+
"""Write the best individual to `filename`.
|
|
637
|
+
|
|
638
|
+
**The file is a loadable edge list**, which GET reads back unedited.
|
|
639
|
+
|
|
640
|
+
The config that produced it is not written here — it belongs to the
|
|
641
|
+
whole invocation rather than to one replicate, so `save_config` writes
|
|
642
|
+
it once into the folder the replicates share.
|
|
643
|
+
"""
|
|
644
|
+
...
|
|
645
|
+
@property
|
|
646
|
+
def seed(self) -> int:
|
|
647
|
+
"""The seed `run` was called with."""
|
|
648
|
+
...
|
|
649
|
+
|
|
650
|
+
class GenerationStats:
|
|
651
|
+
"""One row of the convergence log.
|
|
652
|
+
|
|
653
|
+
`iteration` counts generations under the generational strategy and mating
|
|
654
|
+
events under steady-state.
|
|
655
|
+
"""
|
|
656
|
+
@property
|
|
657
|
+
def best_fitness(self) -> float:
|
|
658
|
+
"""Best fitness in the population at this iteration."""
|
|
659
|
+
...
|
|
660
|
+
@property
|
|
661
|
+
def ci_95(self) -> float:
|
|
662
|
+
"""Half-width of the 95% confidence interval on `mean_fitness`, using the
|
|
663
|
+
**sample** deviation, dividing by `n - 1` — not `std_dev` beside it.
|
|
664
|
+
Zero for a population of one, never `NaN`.
|
|
665
|
+
"""
|
|
666
|
+
...
|
|
667
|
+
@property
|
|
668
|
+
def iteration(self) -> int:
|
|
669
|
+
"""Generation number, or mating-event number."""
|
|
670
|
+
...
|
|
671
|
+
@property
|
|
672
|
+
def mean_fitness(self) -> float:
|
|
673
|
+
"""Population mean fitness at this iteration."""
|
|
674
|
+
...
|
|
675
|
+
@property
|
|
676
|
+
def std_dev(self) -> float:
|
|
677
|
+
"""**Population** standard deviation, dividing by `n`. Zero for a
|
|
678
|
+
population of one.
|
|
679
|
+
"""
|
|
680
|
+
...
|
get/get.pyd
ADDED
|
Binary file
|