Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions docs/decisions/0006-genetics-and-inheritance.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ The roadmap (3.4) makes the life-stage ages genome-controlled; Phase 3 had to pl

### Founders and the config baseline

Founders get **identical** genomes built from the existing config values: `organisms.*` and `reproduction.*` keep their meaning as the founder baseline, which keeps every Phase 3 default meaningful. Brain weights are drawn from the `genetics` stream (Gaussian, std `genetics.brain.initialWeightStd`), one founder at a time in id order. Variation enters the population only at births.
Each gene's effective range is its default range with any `genetics.genes` override applied; a default edge that excludes the founder baseline is **widened** to include it, so changing a baseline in `organisms.*` or `reproduction.*` never needs a matching range override (an edge set explicitly by an override is never moved, and config validation rejects a baseline outside it). Founders get **identical** genomes built from the existing config values: `organisms.*` and `reproduction.*` keep their meaning as the founder baseline, which keeps every Phase 3 default meaningful. Brain weights are drawn from the `genetics` stream (Gaussian, std `genetics.brain.initialWeightStd`), one founder at a time in id order. Variation enters the population only at births.

### Capability is not free: derived quantities

Expand All @@ -49,6 +49,8 @@ parent pays = offspringInvestment * (1 + reproduction.overheadFraction)
child energy = min(investmentA + investmentB, child maxEnergy)
```

Because genes vary independently, a parent could in principle hold a `minEnergy` below the cost it would pay. `ReproductionSystem` therefore requires `energy >= max(minEnergy, parent pays)` to attempt, so paying can never drive energy negative.

The metabolism formula from ADR 0005 is otherwise unchanged and reads the organism's own `movementCost`, `turningCost` and derived terms. `movementCost` and `turningCost` are already coupled to _realized_ speed and turn (they multiply `speed^2` and `lastTurnMagnitude^2`), so a faster or more agile organism pays more when it actually moves.

This **replaces** the config fields `reproduction.energyCost` and `reproduction.offspringEnergy` with `reproduction.offspringInvestment` (the per-parent baseline) and `reproduction.overheadFraction` (energy lost in the transfer). With investment `12.5` and overhead `0.6`, a parent pays `20` and the child receives `25`, exactly Phase 3's values. There are no external users of the old fields, so no compatibility shim is kept.
Expand All @@ -62,7 +64,7 @@ This **replaces** the config fields `reproduction.energyCost` and `reproduction.
- **bit-flip** (booleans).
- **circular** (`hue`): Gaussian in `[0, 1)` turns, wrapped.
- **trait-shift** (correlated traits): genes sharing a `correlationGroup` receive one shared normalized delta, scaled into each gene's own range. The mechanism is required by the roadmap ("where appropriate"); the production table declares no group yet, and tests exercise it with a synthetic table.
- **learning-parameter**: `learningRate` mutates in log space; the other learning genes are bounded Gaussians within `[0, 1]`.
- **learning-parameter**: `learningRate` mutates multiplicatively (a Gaussian fraction of its current value, so it stays positive and scales sensibly; the engine has no deterministic `exp` to mutate in log space); the other learning genes are bounded Gaussians within `[0, 1]`.
- **brain-weight**: each weight mutates independently with probability `genetics.brain.weightMutationRate` by `N(0, weightMutationStd)`.

Global knobs: `genetics.mutationRateScale` multiplies every rate and `genetics.mutationStrengthScale` multiplies every std; `0` disables mutation entirely. Integer, boolean and circular behavior is proven with synthetic spec tables because the production table has no integer or boolean gene.
Expand All @@ -83,11 +85,11 @@ The two partners are symmetric, so "mother" is the parent with the lower organis
- neural weights: **per-block single-point crossover** over each documented weight block (input-hidden, memory-hidden, and so on), with a random choice of which parent supplies the prefix. Every block therefore contains weights from both parents, so both brains actually contribute (roadmap 4.4);
- then mutation is applied to the recombined result.

Every gene consumes a fixed number of draws per birth regardless of configuration: the mutation gate draw is always taken and merely not applied when it fails, so changing a rate never shifts later draws within the stream. All of this draws from the `genetics` stream (reserved since Phase 2). Child spawn jitter and heading stay on the `reproduction` stream, so existing reproduction-stream sequences are untouched.
Each independent gene (or correlated group) consumes the same draws per birth whatever the configuration: the mutation gate, value and direction draws are always taken and merely not applied when the gate fails, so changing a rate or strength never shifts later draws within the stream. All of this draws from the `genetics` stream (reserved since Phase 2). Child spawn jitter and heading stay on the `reproduction` stream, so existing reproduction-stream sequences are untouched.

### Constraints

`validateGenome(genome, specs): ValidationResult` checks: every scalar gene within `[min, max]`; integers are integers; booleans are 0/1; brain architecture equals the configured architecture and the weight count equals the architecture's weight count (all finite); `learningRate >= 0`; `discountFactor` in `[0, 1]`; `offspringInvestment > 0`; and the ordering `maturityAge < senescenceAge < maxAge`. The default ranges of those three genes **do not overlap**, so the ordering holds by construction for any recombination or mutation; config validation rejects overlapping ranges. `repairGenome(genome, specs): Genome` is a pure clamp provided for tooling, but `ReproductionSystem` never calls it (roadmap 4.5: prefer rejecting invalid genomes in development). `World.validate()` rejects any living organism whose genome is invalid (roadmap: "No invalid genome").
`validateGenome(genome, specs): ValidationResult` checks: every scalar gene within `[min, max]`; integers are integers; booleans are 0/1; brain architecture equals the configured architecture and the weight count equals the architecture's weight count (all finite); `learningRate >= 0`; `discountFactor` in `[0, 1]`; `offspringInvestment > 0`; and the ordering `maturityAge < senescenceAge < maxAge`. The ordering is enforced by the operators, not by keeping ranges apart: a gene declared `orderedAfter` another (`senescenceAge` after `maturityAge`, `maxAge` after `senescenceAge`) has its lower bound raised above the earlier gene (by a 1e-6 margin) in mutation, recombination and repair, so the ages stay ordered for any ranges, including after an unrelated change moves the earlier gene. Config validation requires each such gene's range maximum to exceed the earlier gene's, so there is always room. `repairGenome(genome, specs): Genome` is a pure clamp provided for tooling, but `ReproductionSystem` never calls it (roadmap 4.5: prefer rejecting invalid genomes in development). `World.validate()` rejects any living organism whose genome is invalid (roadmap: "No invalid genome").

### Comparison

Expand Down
59 changes: 31 additions & 28 deletions docs/genetics/genome.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,35 @@ All defaults are provisional. They are tuned by the engine's scenario tests, and

Founders start at the **baseline** (the existing `organisms.*` / `reproduction.*` config values). A gene's **range** bounds every genome ever produced. `rate` is the probability that the gene mutates in a birth and `std` is the mutation standard deviation as a fraction of the range width, both before the global `mutationRateScale` / `mutationStrengthScale`.

| Gene | Kind | Baseline (config source) | Range | Rate | Std | Read by |
| ---------------------------------- | ----------- | ----------------------------------------- | ------------- | ---- | ---- | ------------------------------------------------------------ |
| `physical.radius` | float | 5 (`organisms.radius`) | 2.5 - 10 | 0.1 | 0.05 | Interaction (capture radius), size-derived costs, snapshot |
| `physical.maxSpeed` | float | 20 (`organisms.maxSpeed`) | 10 - 40 | 0.1 | 0.05 | Movement |
| `physical.maxTurnRate` | float | 3 (`organisms.maxTurnRate`) | 1.5 - 6 | 0.1 | 0.05 | Movement |
| `physical.maturityAge` | float | 90 (`organisms.maturityAge`) | 45 - 180 | 0.1 | 0.05 | life stage (Movement, Interaction, Metabolism, Reproduction) |
| `physical.senescenceAge` | float | 510 (`organisms.senescenceAge`) | 255 - 560 | 0.1 | 0.05 | life stage |
| `physical.maxAge` | float | 600 (`organisms.maxAge`) | 570 - 1200 | 0.1 | 0.05 | Death |
| `physical.hue` | circular | 0.55 (`organisms.hue`) | [0, 1) turns | 0.1 | 0.05 | carried (rendering, Phase 7) |
| `sensory.sensorRadius` | float | 60 (`organisms.sensorRadius`) | 30 - 120 | 0.1 | 0.05 | Perception; sensor cost |
| `metabolic.basalCost` | float | 0.5 (`organisms.basalCost`) | 0.3 - 0.8 | 0.1 | 0.05 | Metabolism (scaled by body size) |
| `metabolic.movementCost` | float | 0.005 (`organisms.movementCost`) | 0.003 - 0.008 | 0.1 | 0.05 | Metabolism |
| `metabolic.turningCost` | float | 0.05 (`organisms.turningCost`) | 0.03 - 0.08 | 0.1 | 0.05 | Metabolism |
| `behavioral.wanderJitter` | float | 1.5 (`organisms.wanderJitter`) | 0.75 - 3 | 0.1 | 0.05 | Decision |
| `reproduction.minEnergy` | float | 60 (`reproduction.minEnergy`) | 40 - 90 | 0.1 | 0.05 | Reproduction (eligibility) |
| `reproduction.offspringInvestment` | float | 12.5 (`reproduction.offspringInvestment`) | 6 - 25 | 0.1 | 0.05 | Reproduction (energy given to the child) |
| `reproduction.cooldown` | float | 30 (`reproduction.cooldown`) | 15 - 60 | 0.1 | 0.05 | Reproduction |
| `reproduction.searchRadius` | float | 50 (`reproduction.searchRadius`) | 25 - 100 | 0.1 | 0.05 | Reproduction (partner search) |
| `learning.learningRate` | float (log) | 0.05 (`genetics.learning`) | 0.001 - 0.5 | 0.1 | 0.05 | carried (Phase 5) |
| `learning.discountFactor` | float | 0.95 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.eligibilityTraceDecay` | float | 0.9 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.explorationRate` | float | 0.1 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.riskTolerance` | float | 0.5 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.noveltyPreference` | float | 0.5 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.persistence` | float | 0.5 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |

The ranges of `maturityAge`, `senescenceAge` and `maxAge` are disjoint and ordered so that `maturityAge < senescenceAge < maxAge` holds for every genome by construction. Config validation rejects overrides that make them overlap.
| Gene | Kind | Baseline (config source) | Range | Rate | Std | Read by |
| ---------------------------------- | ---------------- | ----------------------------------------- | ------------- | ---- | ---- | ------------------------------------------------------------ |
| `physical.radius` | float | 5 (`organisms.radius`) | 2.5 - 10 | 0.1 | 0.05 | Interaction (capture radius), size-derived costs, snapshot |
| `physical.maxSpeed` | float | 20 (`organisms.maxSpeed`) | 10 - 40 | 0.1 | 0.05 | Movement |
| `physical.maxTurnRate` | float | 3 (`organisms.maxTurnRate`) | 1.5 - 6 | 0.1 | 0.05 | Movement |
| `physical.maturityAge` | float | 90 (`organisms.maturityAge`) | 45 - 180 | 0.1 | 0.05 | life stage (Movement, Interaction, Metabolism, Reproduction) |
| `physical.senescenceAge` | float | 510 (`organisms.senescenceAge`) | 255 - 560 | 0.1 | 0.05 | life stage |
| `physical.maxAge` | float | 600 (`organisms.maxAge`) | 570 - 1200 | 0.1 | 0.05 | Death |
| `physical.hue` | circular | 0.55 (`organisms.hue`) | [0, 1) turns | 0.1 | 0.05 | carried (rendering, Phase 7) |
| `sensory.sensorRadius` | float | 60 (`organisms.sensorRadius`) | 30 - 120 | 0.1 | 0.05 | Perception; sensor cost |
| `metabolic.basalCost` | float | 0.5 (`organisms.basalCost`) | 0.3 - 0.8 | 0.1 | 0.05 | Metabolism (scaled by body size) |
| `metabolic.movementCost` | float | 0.005 (`organisms.movementCost`) | 0.003 - 0.008 | 0.1 | 0.05 | Metabolism |
| `metabolic.turningCost` | float | 0.05 (`organisms.turningCost`) | 0.03 - 0.08 | 0.1 | 0.05 | Metabolism |
| `behavioral.wanderJitter` | float | 1.5 (`organisms.wanderJitter`) | 0.75 - 3 | 0.1 | 0.05 | Decision |
| `reproduction.minEnergy` | float | 60 (`reproduction.minEnergy`) | 40 - 90 | 0.1 | 0.05 | Reproduction (eligibility) |
| `reproduction.offspringInvestment` | float | 12.5 (`reproduction.offspringInvestment`) | 6 - 25 | 0.1 | 0.05 | Reproduction (energy given to the child) |
| `reproduction.cooldown` | float | 30 (`reproduction.cooldown`) | 15 - 60 | 0.1 | 0.05 | Reproduction |
| `reproduction.searchRadius` | float | 50 (`reproduction.searchRadius`) | 25 - 100 | 0.1 | 0.05 | Reproduction (partner search) |
| `learning.learningRate` | float (relative) | 0.05 (`genetics.learning`) | 0.001 - 0.5 | 0.1 | 0.05 | carried (Phase 5) |
| `learning.discountFactor` | float | 0.95 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.eligibilityTraceDecay` | float | 0.9 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.explorationRate` | float | 0.1 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.riskTolerance` | float | 0.5 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.noveltyPreference` | float | 0.5 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |
| `learning.persistence` | float | 0.5 (`genetics.learning`) | 0 - 1 | 0.1 | 0.05 | carried |

The ordering `maturityAge < senescenceAge < maxAge` is enforced by the operators: `senescenceAge` is declared `orderedAfter` `maturityAge`, and `maxAge` after `senescenceAge`, and mutation, recombination and repair raise such a gene's lower bound above the earlier gene (by a 1e-6 margin) even when the later gene itself did not mutate. Config validation requires each ordered gene's range maximum to exceed the earlier gene's, so there is always room. The default ranges happen not to overlap, but nothing relies on that.

If a default range edge excludes a changed founder baseline (for example `organisms.maxSpeed: 0`), that edge is widened to include it. An edge set explicitly in `genetics.genes` is never moved; config validation rejects a baseline outside it. For a relative gene (`learningRate`) the standard deviation is a fraction of the current value rather than of the range width.

Gene order in `GENE_SPECS` is fixed (the order above) and is the order used for mutation draws, the state hash and `genomeDistance`.

Expand All @@ -49,6 +51,7 @@ basal cost = metabolic.basalCost * sizeFactor
sensor cost = baseSensorCost * (sensorRadius / baseSensorRadius) (base = organisms.sensorCost)
parent pays = offspringInvestment * (1 + reproduction.overheadFraction)
child energy = min(investmentA + investmentB, child maxEnergy)
eligibility = energy >= max(minEnergy, parent pays) (so paying never drives energy negative)
```

At the baseline genome each quantity equals its Phase 3 value, which is what lets the behavior-preserving PR prove it changes nothing. Juvenile multipliers (`juvenileSizeScale`, `juvenileSpeedScale`, `juvenileMetabolicScale`) remain world-level config and apply on top.
Expand Down Expand Up @@ -82,7 +85,7 @@ Default total: 249 weights. Crossover operates per block (see below). No Phase 4
| boolean | bit-flip |
| circular (`hue`) | gaussian in turns, wrapped into `[0, 1)` |
| correlated group | trait-shift: one shared normalized delta applied to every gene in the group, scaled to each range |
| `learning.learningRate` | gaussian in log space, clamp |
| `learning.learningRate` | relative gaussian: add `N(0, std) * current value`, clamp |
| brain weight | each weight independently with probability `weightMutationRate`: add `N(0, weightMutationStd)` |

**Recombination** (`genetics/Recombination.ts`): `mother` is the parent with the lower organism id.
Expand Down
Loading
Loading