Adds tooling/econ-sim — a standalone Rust binary for the Phase 2 economics simulation: Layer 1 (Leontief production, #806): - Deterministic per-run PRNG seeding of corp×site productivity (D-176) - Fixed-coefficient production chains; scarcity cascades downstream (D-178) - Per-capita population demand for finals and services - Gate-energy demand reduction for fusion_fuel at connected nodes (D-186) - Price adjustment via local tâtonnement Layer 2 (spatial price equilibrium, #807): - Damped tâtonnement trade flows along gate links (α=0.03, β=0.4, D-178) - 8% transport cost per hop damps long-distance arbitrage - Flows computed from pre-step snapshot; applied atomically - --stability-check implements D-179 Tests 1 and 2: · Test 1: cold-start convergence ±5% at tick 100 → PASS (max 1.05%) · Test 2: long-run stability ±2% over ticks 900–999 → PASS (max 0.00%) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//! Productivity seeding — D-176.
|
||||
//!
|
||||
//! Per-run PRNG seeding of corporation×site productivity on five dimensions.
|
||||
//! Log-normal distribution with corridor correlation ~0.6.
|
||||
//!
|
||||
//! What CANNOT be seeded (D-177): location of production, biological monopoly
|
||||
//! ceilings, aging pipeline contents, gate topology.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::f64::consts::PI;
|
||||
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
|
||||
use crate::db::Economy;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Productivity record (D-176)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Productivity {
|
||||
/// Output per unit time from mines, wells, fisheries
|
||||
pub extraction_rate: f64,
|
||||
/// Units processed per tick in manufacturing and refineries
|
||||
pub processing_throughput: f64,
|
||||
/// Freight volume per gate crossing for logistics operators — used in #807 (trade flows)
|
||||
#[allow(dead_code)]
|
||||
pub transit_capacity: f64,
|
||||
/// Clients served per tick for service firms
|
||||
pub service_throughput: f64,
|
||||
/// Maximum concurrent engagements for service firms — used in #809 (agents)
|
||||
#[allow(dead_code)]
|
||||
pub service_capacity: f64,
|
||||
}
|
||||
|
||||
impl Productivity {
|
||||
/// Multiplier appropriate for a given commodity tier.
|
||||
pub fn for_tier(&self, tier: &str) -> f64 {
|
||||
match tier {
|
||||
"raw" => self.extraction_rate,
|
||||
"intermediate" => self.processing_throughput,
|
||||
"final" => self.processing_throughput,
|
||||
"service_professional" | "service_luxury" => self.service_throughput,
|
||||
_ => 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// PRNG helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Deterministic hash mix: FNV-1a of a string mixed with a 64-bit seed.
|
||||
fn derive_seed(run_seed: u64, key: &str) -> u64 {
|
||||
let mut h = run_seed.wrapping_add(14_695_981_039_346_656_037u64);
|
||||
for byte in key.bytes() {
|
||||
h ^= byte as u64;
|
||||
h = h.wrapping_mul(1_099_511_628_211u64);
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// Box-Muller transform: returns a standard normal variate from [0,1) samples.
|
||||
fn standard_normal(rng: &mut ChaCha8Rng) -> f64 {
|
||||
use rand::Rng;
|
||||
let u1: f64 = 1.0 - rng.random::<f64>(); // avoid ln(0)
|
||||
let u2: f64 = rng.random::<f64>();
|
||||
(-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Seeding entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Seed productivity for all corp×system pairs.
|
||||
///
|
||||
/// Returns a map keyed by (corp_id, system_id) → Productivity.
|
||||
pub fn seed_all_productivity(
|
||||
economy: &Economy,
|
||||
run_seed: u64,
|
||||
) -> HashMap<(String, String), Productivity> {
|
||||
// σ for standard nodes: chosen so that exp(±2σ) ≈ [0.4, 1.8] at 95%
|
||||
// Geometric mean of [0.4, 1.8] ≈ 0.849. μ = ln(0.849) ≈ −0.164.
|
||||
// We use μ=0 (geometric mean = 1) and wider σ; the clamp enforces the range.
|
||||
let sigma_total: f64 = 0.38;
|
||||
|
||||
// Corridor-shared variance fraction: ρ = 0.6 (D-176)
|
||||
let rho: f64 = 0.6;
|
||||
let sigma_shared = (rho).sqrt() * sigma_total;
|
||||
let sigma_individual = (1.0 - rho).sqrt() * sigma_total;
|
||||
|
||||
// Pre-compute corridor Z values (shared across all corps in the same corridor)
|
||||
let mut corridor_z: HashMap<String, f64> = HashMap::new();
|
||||
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for cp in &economy.corp_presences {
|
||||
let system = match economy.systems.get(&cp.system_id) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Corridor shared factor
|
||||
let corridor_contribution = if let Some(corr) = &system.cultural_corridor {
|
||||
let z = *corridor_z.entry(corr.clone()).or_insert_with(|| {
|
||||
let seed = derive_seed(run_seed, corr);
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(seed);
|
||||
standard_normal(&mut rng)
|
||||
});
|
||||
sigma_shared * z
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Individual factor per corp×site
|
||||
let key = format!("{}:{}", cp.corp_id, cp.system_id);
|
||||
let site_seed = derive_seed(run_seed, &key);
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(site_seed);
|
||||
|
||||
let sample = |rng: &mut ChaCha8Rng| -> f64 {
|
||||
let individual_z = standard_normal(rng);
|
||||
let combined = corridor_contribution + sigma_individual * individual_z;
|
||||
combined.exp().clamp(0.4, 1.8)
|
||||
};
|
||||
|
||||
let prod = Productivity {
|
||||
extraction_rate: sample(&mut rng),
|
||||
processing_throughput: sample(&mut rng),
|
||||
transit_capacity: sample(&mut rng),
|
||||
service_throughput: sample(&mut rng),
|
||||
service_capacity: sample(&mut rng),
|
||||
};
|
||||
|
||||
result.insert((cp.corp_id.clone(), cp.system_id.clone()), prod);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
Reference in New Issue
Block a user