"""Genes: (path, low, high) triples addressing values in a nested genome, and their [0, 1] coding for evolution.
A path is a sequence of keys into the genome's nested dicts, possibly followed by integer indices into an array value,
e.g. ("guidance", ("src", "dst"), "B", 0, 1) or ("migration", "region", "mean", 2). Genes whose first key is listed in
`integer_roots` are rounded to integers when set.
"""
from __future__ import annotations
import copy
import numpy as np
INTEGER_ROOTS = ("count", "excitability")
def _split(genome, path):
"""(parent dict, key, array indices) for a path: dict keys first, then integer indices into an array value."""
node, parent, key = genome, None, None
for k, p in enumerate(path):
if isinstance(node, dict):
parent, key, node = node, p, node[p]
else:
return parent, key, tuple(path[k:])
return parent, key, ()
[docs]
def get_gene(genome, path) -> float:
parent, key, idx = _split(genome, path)
v = parent[key]
return float(np.asarray(v, float)[idx]) if idx else float(v)
[docs]
def set_gene(genome, path, v):
"""Set a gene in place (array values are replaced by modified copies)."""
parent, key, idx = _split(genome, path)
if idx:
arr = np.array(parent[key], float)
arr[idx] = v
parent[key] = arr
else:
parent[key] = v
[docs]
def from_unit(genome, genes, u, integer_roots=INTEGER_ROOTS):
"""Copy of genome with gene k set to low_k + clip(u_k, 0, 1) (high_k - low_k)."""
g = copy.deepcopy(genome)
for (path, lo, hi), x in zip(genes, u):
v = lo + float(np.clip(x, 0, 1)) * (hi - lo)
if path[0] in integer_roots:
v = int(round(v))
set_gene(g, path, v)
return g
[docs]
def to_unit(genome, genes) -> np.ndarray:
"""Gene values of a genome scaled to [0, 1] (clipped)."""
return np.clip(np.asarray([(get_gene(genome, path) - lo) / (hi - lo) for path, lo, hi in genes]), 0, 1)
[docs]
def rebound(genes, new):
"""Gene list with some bounds replaced: new = {path: (low, high)}."""
return [(p, *new.get(p, (lo, hi))) for p, lo, hi in genes]