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>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
//!
|
||||
//! Reference: D-178 (Economic Model Architecture)
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::agents;
|
||||
use crate::currency::{CurrencyState, ShadowEconomy};
|
||||
@@ -54,7 +54,7 @@ pub struct CommodityState {
|
||||
pub struct NodeState {
|
||||
pub system_id: String,
|
||||
/// commodity_id → state
|
||||
pub commodities: HashMap<String, CommodityState>,
|
||||
pub commodities: BTreeMap<String, CommodityState>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -89,9 +89,9 @@ pub struct TickRecord {
|
||||
/// Returns a flat list of TickRecords (one per active node×commodity×tick).
|
||||
pub fn run(
|
||||
economy: &Economy,
|
||||
productivity: &HashMap<(String, String), Productivity>,
|
||||
productivity: &BTreeMap<(String, String), Productivity>,
|
||||
shadow: &ShadowEconomy,
|
||||
adjacency: &HashMap<String, Vec<String>>,
|
||||
adjacency: &BTreeMap<String, Vec<String>>,
|
||||
ticks: u32,
|
||||
) -> Vec<TickRecord> {
|
||||
let archetypes = agents::build_archetype_map(economy.corp_archetype_data.clone());
|
||||
@@ -106,7 +106,11 @@ pub fn run(
|
||||
|
||||
let fx_rate = currency.tractus_mark_rate;
|
||||
for node in nodes.values() {
|
||||
let node_shadow = shadow.intensity.get(&node.system_id).copied().unwrap_or(0.0);
|
||||
let node_shadow = shadow
|
||||
.intensity
|
||||
.get(&node.system_id)
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
for (commodity_id, state) in &node.commodities {
|
||||
records.push(TickRecord {
|
||||
tick,
|
||||
@@ -129,8 +133,8 @@ pub fn run(
|
||||
// Initialization
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn init_nodes(economy: &Economy) -> HashMap<String, NodeState> {
|
||||
let mut nodes: HashMap<String, NodeState> = HashMap::new();
|
||||
fn init_nodes(economy: &Economy) -> BTreeMap<String, NodeState> {
|
||||
let mut nodes: BTreeMap<String, NodeState> = BTreeMap::new();
|
||||
|
||||
// Activate nodes that have corp presence or non-zero population
|
||||
for (system_id, system) in &economy.systems {
|
||||
@@ -140,7 +144,7 @@ fn init_nodes(economy: &Economy) -> HashMap<String, NodeState> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut commodity_states: HashMap<String, CommodityState> = HashMap::new();
|
||||
let mut commodity_states: BTreeMap<String, CommodityState> = BTreeMap::new();
|
||||
for commodity in &economy.commodities {
|
||||
let base_price = commodity.base_price;
|
||||
let base_demand = base_population_demand(system.population, &commodity.tier);
|
||||
@@ -197,10 +201,10 @@ const SHADOW_DEMAND_COVERAGE: f64 = 0.30;
|
||||
|
||||
fn step(
|
||||
economy: &Economy,
|
||||
productivity: &HashMap<(String, String), Productivity>,
|
||||
productivity: &BTreeMap<(String, String), Productivity>,
|
||||
shadow: &ShadowEconomy,
|
||||
archetypes: &HashMap<String, agents::Archetype>,
|
||||
nodes: &mut HashMap<String, NodeState>,
|
||||
archetypes: &BTreeMap<String, agents::Archetype>,
|
||||
nodes: &mut BTreeMap<String, NodeState>,
|
||||
) {
|
||||
// Process each active node independently (Layer 1: no inter-system trade)
|
||||
let system_ids: Vec<String> = nodes.keys().cloned().collect();
|
||||
@@ -227,8 +231,7 @@ fn step(
|
||||
.unwrap_or_default();
|
||||
|
||||
for corp_presence in &corps {
|
||||
let prod = match productivity.get(&(corp_presence.corp_id.clone(), system_id.clone()))
|
||||
{
|
||||
let prod = match productivity.get(&(corp_presence.corp_id.clone(), system_id.clone())) {
|
||||
Some(p) => p,
|
||||
None => continue,
|
||||
};
|
||||
@@ -277,9 +280,8 @@ fn step(
|
||||
let available = state.stockpile;
|
||||
let required = input.quantity * effective_capacity;
|
||||
if required > 0.0 {
|
||||
capacity_fraction = capacity_fraction
|
||||
.min(available / required)
|
||||
.clamp(0.0, 1.0);
|
||||
capacity_fraction =
|
||||
capacity_fraction.min(available / required).clamp(0.0, 1.0);
|
||||
}
|
||||
} else {
|
||||
capacity_fraction = 0.0;
|
||||
@@ -318,8 +320,7 @@ fn step(
|
||||
.get(&primary_op)
|
||||
.map_or(1.0, |c| c.base_price);
|
||||
let nudge = base_price * arch_params.price_premium * ALPHA;
|
||||
state.price = (state.price + nudge)
|
||||
.clamp(base_price * 0.05, base_price * 20.0);
|
||||
state.price = (state.price + nudge).clamp(base_price * 0.05, base_price * 20.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -381,8 +382,8 @@ fn step(
|
||||
0.0
|
||||
};
|
||||
|
||||
state.price = (state.price * (1.0 - ALPHA * excess))
|
||||
.clamp(base_price * 0.05, base_price * 20.0);
|
||||
state.price =
|
||||
(state.price * (1.0 - ALPHA * excess)).clamp(base_price * 0.05, base_price * 20.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user