Files
brahman/crates/modules/dominium/dominium-physics/src/diffuse.rs
T
sergio cd3b41a401 feat(dominium): dominium-physics — ciclo del motor (difusión + tick)
- diffuse — ecuación de fluidos discreta sobre los 3 campos dinámicos
  (materia/psique/poder): cada celda intercambia con sus 4 vecinas +
  entropía. Buffer de lectura separado (lee estado viejo). oro y
  degradacion no difunden.
- tick — un paso completo: difusión → transiciones (agente exhausto se
  fuerza a Pelear) → acciones de los agentes → envejecimiento + cosecha
  (la energía del muerto vuelve como materia/fertilidad). run() corre N.

Determinista bit-exacto: aritmética f32 en orden fijo, sin HashMap ni
reducciones paralelas. Test `run_is_deterministic` verifica que mismo
input → mismo estado bit a bit.

7 tests verdes. cargo check --workspace verde. dominium ya CORRE
(core + physics = simulación funcional).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 16:08:01 +00:00

98 lines
3.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Difusión y entropía de los campos de la grilla.
//!
//! Ecuación de fluidos discreta: cada celda intercambia una fracción de
//! su valor con sus 4 vecinas, y luego pierde una fracción al ambiente
//! (entropía). Difunden los 3 campos dinámicos — materia, psique,
//! poder. `oro` (materia sólida) y `degradacion` (cicatriz permanente)
//! no difunden.
use dominium_core::{Grid, SimParams};
/// Difunde una sola capa: `new[c] = c + rate·(media_vecinos c)`, y luego
/// aplica la entropía. Usa un buffer de lectura separado (la difusión
/// debe leer el estado viejo).
fn diffuse_layer(layer: &mut [f32], width: usize, height: usize, rate: f32, entropy: f32) {
let old = layer.to_vec();
for y in 0..height {
for x in 0..width {
let c = y * width + x;
let mut sum = 0.0f32;
let mut count = 0.0f32;
// 4-vecindad (von Neumann), bordes sin wrap.
if x > 0 {
sum += old[c - 1];
count += 1.0;
}
if x + 1 < width {
sum += old[c + 1];
count += 1.0;
}
if y > 0 {
sum += old[c - width];
count += 1.0;
}
if y + 1 < height {
sum += old[c + width];
count += 1.0;
}
let neighbor_avg = if count > 0.0 { sum / count } else { old[c] };
let diffused = old[c] + rate * (neighbor_avg - old[c]);
layer[c] = diffused * (1.0 - entropy);
}
}
}
/// Aplica un paso de difusión + entropía a los 3 campos dinámicos.
pub fn diffuse(grid: &mut Grid, p: &SimParams) {
let (w, h) = (grid.width, grid.height);
let (rate, ent) = (p.diffusion_rate, p.entropy_rate);
diffuse_layer(&mut grid.materia, w, h, rate, ent);
diffuse_layer(&mut grid.psique, w, h, rate, ent);
diffuse_layer(&mut grid.poder, w, h, rate, ent);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diffusion_spreads_a_spike_to_neighbors() {
let mut g = Grid::new(5, 5);
let center = g.idx(2, 2);
g.materia[center] = 100.0;
let p = SimParams::default();
diffuse(&mut g, &p);
// El pico bajó; las vecinas subieron desde 0.
assert!(g.materia[center] < 100.0);
assert!(g.materia[g.idx(1, 2)] > 0.0);
assert!(g.materia[g.idx(3, 2)] > 0.0);
}
#[test]
fn entropy_decays_a_uniform_field() {
let mut g = Grid::new(4, 4);
for v in g.psique.iter_mut() {
*v = 10.0;
}
let p = SimParams::default();
diffuse(&mut g, &p);
// Campo uniforme: la difusión no cambia nada, pero la entropía sí.
for &v in &g.psique {
assert!(v < 10.0 && v > 9.0);
}
}
#[test]
fn diffusion_conserves_mass_minus_entropy() {
let mut g = Grid::new(6, 6);
let c = g.idx(3, 3);
g.materia[c] = 60.0;
let total_before: f32 = g.materia.iter().sum();
let mut p = SimParams::default();
p.entropy_rate = 0.0; // sin pérdida → masa conservada
diffuse(&mut g, &p);
let total_after: f32 = g.materia.iter().sum();
assert!((total_before - total_after).abs() < 1e-2);
}
}