""" 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")