xero-bio-genesis / modules /vovina_evolution_factor.py
transmutationist's picture
XERO: card + code + docs (WIP; see STATUS_AND_AUDIT.md)
e9e9f83 verified
Raw History Blame
10.1 kB
"""
VOVINA ZEDEC PRO - Evolution Factor (variable, constructive mutation rate)
==========================================================================
Author: Michael Laurence Curzi
Company: ZEDEC AI / 36N9 Genetics LLC
License: MIT (Attribution Required)
THE EVOLUTION FACTOR
--------------------
Life never freezes completely. XERO must always be permitted *some* mutation so
it can keep adapting -- but the amount must rise and fall with how much the
environment actually *demands* adaptation, and it must only ever express itself
through **constructive** mutation types (never destructive runaway).
This module supplies exactly that controller:
rate(env) = clamp( baseline + (ceiling - baseline) * need(env)^gamma,
baseline, ceiling )
* **Baseline is always there.** `need == 0` still yields `rate == baseline > 0`,
so the organism never stops exploring entirely.
* **Variable with environmental need.** `need(env)` aggregates novelty, fitness
deficit, stagnation and homeostatic stress (damped by stability); more need ->
more mutation, up to a hard `ceiling` so adaptation can never go destructive.
* **Constructive types only.** The total rate is split across an allowlist of
constructive operators (substitution, insertion/duplication, a small capped
deletion). Destructive operators (nonsense, frameshifting large indels) are
never assigned a rate, and the replication engine independently rejects any
edit that breaks a gene's reading frame.
* **Immutable core protected.** `constructive_mutate_genome(...)` leaves any
chromosome named in `protected_modules` untouched, so identity is preserved
while the rest of the organism is free to adapt.
The controller is a pure function of its inputs (no internal RNG): the same
environment always yields the same rate, so evolution stays reproducible.
"""
from __future__ import annotations
import copy
from dataclasses import dataclass, field
from typing import Iterable
# ------------------------------------------------------------------
# Mutation taxonomy: which operators are constructive vs destructive.
# ------------------------------------------------------------------
CONSTRUCTIVE_TYPES = ("substitution", "insertion", "duplication", "trim_deletion")
DESTRUCTIVE_TYPES = ("nonsense", "frameshift", "large_deletion", "truncation")
# How the total rate is divided among constructive operators (sums to 1.0).
# Substitution dominates (explores sequence space at constant length);
# insertion/duplication add novel material (the classic engine of evolution);
# deletion is permitted only as a small, capped "trim" because it is the most
# likely constructive operator to tip into destruction.
CONSTRUCTIVE_MIX = {"substitution": 0.70, "insertion": 0.20, "trim_deletion": 0.10}
@dataclass(frozen=True)
class Environment:
"""Normalised (0..1) signals describing how much the environment is asking
the organism to adapt right now. All optional; defaults describe a calm,
well-adapted environment (need -> 0 -> rate -> baseline)."""
novelty: float = 0.0 # proportion of new vs already-known input
fitness_deficit: float = 0.0 # how far below the adapted target (1 = far)
stagnation: float = 0.0 # plateau pressure (no recent improvement)
stress: float = 0.0 # homeostatic stress (1 - homeostasis_index)
stability: float = 0.0 # coherence/negentropy; damps need
def clamped(self) -> "Environment":
c = lambda x: 0.0 if x < 0.0 else 1.0 if x > 1.0 else float(x)
return Environment(c(self.novelty), c(self.fitness_deficit),
c(self.stagnation), c(self.stress), c(self.stability))
@dataclass(frozen=True)
class EvolutionFactor:
"""Variable, constructive mutation-rate controller. The baseline is always
present; the adaptive component scales with environmental need."""
baseline: float = 1e-3 # always-on floor -- "the baseline always there"
ceiling: float = 5e-2 # hard cap -- adaptation can never run destructive
gamma: float = 1.0 # response curve (1=linear; >1 = conservative)
# weights for aggregating environmental need (need is clamped to [0,1])
w_novelty: float = 0.30
w_deficit: float = 0.30
w_stagnation:float = 0.20
w_stress: float = 0.20
w_stability: float = 0.30 # subtractive damping
mix: dict = field(default_factory=lambda: dict(CONSTRUCTIVE_MIX))
# ---- environmental need -> [0, 1] ----------------------------
def need(self, env: Environment) -> float:
e = env.clamped()
raw = (self.w_novelty * e.novelty
+ self.w_deficit * e.fitness_deficit
+ self.w_stagnation * e.stagnation
+ self.w_stress * e.stress
- self.w_stability * e.stability)
return 0.0 if raw < 0.0 else 1.0 if raw > 1.0 else raw
# ---- the variable rate (always >= baseline) ------------------
def rate(self, env: Environment) -> float:
n = self.need(env) ** self.gamma
r = self.baseline + (self.ceiling - self.baseline) * n
return self.baseline if r < self.baseline else self.ceiling if r > self.ceiling else r
def regime(self, env: Environment) -> str:
n = self.need(env)
if n <= 0.0: return "baseline"
if n < 0.34: return "exploring"
if n < 0.67: return "adapting"
return "max-adaptation"
# ---- constructive operator split -> engine kwargs ------------
def mutation_kwargs(self, env: Environment) -> dict:
"""Return {sub_rate, ins_rate, del_rate} for the replication engine,
dividing the variable rate across constructive operators only."""
r = self.rate(env)
return {
"sub_rate": r * self.mix.get("substitution", 0.0),
"ins_rate": r * self.mix.get("insertion", 0.0),
"del_rate": r * self.mix.get("trim_deletion", 0.0),
}
def snapshot(self, env: Environment) -> dict:
kw = self.mutation_kwargs(env)
return {
"evolution_factor": True,
"baseline": self.baseline,
"ceiling": self.ceiling,
"need": round(self.need(env), 4),
"rate": round(self.rate(env), 6),
"regime": self.regime(env),
"constructive_types": list(CONSTRUCTIVE_TYPES),
"excluded_destructive": list(DESTRUCTIVE_TYPES),
"mutation_kwargs": {k: round(v, 7) for k, v in kw.items()},
}
def default_factor() -> EvolutionFactor:
"""The organism's default evolution factor (sensible, conservative)."""
return EvolutionFactor()
def is_constructive(mutation_type: str) -> bool:
return mutation_type in CONSTRUCTIVE_TYPES
# ------------------------------------------------------------------
# Constructive, immutable-core-preserving genome mutation.
# ------------------------------------------------------------------
def constructive_mutate_genome(genome, ef: EvolutionFactor, env: Environment,
protected_modules: Iterable[str] = ()):
"""Mutate every chromosome at the factor's environment-adaptive constructive
rate, EXCEPT those whose name/module is in `protected_modules` (the immutable
core), which are copied through unchanged. The engine rejects any edit that
breaks a reading frame, so only constructive results survive."""
from vovina_digital_genome import Genome
from vovina_replication_engine import mutate_chromosome
protected = set(protected_modules)
kw = ef.mutation_kwargs(env)
new = Genome(organism_name=genome.organism_name + "_ef",
exotic_strand=list(getattr(genome, "exotic_strand", []) or []))
for chrom in genome.chromosomes:
if chrom.name in protected or getattr(chrom, "module_name", None) in protected:
new.chromosomes.append(copy.deepcopy(chrom)) # identity preserved
else:
new.chromosomes.append(mutate_chromosome(chrom, **kw))
return new
__all__ = [
"CONSTRUCTIVE_TYPES", "DESTRUCTIVE_TYPES", "CONSTRUCTIVE_MIX",
"Environment", "EvolutionFactor", "default_factor", "is_constructive",
"constructive_mutate_genome",
]
if __name__ == "__main__":
ef = default_factor()
print("=== EVOLUTION FACTOR ===")
print("constructive types :", CONSTRUCTIVE_TYPES)
print("excluded destructive:", DESTRUCTIVE_TYPES)
calm = Environment() # well-adapted
novel = Environment(novelty=0.8, fitness_deficit=0.3) # new territory
crisis = Environment(novelty=1.0, fitness_deficit=1.0, stagnation=1.0, stress=1.0)
stable = Environment(novelty=1.0, stability=1.0) # novel but very stable
print("\n=== BASELINE IS ALWAYS THERE ===")
for name, env in [("calm", calm), ("novel", novel), ("crisis", crisis), ("stable", stable)]:
r = ef.rate(env)
print(f" {name:7s}: need={ef.need(env):.3f} rate={r:.5f} regime={ef.regime(env)}")
assert r >= ef.baseline, "baseline floor violated"
assert ef.rate(calm) == ef.baseline, "calm env must sit exactly at baseline"
print("\n=== VARIABLE WITH ENVIRONMENTAL NEED (monotonic) ===")
rates = [ef.rate(Environment(fitness_deficit=d)) for d in (0.0, 0.25, 0.5, 0.75, 1.0)]
print(" deficit 0->1 rates:", [round(x, 5) for x in rates])
assert all(rates[i] <= rates[i + 1] for i in range(len(rates) - 1)), "rate must rise with need"
assert ef.rate(crisis) <= ef.ceiling, "ceiling cap violated"
print("\n=== STABILITY DAMPS NEED ===")
print(" novel rate :", round(ef.rate(novel), 5))
print(" stable rate:", round(ef.rate(stable), 5))
print("\n=== CONSTRUCTIVE SPLIT (crisis) ===")
print(" ", ef.mutation_kwargs(crisis))
print("\n=== DETERMINISM ===")
assert ef.snapshot(novel) == ef.snapshot(novel), "controller must be deterministic"
print(" identical snapshots for identical env: True")
print("\nALL EVOLUTION-FACTOR SELF-CHECKS PASSED")