//! 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 = 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 }