Files
settled-reach/tooling/econ-sim/src/seed.rs
T
jpmschweitzerandClaude Opus 4.6 d45cfe0fa3 fix(simulation): address PR #122 review — determinism, correctness, labeling
- HashMap → BTreeMap throughout econ-sim for deterministic iteration (D-010)
- Fix cost_factor: multiplicative gate×zone instead of additive (trade.rs)
- Extract derive_seed to shared prng.rs, consolidate FNV-1a implementation
- Rename run_shock_test → run_no_explosion_check (not D-179 Test 3)
- Deduplicate cross-zone FX rate collection in Test 4
- Replace ORDER BY RANDOM() with deterministic ordering + ChaCha8Rng
- Make commodity coverage failure a hard error consistent with D-175
- Fix gap-fill off-by-one (4 corps → 3 when coverage = 0)
- Correct test report: EconEvent exists, location_type is body/station

All four D-179 stability tests still pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:51:23 +02:00

118 lines
4.1 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.
//! 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::BTreeMap;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
use crate::db::Economy;
use crate::prng::{derive_seed, standard_normal};
// ---------------------------------------------------------------------------
// 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,
}
}
}
// ---------------------------------------------------------------------------
// 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,
) -> BTreeMap<(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: BTreeMap<String, f64> = BTreeMap::new();
let mut result = BTreeMap::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
}