- 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>
118 lines
4.1 KiB
Rust
118 lines
4.1 KiB
Rust
//! 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
|
||
}
|