use std::cmp::Reverse;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::thread;
use serde::Serialize;
use crate::optimize::rank_key;
use crate::params::TaxTables;
use crate::plan::{Dollars, Issue, Plan, push_issue};
use crate::project::{MarketPath, Projection, deflate, project_on};
use super::{History, draw, start_in};
pub const BAND_PERCENTILES: [u8; 5] = [10, 25, 50, 75, 90];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunName {
Planned,
Trial(u32),
Start(i16),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Run {
pub name: RunName,
pub ending: Dollars,
pub unfunded: Dollars,
pub first_short: Option<i16>,
pub net_worth: Vec<Dollars>,
pub is_success: bool,
rank: (Dollars, Reverse<Dollars>),
}
impl Run {
fn of(name: RunName, projection: &Projection, leave_at_least: Option<Dollars>) -> Self {
let summary = projection.summary(true);
let first_short = summary.first_unfunded_year;
let ending = summary.final_net_worth;
Self {
name,
ending,
unfunded: summary.lifetime_unfunded,
first_short,
net_worth: projection
.years
.iter()
.map(|row| deflate(row.net_worth, row.deflator))
.collect(),
is_success: first_short.is_none() && leave_at_least.is_none_or(|floor| ending >= floor),
rank: rank_key(projection),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Band {
pub year: i16,
pub net_worth: [Dollars; BAND_PERCENTILES.len()],
pub funded: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Runs {
pub planned: Run,
pub runs: Vec<Run>,
pub successes: usize,
pub bands: Vec<Band>,
}
impl Runs {
fn of(planned: Run, runs: Vec<Run>, start_year: i16) -> Self {
let years = planned.net_worth.len();
let bands = (0..years)
.map(|at| {
let year = start_year + i16::try_from(at).unwrap_or(i16::MAX);
let mut worths: Vec<Dollars> = runs.iter().map(|run| run.net_worth[at]).collect();
worths.sort_unstable();
let funded = runs
.iter()
.filter(|run| run.first_short.is_none_or(|short| short > year))
.count();
Band {
year,
net_worth: BAND_PERCENTILES
.map(|percentile| at_percentile(&worths, percentile).unwrap_or_default()),
funded: funded as f64 / runs.len().max(1) as f64,
}
})
.collect();
Self {
successes: runs.iter().filter(|run| run.is_success).count(),
planned,
runs,
bands,
}
}
#[must_use]
pub fn success_rate(&self) -> f64 {
self.successes as f64 / self.runs.len().max(1) as f64
}
#[must_use]
pub fn worst_first(&self) -> Vec<usize> {
let mut order: Vec<usize> = (0..self.runs.len()).collect();
order.sort_by_key(|&at| (Reverse(self.runs[at].rank), at));
order
}
}
fn at_percentile<T: Copy>(sorted: &[T], percentile: u8) -> Option<T> {
let last = sorted.len().checked_sub(1)?;
let at = (f64::from(percentile) / 100.0 * last as f64).round() as usize;
Some(sorted[at.min(last)])
}
#[derive(Debug, Default)]
pub struct Progress {
done: AtomicUsize,
is_cancelled: AtomicBool,
}
impl Progress {
#[must_use]
pub fn done(&self) -> usize {
self.done.load(Ordering::Relaxed)
}
pub fn cancel(&self) {
self.is_cancelled.store(true, Ordering::Relaxed);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.is_cancelled.load(Ordering::Relaxed)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum RunError {
Cancelled,
Refused(Vec<Issue>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct MonteCarlo {
pub runs: Runs,
pub singled_out: Vec<usize>,
}
fn run_range(
range: std::ops::Range<usize>,
progress: &Progress,
one: &(impl Fn(usize) -> Option<Run> + Sync),
) -> Vec<Option<Run>> {
range
.take_while(|_| !progress.is_cancelled())
.map(|at| {
let run = one(at);
progress.done.fetch_add(1, Ordering::Relaxed);
run
})
.collect()
}
fn run_all(
count: usize,
progress: &Progress,
one: impl Fn(usize) -> Option<Run> + Sync,
) -> Result<Vec<Run>, RunError> {
let threads = thread::available_parallelism()
.map_or(1, usize::from)
.min(count.max(1));
let chunk = count.div_ceil(threads).max(1);
let one = &one;
let finished: Vec<Vec<Option<Run>>> = thread::scope(|scope| {
let handles: Vec<_> = (0..count)
.step_by(chunk)
.map(|first| {
let range = first..(first + chunk).min(count);
scope.spawn(move || run_range(range, progress, one))
})
.collect();
handles
.into_iter()
.map(|handle| handle.join().expect("a run does not panic"))
.collect()
});
if progress.is_cancelled() {
return Err(RunError::Cancelled);
}
Ok(finished.into_iter().flatten().flatten().collect())
}
fn planned(plan: &Plan, tables: &TaxTables) -> Run {
let projection = project_on(plan, tables, &MarketPath::expected(plan));
Run::of(
RunName::Planned,
&projection,
plan.market().leave_at_least(),
)
}
pub fn monte_carlo(
plan: &Plan,
tables: &TaxTables,
history: &History,
progress: &Progress,
) -> Result<MonteCarlo, RunError> {
let market = plan.market();
let floor = market.leave_at_least();
let count = usize::try_from(market.trials()).unwrap_or_default();
let runs = run_all(count, progress, |at| {
let trial = u32::try_from(at).unwrap_or(u32::MAX);
let projection = project_on(plan, tables, &draw(plan, history, trial));
Some(Run::of(RunName::Trial(trial), &projection, floor))
})?;
let runs = Runs::of(planned(plan, tables), runs, plan.plan.start_year);
let worst_first = runs.worst_first();
let mut singled_out: Vec<usize> = BAND_PERCENTILES
.iter()
.rev()
.filter_map(|&percentile| at_percentile(&worst_first, percentile))
.collect();
singled_out.extend(worst_first.first());
Ok(MonteCarlo { runs, singled_out })
}
pub fn historical(
plan: &Plan,
tables: &TaxTables,
history: &History,
progress: &Progress,
) -> Result<Runs, RunError> {
let market = plan.market();
let (from, to) = (market.from(), market.to());
let covered = format!(
"the record covers {} to {}",
history.first_year(),
history.last_year()
);
let mut issues = Vec::new();
if from < history.first_year() {
push_issue(&mut issues, "market.historical.from", &covered);
}
if to > history.last_year() {
push_issue(&mut issues, "market.historical.to", &covered);
}
if !issues.is_empty() {
return Err(RunError::Refused(issues));
}
let floor = market.leave_at_least();
let count = usize::try_from(to - from + 1).unwrap_or_default();
let runs = run_all(count, progress, |at| {
let start = from + i16::try_from(at).unwrap_or(i16::MAX);
let path = start_in(plan, history, start, market.wrap())?;
Some(Run::of(
RunName::Start(start),
&project_on(plan, tables, &path),
floor,
))
})?;
if runs.is_empty() {
push_issue(
&mut issues,
"market.historical.wrap",
"no start year has a whole horizon of history; wrap, or start earlier",
);
return Err(RunError::Refused(issues));
}
Ok(Runs::of(planned(plan, tables), runs, plan.plan.start_year))
}
#[must_use]
pub fn replay(
plan: &Plan,
tables: &TaxTables,
history: &History,
name: RunName,
) -> Option<Projection> {
let path = match name {
RunName::Planned => MarketPath::expected(plan),
RunName::Trial(trial) => draw(plan, history, trial),
RunName::Start(start) => start_in(plan, history, start, plan.market().wrap())?,
};
Some(project_on(plan, tables, &path))
}