use crate::core::array_calculator::ArrayCalculator;
use crate::error::{ForgeError, ForgeResult};
use crate::monte_carlo::{MonteCarloConfig, MonteCarloEngine};
use crate::parser;
use colored::Colorize;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
pub fn simulate_core(
file: &Path,
iterations_override: Option<usize>,
seed_override: Option<u64>,
sampling_override: Option<&str>,
) -> ForgeResult<crate::monte_carlo::SimulationResult> {
let yaml_content = fs::read_to_string(file).map_err(ForgeError::Io)?;
let mut config = parse_monte_carlo_config(&yaml_content)?;
if let Some(n) = iterations_override {
config.iterations = n;
}
if let Some(s) = seed_override {
config.seed = Some(s);
}
if let Some(sampling) = sampling_override {
config.sampling = sampling.to_string();
}
config.validate().map_err(ForgeError::Validation)?;
let model = parser::parse_model(file)?;
let mut engine = MonteCarloEngine::new(config.clone()).map_err(ForgeError::Validation)?;
engine
.parse_distributions_from_model(&model)
.map_err(ForgeError::Validation)?;
let output_vars: Vec<String> = config.outputs.iter().map(|o| o.variable.clone()).collect();
engine
.run_with_evaluator(|inputs: &HashMap<String, f64>| {
let mut iter_model = model.clone();
for (var_name, &value) in inputs {
if let Some(scalar) = iter_model.scalars.get_mut(var_name) {
scalar.value = Some(value);
scalar.formula = None;
}
}
let calculator = crate::core::array_calculator::ArrayCalculator::new(iter_model);
let Ok(calculated) = calculator.calculate_all() else {
return HashMap::new();
};
let mut outputs = HashMap::new();
for var_name in &output_vars {
let value = calculated
.scalars
.get(var_name)
.or_else(|| calculated.scalars.get(&format!("outputs.{var_name}")))
.or_else(|| calculated.scalars.get(&format!("scalars.{var_name}")))
.and_then(|s| s.value)
.unwrap_or(0.0);
outputs.insert(var_name.clone(), value);
}
outputs
})
.map_err(ForgeError::Eval)
}
pub fn simulate(
file: &Path,
iterations_override: Option<usize>,
seed_override: Option<u64>,
sampling_override: Option<&str>,
output_file: Option<PathBuf>,
verbose: bool,
) -> ForgeResult<()> {
println!("{}", "🎲 Forge - Monte Carlo Simulation".bold().green());
println!(" File: {}", file.display());
println!();
if verbose {
println!("{}", "📖 Parsing YAML file...".cyan());
}
let yaml_content = fs::read_to_string(file).map_err(ForgeError::Io)?;
let mut config = parse_monte_carlo_config(&yaml_content)?;
if let Some(n) = iterations_override {
config.iterations = n;
}
if let Some(s) = seed_override {
config.seed = Some(s);
}
if let Some(sampling) = sampling_override {
config.sampling = sampling.to_string();
}
config.validate().map_err(ForgeError::Validation)?;
println!(" {}", "Configuration:".bold());
println!(
" Iterations: {}",
config.iterations.to_string().bright_blue()
);
println!(" Sampling: {}", config.sampling.bright_blue());
if let Some(seed) = config.seed {
println!(" Seed: {}", seed.to_string().bright_blue());
}
println!();
let model = parser::parse_model(file)?;
if verbose {
println!(
" Found {} tables, {} scalars",
model.tables.len(),
model.scalars.len()
);
}
let mut engine = MonteCarloEngine::new(config.clone()).map_err(ForgeError::Validation)?;
engine
.parse_distributions_from_model(&model)
.map_err(ForgeError::Validation)?;
if verbose {
println!("{}", "🎲 Running simulation...".cyan());
}
let output_vars: Vec<String> = config.outputs.iter().map(|o| o.variable.clone()).collect();
let result = engine
.run_with_evaluator(|inputs: &HashMap<String, f64>| {
let mut iter_model = model.clone();
for (var_name, &value) in inputs {
if let Some(scalar) = iter_model.scalars.get_mut(var_name) {
scalar.value = Some(value);
scalar.formula = None; }
}
let calculator = ArrayCalculator::new(iter_model);
let Ok(calculated) = calculator.calculate_all() else {
return HashMap::new();
};
let mut outputs = HashMap::new();
for var_name in &output_vars {
let value = calculated
.scalars
.get(var_name)
.or_else(|| calculated.scalars.get(&format!("outputs.{var_name}")))
.or_else(|| calculated.scalars.get(&format!("scalars.{var_name}")))
.and_then(|s| s.value)
.unwrap_or(0.0);
outputs.insert(var_name.clone(), value);
}
outputs
})
.map_err(ForgeError::Eval)?;
print_simulation_results(&result);
if let Some(output_path) = output_file {
write_simulation_output(&result, &output_path)?;
}
println!("{}", "✅ Simulation complete".bold().green());
Ok(())
}
fn print_simulation_results(result: &crate::monte_carlo::SimulationResult) {
println!("{}", "📊 Simulation Results:".bold().green());
println!(" Iterations: {}", result.iterations_completed);
println!(" Execution time: {} ms", result.execution_time_ms);
println!();
println!(" {}", "Input Distributions:".bold());
for (var_name, samples) in &result.input_samples {
#[allow(clippy::cast_precision_loss)] let mean: f64 = samples.iter().sum::<f64>() / samples.len() as f64;
let min = samples.iter().copied().fold(f64::INFINITY, f64::min);
let max = samples.iter().copied().fold(f64::NEG_INFINITY, f64::max);
println!(
" {} mean={:.2} min={:.2} max={:.2}",
var_name.bright_blue(),
mean,
min,
max
);
}
println!();
if !result.outputs.is_empty() {
println!(" {}", "Output Statistics:".bold());
for (var_name, output) in &result.outputs {
let stats = &output.statistics;
println!(" {}:", var_name.bright_blue().bold());
println!(" Mean: {:.4}", stats.mean);
println!(" Median: {:.4}", stats.median);
println!(" Std Dev: {:.4}", stats.std_dev);
println!(" Min: {:.4}", stats.min);
println!(" Max: {:.4}", stats.max);
println!(" Percentiles:");
for (p, v) in &stats.percentiles {
println!(" P{p}: {v:.4}");
}
for (threshold, prob) in &output.threshold_probabilities {
println!(
" P({} {}) = {:.2}%",
var_name,
threshold,
prob * 100.0
);
}
println!();
}
}
}
fn write_simulation_output(
result: &crate::monte_carlo::SimulationResult,
output_path: &Path,
) -> ForgeResult<()> {
let ext = output_path.extension().and_then(|e| e.to_str());
match ext {
Some("xlsx") => {
crate::monte_carlo::excel_export::export_results(result, output_path)
.map_err(ForgeError::Validation)?;
},
Some("json") => {
let output_str = result
.to_json()
.map_err(|e| ForgeError::Validation(format!("JSON error: {e}")))?;
fs::write(output_path, output_str).map_err(ForgeError::Io)?;
},
_ => {
let output_str = result.to_yaml();
fs::write(output_path, output_str).map_err(ForgeError::Io)?;
},
}
println!(
"{}",
format!("💾 Results written to {}", output_path.display())
.bold()
.green()
);
Ok(())
}
fn parse_monte_carlo_config(yaml_content: &str) -> ForgeResult<MonteCarloConfig> {
let value: serde_yaml_ng::Value = serde_yaml_ng::from_str(yaml_content)
.map_err(|e| ForgeError::Validation(format!("YAML parse error: {e}")))?;
if let Some(mc_value) = value.get("monte_carlo") {
let config: MonteCarloConfig = serde_yaml_ng::from_value(mc_value.clone())
.map_err(|e| ForgeError::Validation(format!("monte_carlo config error: {e}")))?;
Ok(config)
} else {
Ok(MonteCarloConfig::default().enabled())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_parse_monte_carlo_config() {
let yaml = r#"
_forge_version: "5.0.0"
monte_carlo:
enabled: true
iterations: 5000
sampling: latin_hypercube
seed: 42
outputs:
- variable: revenue
percentiles: [10, 50, 90]
scalars:
revenue:
value: 100000
formula: "=MC.Normal(100000, 15000)"
"#;
let config = parse_monte_carlo_config(yaml).unwrap();
assert!(config.enabled);
assert_eq!(config.iterations, 5000);
assert_eq!(config.seed, Some(42));
assert_eq!(config.outputs.len(), 1);
}
#[test]
fn test_parse_monte_carlo_config_defaults() {
let yaml = r#"
_forge_version: "5.0.0"
scalars:
revenue:
value: 100000
"#;
let config = parse_monte_carlo_config(yaml).unwrap();
assert!(config.enabled); assert_eq!(config.iterations, 10000); }
#[test]
fn test_simulate_with_mc_distributions() {
let yaml = r#"
_forge_version: "5.0.0"
monte_carlo:
enabled: true
iterations: 1000
sampling: latin_hypercube
seed: 12345
outputs:
- variable: revenue
percentiles: [10, 50, 90]
threshold: "> 90000"
scalars:
revenue:
value: 100000
formula: "=MC.Normal(100000, 15000)"
"#;
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "{yaml}").unwrap();
let config = parse_monte_carlo_config(yaml).unwrap();
assert!(config.enabled);
assert_eq!(config.iterations, 1000);
assert_eq!(config.outputs[0].variable, "revenue");
assert_eq!(config.outputs[0].threshold, Some("> 90000".to_string()));
}
#[test]
fn test_mc_dependent_formula_evaluation() {
use crate::monte_carlo::MonteCarloEngine;
use crate::parser;
let yaml = r#"
_forge_version: "5.0.0"
monte_carlo:
enabled: true
iterations: 100
sampling: latin_hypercube
seed: 42
outputs:
- variable: result
percentiles: [50]
scalars:
p_sampled:
value: null
formula: "=MC.Triangular(0.4, 0.6, 0.8)"
result:
value: null
formula: "=scalars.p_sampled * 100"
"#;
let mut file = NamedTempFile::new().unwrap();
writeln!(file, "{yaml}").unwrap();
let config = parse_monte_carlo_config(yaml).unwrap();
let model = parser::parse_model(file.path()).unwrap();
let mut engine = MonteCarloEngine::new(config.clone()).unwrap();
engine.parse_distributions_from_model(&model).unwrap();
let output_vars: Vec<String> = config.outputs.iter().map(|o| o.variable.clone()).collect();
let result = engine
.run_with_evaluator(|inputs: &HashMap<String, f64>| {
let mut iter_model = model.clone();
for (var_name, &value) in inputs {
if let Some(scalar) = iter_model.scalars.get_mut(var_name) {
scalar.value = Some(value);
scalar.formula = None;
}
}
let calculator = crate::core::array_calculator::ArrayCalculator::new(iter_model);
let Ok(calculated) = calculator.calculate_all() else {
return HashMap::new();
};
let mut outputs = HashMap::new();
for var_name in &output_vars {
let value = calculated
.scalars
.get(var_name)
.or_else(|| calculated.scalars.get(&format!("scalars.{var_name}")))
.and_then(|s| s.value)
.unwrap_or(0.0);
outputs.insert(var_name.clone(), value);
}
outputs
})
.unwrap();
let result_stats = &result.outputs["result"];
assert!(
result_stats.statistics.mean > 50.0,
"Mean should be ~60 (p_sampled * 100), got {}",
result_stats.statistics.mean
);
assert!(
result_stats.statistics.mean < 70.0,
"Mean should be ~60 (p_sampled * 100), got {}",
result_stats.statistics.mean
);
}
}