"""Shareable modules: a body plan, a genome, an interface and validation records.
A module is what one researcher evolves or designs (for example a retina whose ganglion cells show ON / OFF responses)
and another reuses as-is: inputs are the regions driven by external currents, outputs the regions read out. Modules
can be composed into a larger nervous system: region names get a prefix ("retina.ganglion"), each module keeps its own
genes, and links add new projections (with their own guidance genes) between modules.
"""
from __future__ import annotations
import copy
import json
from dataclasses import dataclass, field
from pathlib import Path
from ..development.grow import grow
from ..development.plan import BodyPlan, Count, FixedPlacement
from ..genome.io import from_json, to_json
REGION_ROOTS = ("count", "migration", "inhibitory", "excitability", "differentiation", "identity", "types", "mosaic", "tonic")
FORMAT = "neurogenesis-module/1"
[docs]
@dataclass
class Module:
name: str
plan: BodyPlan
genome: dict
inputs: list = field(default_factory=list)
outputs: list = field(default_factory=list)
description: str = ""
validation: dict = field(default_factory=dict) # metrics, seeds, pass fractions (how the module was tested)
provenance: dict = field(default_factory=dict) # authors, source repository, commit, date
neuron_model: str = "lif_int" # the neuron model the module was validated with
version: str = "0.1.0" # semantic version of the module
toolbox: str = "*" # compatible toolbox versions, e.g. ">=0.1, <0.2"
[docs]
def grow(self, seed=0, **kw):
return grow(self.plan, self.genome, seed, **kw)
[docs]
def to_dict(self):
return dict(format=FORMAT, name=self.name, description=self.description, plan=self.plan.to_dict(),
genome=to_json(self.genome), inputs=list(self.inputs), outputs=list(self.outputs),
validation=to_json(self.validation), provenance=to_json(self.provenance), neuron_model=self.neuron_model,
version=self.version, toolbox=self.toolbox)
[docs]
@classmethod
def from_dict(cls, d):
assert d.get("format") == FORMAT, f"not a {FORMAT} file"
return cls(name=d["name"], plan=BodyPlan.from_dict(d["plan"]), genome=from_json(d["genome"]), inputs=list(d["inputs"]),
outputs=list(d["outputs"]), description=d.get("description", ""), validation=d.get("validation", {}),
provenance=d.get("provenance", {}), neuron_model=d.get("neuron_model", "lif_int"),
version=d.get("version", "0.1.0"), toolbox=d.get("toolbox", "*"))
[docs]
def save(self, path):
Path(path).write_text(json.dumps(self.to_dict(), indent=1))
[docs]
@classmethod
def load(cls, path):
return cls.from_dict(json.loads(Path(path).read_text()))
def _prefix_genome(genome, plan, prefix):
"""Genome of a module with region names prefixed; per-module global genes moved into its projections; other
top-level genes (used by placements) renamed "prefix.key"."""
g = copy.deepcopy(genome)
out = {r: {} for r in REGION_ROOTS}
out["guidance"] = {}
names = {r.name for r in plan.regions}
for root in REGION_ROOTS:
for k, v in g.get(root, {}).items():
if k in names:
out[root][f"{prefix}.{k}"] = v
for (s, d), gd in g.get("guidance", {}).items():
if s in names and d in names:
gd = dict(gd)
gd.setdefault("wc", int(g.get("w_clip", 512)))
if "inh_weight_factor" in g:
gd.setdefault("inh", g["inh_weight_factor"])
out["guidance"][(f"{prefix}.{s}", f"{prefix}.{d}")] = gd
custom = {k: v for k, v in g.items() if k not in REGION_ROOTS + ("guidance", "w_clip", "inh_weight_factor")}
for k, v in custom.items():
out[f"{prefix}.{k}"] = v
return out, set(custom)
def _prefix_region(r, prefix, custom, offset):
r = copy.deepcopy(r)
r.name = f"{prefix}.{r.name}"
r.origin = tuple(float(o) + float(d) for o, d in zip(r.origin, offset))
if r.placement is not None and r.placement.counts is not None:
counts = []
for c in r.placement.counts:
c = Count(**c) if isinstance(c, dict) else copy.copy(c)
if c.gene is not None and c.gene[0] in custom:
c.gene = (f"{prefix}.{c.gene[0]}",) + tuple(c.gene[1:])
counts.append(c)
r.placement = FixedPlacement(labels=r.placement.labels, counts=counts, jitter=r.placement.jitter)
return r
[docs]
def compose(name, modules, links=(), offsets=None, inh_weight_factor=2.0, w_clip=512):
"""One plan and genome from several modules.
modules: {prefix: Module}. links: [(src, dst, guidance genes)] with prefixed region names ("retina.ganglion").
offsets: {prefix: (dx, dy, dz)} shifts a module's regions in space (this changes conduction delays of links only).
Returns a Module whose inputs and outputs are those of the parts, prefixed.
"""
regions, projections, genome, inputs, outputs, plugins = [], [], {r: {} for r in REGION_ROOTS}, [], [], []
genome["guidance"] = {}
rules = None
for prefix, m in modules.items():
gm, custom = _prefix_genome(m.genome, m.plan, prefix)
for root in REGION_ROOTS + ("guidance",):
genome[root].update(gm.pop(root))
genome.update(gm)
off = (offsets or {}).get(prefix, (0.0, 0.0, 0.0))
regions += [_prefix_region(r, prefix, custom, off) for r in m.plan.regions]
projections += [(f"{prefix}.{s}", f"{prefix}.{d}") for s, d in m.plan.projections]
inputs += [f"{prefix}.{r}" for r in m.inputs]
outputs += [f"{prefix}.{r}" for r in m.outputs]
rules = rules or {}
for phase, names in m.plan.rules.items(): # ordered union of every module's rules, per phase
rules[phase] = tuple(rules.get(phase, ())) + tuple(n for n in names if n not in rules.get(phase, ()))
plugins += [x for x in m.plan.plugins if x not in plugins]
for s, d, gd in links:
projections.append((s, d))
genome["guidance"][(s, d)] = dict(gd)
genome["inh_weight_factor"], genome["w_clip"] = inh_weight_factor, w_clip
genome = {k: v for k, v in genome.items() if v != {}}
plan = BodyPlan(name=name, regions=regions, projections=projections, rules=rules or {}, plugins=plugins)
return Module(name=name, plan=plan, genome=genome, inputs=inputs, outputs=outputs,
provenance=dict(composed_from={p: m.name for p, m in modules.items()}))