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>
This commit is contained in:
Generated
+7
@@ -3482,6 +3482,13 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dominium-physics"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dominium-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "double-ended-peekable"
|
name = "double-ended-peekable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ members = [
|
|||||||
# modules/dominium/ — Simulador psicológico de campo medio
|
# modules/dominium/ — Simulador psicológico de campo medio
|
||||||
# ============================================================
|
# ============================================================
|
||||||
"crates/modules/dominium/dominium-core",
|
"crates/modules/dominium/dominium-core",
|
||||||
|
"crates/modules/dominium/dominium-physics",
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
# modules/gioser/ — Landing WASM (chacana + 4 elementos)
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ pub struct SimParams {
|
|||||||
pub desperation_threshold: f32,
|
pub desperation_threshold: f32,
|
||||||
/// Edad máxima; al superarla el agente muere.
|
/// Edad máxima; al superarla el agente muere.
|
||||||
pub max_edad: u32,
|
pub max_edad: u32,
|
||||||
|
/// Fracción que cada celda difunde hacia sus 4 vecinas por tick (0-1).
|
||||||
|
pub diffusion_rate: f32,
|
||||||
|
/// Tasa de pérdida natural (entropía) de los campos por tick (0-1).
|
||||||
|
pub entropy_rate: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SimParams {
|
impl Default for SimParams {
|
||||||
@@ -49,6 +53,8 @@ impl Default for SimParams {
|
|||||||
absorb_frac: 0.50,
|
absorb_frac: 0.50,
|
||||||
desperation_threshold: 5.0,
|
desperation_threshold: 5.0,
|
||||||
max_edad: 1000,
|
max_edad: 1000,
|
||||||
|
diffusion_rate: 0.10,
|
||||||
|
entropy_rate: 0.01,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[package]
|
||||||
|
name = "dominium-physics"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "dominium — ciclo del motor: difusión + entropía de los campos + tick completo (transiciones, acciones, envejecimiento). Determinista bit-exacto."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dominium-core = { path = "../dominium-core" }
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
//! 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! `dominium-physics` — el ciclo del motor de simulación.
|
||||||
|
//!
|
||||||
|
//! - [`diffuse`] — difusión + entropía de los campos de la grilla.
|
||||||
|
//! - [`tick`] — un paso completo: difusión → transiciones → acciones →
|
||||||
|
//! envejecimiento/cosecha. [`tick::run`] corre N pasos.
|
||||||
|
//!
|
||||||
|
//! Determinista bit-exacto: sólo aritmética f32 en orden fijo, sin
|
||||||
|
//! HashMap iteration ni reducciones paralelas. Mismo seed → mismo estado
|
||||||
|
//! en cualquier plataforma.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
pub mod diffuse;
|
||||||
|
pub mod tick;
|
||||||
|
|
||||||
|
pub use diffuse::diffuse;
|
||||||
|
pub use tick::{run, tick};
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
//! El ciclo del motor — un `tick` completo de la simulación.
|
||||||
|
//!
|
||||||
|
//! Orden fijo: difusión/entropía → evaluación de transiciones → acciones
|
||||||
|
//! de los agentes → envejecimiento y cosecha de muertos.
|
||||||
|
|
||||||
|
use crate::diffuse::diffuse;
|
||||||
|
use dominium_core::{SimParams, World};
|
||||||
|
|
||||||
|
/// Evaluación de transiciones: un agente exhausto se fuerza a `Pelear`.
|
||||||
|
fn apply_transitions(world: &mut World, p: &SimParams) {
|
||||||
|
for i in 0..world.lemmings.len() {
|
||||||
|
if world.lemmings.energia[i] < p.desperation_threshold {
|
||||||
|
world.lemmings.accion[i] = 5; // Degradar (Pelear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Envejece a los agentes y cosecha a los muertos: la energía remanente
|
||||||
|
/// de un agente que muere se inyecta como fertilidad (`materia`) en su
|
||||||
|
/// celda. Devuelve cuántos murieron.
|
||||||
|
fn age_and_reap(world: &mut World, p: &SimParams) -> usize {
|
||||||
|
for e in world.lemmings.edad.iter_mut() {
|
||||||
|
*e += 1;
|
||||||
|
}
|
||||||
|
// Recolecta los índices muertos (energía agotada o edad excedida).
|
||||||
|
let mut dead: Vec<usize> = (0..world.lemmings.len())
|
||||||
|
.filter(|&i| {
|
||||||
|
world.lemmings.energia[i] <= 0.0 || world.lemmings.edad[i] > p.max_edad
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
// Remueve de mayor a menor índice: swap_remove no invalida los menores.
|
||||||
|
dead.sort_unstable_by(|a, b| b.cmp(a));
|
||||||
|
let count = dead.len();
|
||||||
|
for i in dead {
|
||||||
|
let (cx, cy) = world
|
||||||
|
.grid
|
||||||
|
.clamp_cell(world.lemmings.pos_x[i], world.lemmings.pos_y[i]);
|
||||||
|
let idx = world.grid.idx(cx, cy);
|
||||||
|
// La energía remanente vuelve a la tierra como biomasa.
|
||||||
|
let remnant = world.lemmings.energia[i].max(0.0);
|
||||||
|
world.grid.materia[idx] += remnant;
|
||||||
|
world.lemmings.remove(i);
|
||||||
|
}
|
||||||
|
count
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un paso completo de la simulación.
|
||||||
|
pub fn tick(world: &mut World, p: &SimParams) {
|
||||||
|
// 1. Difusión y entropía sobre los campos.
|
||||||
|
diffuse(&mut world.grid, p);
|
||||||
|
// 2. Transiciones de estado forzadas.
|
||||||
|
apply_transitions(world, p);
|
||||||
|
// 3. Acciones de los agentes. Se fija `n` antes del loop: los hijos
|
||||||
|
// que `Replicar` agrega al final NO actúan este tick.
|
||||||
|
let n = world.lemmings.len();
|
||||||
|
for i in 0..n {
|
||||||
|
if i < world.lemmings.len() {
|
||||||
|
world.step_lemming(i, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 4. Envejecer + cosechar muertos.
|
||||||
|
age_and_reap(world, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Corre `steps` ticks seguidos.
|
||||||
|
pub fn run(world: &mut World, p: &SimParams, steps: usize) {
|
||||||
|
for _ in 0..steps {
|
||||||
|
tick(world, p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exhausted_agent_is_forced_to_fight() {
|
||||||
|
let mut w = World::new(8, 8);
|
||||||
|
w.lemmings.spawn(4.0, 4.0, 1.0, [0.0; 4]); // energía 1 < umbral 5
|
||||||
|
w.lemmings.accion[0] = 0; // Mover
|
||||||
|
let p = SimParams::default();
|
||||||
|
apply_transitions(&mut w, &p);
|
||||||
|
assert_eq!(w.lemmings.accion[0], 5); // forzado a Degradar
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dead_agent_returns_energy_as_materia() {
|
||||||
|
let mut w = World::new(8, 8);
|
||||||
|
// Agente sin energía → muere este tick.
|
||||||
|
w.lemmings.spawn(4.0, 4.0, 0.0, [0.0; 4]);
|
||||||
|
let idx = w.grid.idx(4, 4);
|
||||||
|
let p = SimParams::default();
|
||||||
|
let reaped = age_and_reap(&mut w, &p);
|
||||||
|
assert_eq!(reaped, 1);
|
||||||
|
assert_eq!(w.lemmings.len(), 0);
|
||||||
|
// (energía remanente 0 → materia no sube, pero no panickea)
|
||||||
|
assert!(w.grid.materia[idx] >= 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tick_runs_without_panicking_on_a_populated_world() {
|
||||||
|
let mut w = World::new(32, 32);
|
||||||
|
for k in 0..20 {
|
||||||
|
let x = (k % 8) as f32 + 2.0;
|
||||||
|
let y = (k / 8) as f32 + 2.0;
|
||||||
|
w.lemmings.spawn(x, y, 30.0, [1.0, 0.2, 0.5, 0.1]);
|
||||||
|
w.lemmings.accion[k] = (k % 6) as u8;
|
||||||
|
}
|
||||||
|
// Sembrar algo de materia.
|
||||||
|
for c in w.grid.materia.iter_mut() {
|
||||||
|
*c = 5.0;
|
||||||
|
}
|
||||||
|
let p = SimParams::default();
|
||||||
|
run(&mut w, &p, 50);
|
||||||
|
// La sim avanzó 50 ticks sin romperse.
|
||||||
|
assert!(w.lemmings.edad.iter().all(|&e| e <= 50));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn run_is_deterministic() {
|
||||||
|
let build = || {
|
||||||
|
let mut w = World::new(16, 16);
|
||||||
|
for k in 0..10 {
|
||||||
|
w.lemmings.spawn(3.0 + k as f32, 8.0, 40.0, [1.0, 0.0, 0.3, 0.0]);
|
||||||
|
w.lemmings.accion[k] = (k % 6) as u8;
|
||||||
|
}
|
||||||
|
for c in w.grid.materia.iter_mut() {
|
||||||
|
*c = 3.0;
|
||||||
|
}
|
||||||
|
w
|
||||||
|
};
|
||||||
|
let p = SimParams::default();
|
||||||
|
let mut a = build();
|
||||||
|
let mut b = build();
|
||||||
|
run(&mut a, &p, 30);
|
||||||
|
run(&mut b, &p, 30);
|
||||||
|
// Mismo input → mismo estado, bit a bit.
|
||||||
|
assert_eq!(a.lemmings.pos_x, b.lemmings.pos_x);
|
||||||
|
assert_eq!(a.lemmings.energia, b.lemmings.energia);
|
||||||
|
assert_eq!(a.grid.materia, b.grid.materia);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -820,3 +820,80 @@
|
|||||||
./target/debug/sandokan stop <card-id>
|
./target/debug/sandokan stop <card-id>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
● Comandos de prueba
|
||||||
|
|
||||||
|
target/ está en 22 GB — pero tranquilo: está gitignored, el repo git
|
||||||
|
real son 96 MB. El "desborde" es disco, no el repositorio. Más abajo
|
||||||
|
el cargo clean.
|
||||||
|
|
||||||
|
Todo de una
|
||||||
|
|
||||||
|
cargo check --workspace # compila los ~145 crates
|
||||||
|
cargo test --workspace # corre TODOS los tests (~270)
|
||||||
|
|
||||||
|
sandokan — el ejecutable interactivo
|
||||||
|
|
||||||
|
cargo build -p sandokan-cli
|
||||||
|
./target/debug/sandokan daemon & # deja el daemon
|
||||||
|
corriendo
|
||||||
|
./target/debug/sandokan run /bin/sleep 300 # imprime un card_id
|
||||||
|
./target/debug/sandokan list # lo ves activo
|
||||||
|
./target/debug/sandokan status <card-id> # → Running
|
||||||
|
./target/debug/sandokan telemetry <card-id> # → mem / nproc
|
||||||
|
./target/debug/sandokan stop <card-id> # → detenido
|
||||||
|
./target/debug/sandokan status <card-id> # → Killed
|
||||||
|
|
||||||
|
Tests por área (lo construido esta sesión)
|
||||||
|
|
||||||
|
# Fase A — foundations
|
||||||
|
cargo test -p arje-brain-rules -p arje-brain-cognitive -p
|
||||||
|
arje-brain-audit \
|
||||||
|
-p sandokan-lifecycle -p arje-incarnate
|
||||||
|
|
||||||
|
# Fase B — orquestador + discovery
|
||||||
|
cargo test -p sandokan-core -p sandokan-local -p sandokan-daemon -p
|
||||||
|
sandokan \
|
||||||
|
-p brahman-dht -p brahman-card-discovery -p
|
||||||
|
brahman-ssh-multiplex
|
||||||
|
|
||||||
|
# Fase F — pineal (6 charts)
|
||||||
|
cargo test -p pineal-render -p pineal-export -p pineal-heatmap \
|
||||||
|
-p pineal-polar -p pineal-treemap -p pineal-flow -p
|
||||||
|
pineal-mesh
|
||||||
|
|
||||||
|
# Apps Fase C — núcleos
|
||||||
|
cargo test -p fana-core -p fana-graph -p fana-store -p fana-semantic
|
||||||
|
# fana
|
||||||
|
cargo test -p shuma-intent
|
||||||
|
# shuma shell
|
||||||
|
cargo test -p verbo-core -p verbo-mock
|
||||||
|
# verbo
|
||||||
|
cargo test -p dominium-core
|
||||||
|
# dominium
|
||||||
|
|
||||||
|
Validación arje (boot — ya existía)
|
||||||
|
|
||||||
|
seeds/validate.sh seeds/arje-minimal.card.json
|
||||||
|
|
||||||
|
⚠ Disco — cargo clean
|
||||||
|
|
||||||
|
target/ = 22 GB y va a seguir creciendo (gpui, libp2p, russh son
|
||||||
|
pesados). Cuando quieras recuperar ese espacio:
|
||||||
|
|
||||||
|
cargo clean # borra target/ entero — recupera ~22 GB
|
||||||
|
|
||||||
|
Tras eso, el próximo cargo build recompila todo desde cero (lento,
|
||||||
|
~varios min). Recomendación: corré tus pruebas, y cuando termines una
|
||||||
|
tanda, cargo clean. No afecta nada del código ni del repo — sólo
|
||||||
|
artefactos de compilación.
|
||||||
|
|
||||||
|
¿Querés que lo corra yo ahora (cargo clean), o lo dejás para cuando
|
||||||
|
termines de probar esta tanda? Si lo corro ahora, tu próxima prueba
|
||||||
|
recompila de cero.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user