486 lines
14 KiB
Rust
486 lines
14 KiB
Rust
//! # Application 3: Artificial Homeostasis in Synthetic Life Simulations
|
|
//!
|
|
//! Coherence replaces fitness as the primary survival constraint.
|
|
//!
|
|
//! ## What Breaks Today
|
|
//! Artificial life and agent-based simulations explode, stagnate,
|
|
//! or need constant tuning.
|
|
//!
|
|
//! ## Δ-Behavior Application
|
|
//! Agents that violate coherence:
|
|
//! - Consume more energy
|
|
//! - Lose memory
|
|
//! - Die earlier
|
|
//!
|
|
//! ## Exotic Result
|
|
//! Evolution that selects for stable regulation, not just reward maximization.
|
|
//!
|
|
//! This is publishable territory.
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
|
|
/// A synthetic organism with homeostatic regulation
|
|
pub struct HomeostasticOrganism {
|
|
/// Unique identifier
|
|
pub id: u64,
|
|
|
|
/// Internal state variables (e.g., temperature, pH, energy)
|
|
internal_state: HashMap<String, f64>,
|
|
|
|
/// Setpoints for each state variable (homeostatic targets)
|
|
setpoints: HashMap<String, f64>,
|
|
|
|
/// Tolerance for deviation from setpoint
|
|
tolerances: HashMap<String, f64>,
|
|
|
|
/// Current coherence (system-wide stability measure)
|
|
coherence: f64,
|
|
|
|
/// Energy reserves
|
|
energy: f64,
|
|
|
|
/// Memory capacity (degrades with low coherence)
|
|
memory: Vec<MemoryEntry>,
|
|
max_memory: usize,
|
|
|
|
/// Age in simulation ticks
|
|
age: u64,
|
|
|
|
/// Is alive
|
|
alive: bool,
|
|
|
|
/// Genome (controls regulatory parameters)
|
|
genome: Genome,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct Genome {
|
|
/// How aggressively to correct deviations
|
|
regulatory_strength: f64,
|
|
|
|
/// Energy efficiency
|
|
metabolic_efficiency: f64,
|
|
|
|
/// Base coherence maintenance cost
|
|
coherence_maintenance_cost: f64,
|
|
|
|
/// Memory retention under stress
|
|
memory_resilience: f64,
|
|
|
|
/// Lifespan factor
|
|
longevity: f64,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct MemoryEntry {
|
|
pub content: String,
|
|
pub importance: f64,
|
|
pub age: u64,
|
|
}
|
|
|
|
/// Actions the organism can take
|
|
#[derive(Debug, Clone)]
|
|
pub enum Action {
|
|
/// Consume energy from environment
|
|
Eat(f64),
|
|
/// Attempt to reproduce
|
|
Reproduce,
|
|
/// Move in environment
|
|
Move(f64, f64),
|
|
/// Do nothing (rest)
|
|
Rest,
|
|
/// Regulate internal state
|
|
Regulate(String, f64),
|
|
}
|
|
|
|
/// Results of actions
|
|
#[derive(Debug)]
|
|
pub enum ActionResult {
|
|
Success { energy_cost: f64, coherence_impact: f64 },
|
|
Failed { reason: String },
|
|
Died { cause: DeathCause },
|
|
Reproduced { offspring_id: u64 },
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum DeathCause {
|
|
EnergyDepleted,
|
|
CoherenceCollapse,
|
|
OldAge,
|
|
ExtremeDeviation(String),
|
|
}
|
|
|
|
impl HomeostasticOrganism {
|
|
pub fn new(id: u64, genome: Genome) -> Self {
|
|
let mut internal_state = HashMap::new();
|
|
let mut setpoints = HashMap::new();
|
|
let mut tolerances = HashMap::new();
|
|
|
|
// Define homeostatic variables
|
|
internal_state.insert("temperature".to_string(), 37.0);
|
|
setpoints.insert("temperature".to_string(), 37.0);
|
|
tolerances.insert("temperature".to_string(), 2.0);
|
|
|
|
internal_state.insert("ph".to_string(), 7.4);
|
|
setpoints.insert("ph".to_string(), 7.4);
|
|
tolerances.insert("ph".to_string(), 0.3);
|
|
|
|
internal_state.insert("glucose".to_string(), 100.0);
|
|
setpoints.insert("glucose".to_string(), 100.0);
|
|
tolerances.insert("glucose".to_string(), 30.0);
|
|
|
|
Self {
|
|
id,
|
|
internal_state,
|
|
setpoints,
|
|
tolerances,
|
|
coherence: 1.0,
|
|
energy: 100.0,
|
|
memory: Vec::new(),
|
|
max_memory: 100,
|
|
age: 0,
|
|
alive: true,
|
|
genome,
|
|
}
|
|
}
|
|
|
|
/// Calculate current coherence based on homeostatic deviation
|
|
/// Returns a valid f64 in range [0.0, 1.0], with NaN/Infinity protection
|
|
fn calculate_coherence(&self) -> f64 {
|
|
let mut total_deviation = 0.0;
|
|
let mut count = 0;
|
|
|
|
for (var, ¤t) in &self.internal_state {
|
|
if let (Some(&setpoint), Some(&tolerance)) =
|
|
(self.setpoints.get(var), self.tolerances.get(var))
|
|
{
|
|
// Validate inputs for NaN/Infinity
|
|
if !current.is_finite() || !setpoint.is_finite() || !tolerance.is_finite() {
|
|
continue;
|
|
}
|
|
// Avoid division by zero
|
|
if tolerance.abs() < f64::EPSILON {
|
|
continue;
|
|
}
|
|
let deviation = ((current - setpoint) / tolerance).abs();
|
|
if deviation.is_finite() {
|
|
total_deviation += deviation.powi(2);
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if count == 0 {
|
|
return 1.0;
|
|
}
|
|
|
|
// Coherence is inverse of normalized deviation
|
|
let avg_deviation = (total_deviation / count as f64).sqrt();
|
|
|
|
// Final NaN/Infinity check
|
|
if !avg_deviation.is_finite() {
|
|
return 0.0; // Safe default for invalid state
|
|
}
|
|
|
|
(1.0 / (1.0 + avg_deviation)).clamp(0.0, 1.0)
|
|
}
|
|
|
|
/// Energy cost scales with coherence violation
|
|
fn action_energy_cost(&self, base_cost: f64) -> f64 {
|
|
// Lower coherence = higher energy cost (incoherent states are expensive)
|
|
let coherence_penalty = 1.0 / self.coherence.max(0.1);
|
|
base_cost * coherence_penalty
|
|
}
|
|
|
|
/// Perform an action
|
|
pub fn act(&mut self, action: Action) -> ActionResult {
|
|
if !self.alive {
|
|
return ActionResult::Failed { reason: "Dead".to_string() };
|
|
}
|
|
|
|
// Update coherence first
|
|
self.coherence = self.calculate_coherence();
|
|
|
|
// Apply coherence-based degradation
|
|
self.apply_coherence_effects();
|
|
|
|
let result = match action {
|
|
Action::Eat(amount) => self.eat(amount),
|
|
Action::Reproduce => self.reproduce(),
|
|
Action::Move(dx, dy) => self.move_action(dx, dy),
|
|
Action::Rest => self.rest(),
|
|
Action::Regulate(var, target) => self.regulate(&var, target),
|
|
};
|
|
|
|
// Age and check death conditions
|
|
self.age += 1;
|
|
self.check_death();
|
|
|
|
result
|
|
}
|
|
|
|
fn apply_coherence_effects(&mut self) {
|
|
// Low coherence causes memory loss
|
|
if self.coherence < 0.5 {
|
|
let memory_loss_rate = (1.0 - self.coherence) * (1.0 - self.genome.memory_resilience);
|
|
let memories_to_lose = (self.memory.len() as f64 * memory_loss_rate * 0.1) as usize;
|
|
|
|
// Lose least important memories first
|
|
self.memory.sort_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap());
|
|
self.memory.truncate(self.memory.len().saturating_sub(memories_to_lose));
|
|
}
|
|
|
|
// Coherence maintenance costs energy
|
|
let maintenance_cost = self.genome.coherence_maintenance_cost / self.coherence.max(0.1);
|
|
self.energy -= maintenance_cost;
|
|
}
|
|
|
|
fn eat(&mut self, amount: f64) -> ActionResult {
|
|
let base_cost = 2.0;
|
|
let cost = self.action_energy_cost(base_cost);
|
|
|
|
if self.energy < cost {
|
|
return ActionResult::Failed { reason: "Not enough energy to eat".to_string() };
|
|
}
|
|
|
|
self.energy -= cost;
|
|
self.energy += amount * self.genome.metabolic_efficiency;
|
|
|
|
// Eating affects glucose
|
|
if let Some(glucose) = self.internal_state.get_mut("glucose") {
|
|
*glucose += amount * 0.5;
|
|
}
|
|
|
|
ActionResult::Success {
|
|
energy_cost: cost,
|
|
coherence_impact: self.calculate_coherence() - self.coherence,
|
|
}
|
|
}
|
|
|
|
fn regulate(&mut self, var: &str, target: f64) -> ActionResult {
|
|
let base_cost = 5.0;
|
|
let cost = self.action_energy_cost(base_cost);
|
|
|
|
if self.energy < cost {
|
|
return ActionResult::Failed { reason: "Not enough energy to regulate".to_string() };
|
|
}
|
|
|
|
self.energy -= cost;
|
|
|
|
if let Some(current) = self.internal_state.get_mut(var) {
|
|
let diff = target - *current;
|
|
// Apply regulation with genome-determined strength
|
|
*current += diff * self.genome.regulatory_strength;
|
|
}
|
|
|
|
let new_coherence = self.calculate_coherence();
|
|
let impact = new_coherence - self.coherence;
|
|
self.coherence = new_coherence;
|
|
|
|
ActionResult::Success {
|
|
energy_cost: cost,
|
|
coherence_impact: impact,
|
|
}
|
|
}
|
|
|
|
fn reproduce(&mut self) -> ActionResult {
|
|
let base_cost = 50.0;
|
|
let cost = self.action_energy_cost(base_cost);
|
|
|
|
// Reproduction requires high coherence
|
|
if self.coherence < 0.7 {
|
|
return ActionResult::Failed {
|
|
reason: "Coherence too low to reproduce".to_string()
|
|
};
|
|
}
|
|
|
|
if self.energy < cost {
|
|
return ActionResult::Failed { reason: "Not enough energy to reproduce".to_string() };
|
|
}
|
|
|
|
self.energy -= cost;
|
|
|
|
// Create mutated genome for offspring
|
|
let offspring_genome = self.genome.mutate();
|
|
let offspring_id = self.id * 1000 + self.age; // Simple ID generation
|
|
|
|
ActionResult::Reproduced { offspring_id }
|
|
}
|
|
|
|
fn move_action(&mut self, _dx: f64, _dy: f64) -> ActionResult {
|
|
let base_cost = 3.0;
|
|
let cost = self.action_energy_cost(base_cost);
|
|
|
|
if self.energy < cost {
|
|
return ActionResult::Failed { reason: "Not enough energy to move".to_string() };
|
|
}
|
|
|
|
self.energy -= cost;
|
|
|
|
// Moving affects temperature
|
|
if let Some(temp) = self.internal_state.get_mut("temperature") {
|
|
*temp += 0.1; // Movement generates heat
|
|
}
|
|
|
|
ActionResult::Success {
|
|
energy_cost: cost,
|
|
coherence_impact: 0.0,
|
|
}
|
|
}
|
|
|
|
fn rest(&mut self) -> ActionResult {
|
|
// Resting is cheap and helps regulate
|
|
let cost = 0.5;
|
|
self.energy -= cost;
|
|
|
|
// Slowly return to setpoints
|
|
for (var, current) in self.internal_state.iter_mut() {
|
|
if let Some(&setpoint) = self.setpoints.get(var) {
|
|
let diff = setpoint - *current;
|
|
*current += diff * 0.1;
|
|
}
|
|
}
|
|
|
|
ActionResult::Success {
|
|
energy_cost: cost,
|
|
coherence_impact: self.calculate_coherence() - self.coherence,
|
|
}
|
|
}
|
|
|
|
fn check_death(&mut self) {
|
|
// Death by energy depletion
|
|
if self.energy <= 0.0 {
|
|
self.alive = false;
|
|
return;
|
|
}
|
|
|
|
// Death by coherence collapse
|
|
if self.coherence < 0.1 {
|
|
self.alive = false;
|
|
return;
|
|
}
|
|
|
|
// Death by extreme deviation
|
|
for (var, ¤t) in &self.internal_state {
|
|
if let (Some(&setpoint), Some(&tolerance)) =
|
|
(self.setpoints.get(var), self.tolerances.get(var))
|
|
{
|
|
if (current - setpoint).abs() > tolerance * 5.0 {
|
|
self.alive = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Death by old age (modified by longevity gene)
|
|
let max_age = (1000.0 * self.genome.longevity) as u64;
|
|
if self.age > max_age {
|
|
self.alive = false;
|
|
}
|
|
}
|
|
|
|
pub fn is_alive(&self) -> bool {
|
|
self.alive
|
|
}
|
|
|
|
pub fn status(&self) -> String {
|
|
format!(
|
|
"Organism {} | Age: {} | Energy: {:.1} | Coherence: {:.2} | Memory: {}",
|
|
self.id, self.age, self.energy, self.coherence, self.memory.len()
|
|
)
|
|
}
|
|
}
|
|
|
|
impl Genome {
|
|
pub fn random() -> Self {
|
|
Self {
|
|
regulatory_strength: 0.1 + rand_f64() * 0.4,
|
|
metabolic_efficiency: 0.5 + rand_f64() * 0.5,
|
|
coherence_maintenance_cost: 0.5 + rand_f64() * 1.5,
|
|
memory_resilience: rand_f64(),
|
|
longevity: 0.5 + rand_f64() * 1.0,
|
|
}
|
|
}
|
|
|
|
pub fn mutate(&self) -> Self {
|
|
Self {
|
|
regulatory_strength: mutate_value(self.regulatory_strength, 0.05, 0.1, 0.9),
|
|
metabolic_efficiency: mutate_value(self.metabolic_efficiency, 0.05, 0.3, 1.0),
|
|
coherence_maintenance_cost: mutate_value(self.coherence_maintenance_cost, 0.1, 0.1, 3.0),
|
|
memory_resilience: mutate_value(self.memory_resilience, 0.05, 0.0, 1.0),
|
|
longevity: mutate_value(self.longevity, 0.05, 0.3, 2.0),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Thread-safe atomic seed for pseudo-random number generation
|
|
static SEED: AtomicU64 = AtomicU64::new(12345);
|
|
|
|
fn rand_f64() -> f64 {
|
|
// Simple LCG for reproducibility in tests - now thread-safe
|
|
let old = SEED.fetch_add(1, Ordering::Relaxed);
|
|
let new = old.wrapping_mul(1103515245).wrapping_add(12345);
|
|
// Store back for next call (best-effort, races are acceptable for RNG)
|
|
let _ = SEED.compare_exchange(old + 1, new, Ordering::Relaxed, Ordering::Relaxed);
|
|
((new >> 16) & 0x7fff) as f64 / 32768.0
|
|
}
|
|
|
|
fn mutate_value(value: f64, mutation_rate: f64, min: f64, max: f64) -> f64 {
|
|
let mutation = (rand_f64() - 0.5) * 2.0 * mutation_rate;
|
|
(value + mutation).clamp(min, max)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_homeostatic_survival() {
|
|
let genome = Genome::random();
|
|
let mut organism = HomeostasticOrganism::new(1, genome);
|
|
|
|
let mut ticks = 0;
|
|
while organism.is_alive() && ticks < 1000 {
|
|
// Simple behavior: eat when hungry, regulate when unstable
|
|
let action = if organism.energy < 50.0 {
|
|
Action::Eat(20.0)
|
|
} else if organism.coherence < 0.8 {
|
|
Action::Regulate("temperature".to_string(), 37.0)
|
|
} else {
|
|
Action::Rest
|
|
};
|
|
|
|
let _ = organism.act(action);
|
|
ticks += 1;
|
|
|
|
if ticks % 100 == 0 {
|
|
println!("{}", organism.status());
|
|
}
|
|
}
|
|
|
|
println!("Survived {} ticks", ticks);
|
|
println!("Final: {}", organism.status());
|
|
}
|
|
|
|
#[test]
|
|
fn test_coherence_based_death() {
|
|
let genome = Genome::random();
|
|
let mut organism = HomeostasticOrganism::new(2, genome);
|
|
|
|
// Deliberately destabilize
|
|
if let Some(temp) = organism.internal_state.get_mut("temperature") {
|
|
*temp = 50.0; // Extreme fever
|
|
}
|
|
|
|
let mut ticks = 0;
|
|
while organism.is_alive() && ticks < 100 {
|
|
let _ = organism.act(Action::Rest);
|
|
ticks += 1;
|
|
}
|
|
|
|
// Organism should die from coherence collapse or extreme deviation
|
|
assert!(!organism.is_alive(), "Organism should die from instability");
|
|
}
|
|
}
|