Tutorial 5 — Projects, schedules and stages (experimental)

Experimental

The stage library (roadmap phase C) is new and its API may still change. Every stage is a no-op with empty parameters, stages that write delays clip them to max_delay (default: the storage bound of 255 ticks) with a warning, and grow_axons raises an error when a projection has no guidance. Every stage’s parameters and known limits are listed in Stage, geometry and field reference.

A schedule is data

A schedule is an ordered list of stages. Each entry names an operator (a registered stage), its params (each a Param, see Core concepts), and optionally repeat and when.

from neurogenesis.stages.base import run_schedule

project = {
    "version": "1.0.0", "fields": {}, "plugins": [],
    "schedule": [
        {"name": "seed",   "operator": "seed_cells",  "params": {"N": 20, "jitter": 5.0}},
        {"name": "divide", "operator": "proliferate", "params": {"prob": 0.5, "jitter": 1.0}, "repeat": 3},
        {"name": "death",  "operator": "apoptosis",
         "params": {"prob": {"type": "Gene", "path": ["death", "p"], "lo": 0.0, "hi": 0.5}}},
    ],
}
genome = {"death": {"p": 0.2}}

checkpoints = run_schedule(project, genome, seed=0, checkpoints=True)
print([int(t.alive.sum()) for t in checkpoints])
[20, 29, 43, 65, 56]
  • seed_cells places 20 cells; proliferate runs three times (each cell divides with probability 0.5); apoptosis removes cells with a probability read from the genome — an evolvable gene.

  • checkpoints=True returns the tissue after every stage repetition, for previews and analysis.

  • Each repetition has its own random stream derived from (seed, stage name, repetition): inserting a stage does not change what the others draw.

../_images/development_stages.gif

One frame per checkpoint of a schedule: seed_cells (12 founders in a sphere) → proliferate ×5 (p = 0.6) → apoptosis (p = 0.15) → apoptosis by crowding → differentiate (every third clone inhibitory, triangles) → migrate ×3 (radial). Colour = clone. Generated by docs/_scripts/make_media.py.

A complete pipeline from JSON

Two regions seeded in boxes, divisions, a gene-regulatory step driven by a gradient, fates from the gradient, axons whose tuning width is a gene, myelination, activity-dependent refinement and pruning:

import json
import numpy as np
from neurogenesis.stages.base import run_schedule
from neurogenesis.spiking.batch import simulate_counts

gene = lambda path, lo, hi: {"type": "Gene", "path": path, "lo": lo, "hi": hi}
project = {
    "version": "1.0.0", "plugins": [],
    "fields": {"tissue": {"type": "Box", "center": [0, 0, 0], "size": [40, 40, 10]},
               "target": {"type": "Box", "center": [60, 0, 0], "size": [40, 40, 10]},
               "grad": {"type": "AnalyticField", "expr": "x / 40 + 0.5"}},
    "schedule": [
        {"name": "seed", "operator": "seed_cells", "params": {"N": 30, "geometry": "tissue", "region": 0}},
        {"name": "seed2", "operator": "seed_cells", "params": {"N": 30, "geometry": "target", "region": 1}},
        {"name": "divide", "operator": "proliferate", "params": {"prob": 0.5, "jitter": 1.0}, "repeat": 2},
        {"name": "express", "operator": "express",
         "params": {"K": 1, "morphogens": [{"field": {"type": "Field", "name": "grad"}, "weights": [1.0]}]}},
        {"name": "fate", "operator": "differentiate", "params": {"types": [
            {"type_id": 1, "condition": "field_grad < 0.5", "sign": 1},
            {"type_id": 2, "condition": "field_grad >= 0.5", "sign": -1}]}},
        {"name": "axons", "operator": "grow_axons", "params": {
            "projections": [[0, 1]],
            "guidance": {"0->1": {"B": [[1, 0, 0], [0, 1, 0], [0, 0, 0]], "c": [0, 0, 0],
                                  "sigma": gene(["g", "sigma"], 0.05, 0.5), "p0": 0.8, "w": 300}},
            "conduction_velocity": 2.0}},
        {"name": "myelin", "operator": "myelinate", "params": {"factor": 0.5}},
        {"name": "refine", "operator": "refine_by_activity", "params": {"ticks": 100}},
        {"name": "prune", "operator": "prune", "params": {"threshold": 1}},
    ]}
project = json.loads(json.dumps(project))          # a project is plain JSON
tissue = run_schedule(project, genome={"g": {"sigma": 0.2}}, seed=0)
net = tissue.to_network()
ext = np.zeros((1, net.num_neurons), np.int32); ext[0, :5] = 100
for model in ("lif_int", "two_compartment", "hh"):
    print(model, int(simulate_counts([net], [ext], ticks=100, model=model)[0].sum()))
lif_int 203
two_compartment 205
hh 27

The network has 131 cells and 319 synapses (166 inhibitory, from the type-2 cells), with delays of 10–22 ticks after myelination. In this run refine_by_activity has no rule, so it leaves the weights unchanged.

Genes of a schedule

from neurogenesis.params import collect_genes
print(collect_genes(project["schedule"]))      # [(path, low, high, integer), ...] — ready for evolution

Cost of a schedule

from neurogenesis.stages.base import schedule_cost
print(schedule_cost(project, N=10_000, E=1_000_000))    # (bytes, operations), declared by each stage

Available stages

Operator

Task

What it does

seed_cells

C1

create cells at a point, in a geometry, with a region and type

proliferate

C2

cell division with a probability; daughters inherit lineage

apoptosis

C3

cell death: by probability, density, or missing inputs / outputs

migrate

C4

move cells (direction, radial, towards a target; stop field, boundary)

sort

C5

reorder cells along an axis by a key

express

C6

gene-regulatory step on expression channels (morphogens, neighbours)

differentiate

C7

assign cell types and neuron parameters from conditions

grow_axons

C8

connections by axon guidance

place_by_hand, connect_by_hand

C9

explicit cell positions and synapses (supervised)

prune, scale, myelinate

C10, C12

remove weak synapses, scale weights, shorten delays

refine_by_activity

C11

simulate inside development and apply a plasticity rule

legacy_grow_stage

A5

the original grow procedure as one stage

Every stage’s parameters, defaults and limits: Stage, geometry and field reference (also through the bridge stages method).

Legacy plans as schedules

Any BodyPlan can be compiled into a Project whose schedule reproduces grow() bit for bit:

from neurogenesis.development.legacy_compiler import compile_plan_to_project
project = compile_plan_to_project(plan)
tissue = run_schedule(project, genome, seed=0, legacy_stream=True)
net = tissue.to_network()

legacy_stream=True exists only for legacy plans (one random stream shared by all stages, as in grow); new schedules never use it.

PLACEHOLDER

A full worked example — a layered sheet grown from progenitors (seed → proliferate → migrate into layers → differentiate by layer → grow axons → refine by spontaneous waves → prune) — once the fix wave is merged (roadmap milestone M2, example G2.3), with a GIF of every stage.