mod check;
mod fill;
mod ladder;
mod targets;
pub use ladder::{LADDER_ID_PREFIX, apply_ladder, is_ladder, ladder_overlay};
use serde::Serialize;
use super::rank_key;
use crate::params::TaxTables;
use crate::plan::{Dollars, Issue, Plan, TreatmentClass};
use crate::project::{Projection, plan_inflation, project};
use crate::search::{Progress, RunError, run_all};
use check::check_options;
use fill::search_ladder;
#[derive(Debug, Clone)]
pub struct OptimizeOptions {
pub sources: Vec<String>,
pub destination: String,
pub start_year: Option<i16>,
pub end_year: Option<i16>,
pub annual_max: Option<Dollars>,
pub total_max: Option<Dollars>,
pub headroom: Dollars,
pub irmaa_tier: Option<u8>,
pub max_magi: Option<Dollars>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct LadderStep {
pub year: i16,
pub source: String,
pub amount: Dollars,
}
#[derive(Debug, Clone)]
pub struct OptimizedLadder {
pub baseline: Projection,
pub ladder: SweptBracket,
}
#[derive(Debug, Clone)]
pub struct BracketSweep {
pub baseline: Projection,
pub brackets: Vec<SweptBracket>,
}
#[derive(Debug, Clone)]
pub struct SweptBracket {
pub rate: f64,
pub steps: Vec<LadderStep>,
pub optimized: Projection,
}
impl SweptBracket {
#[must_use]
pub fn converted(&self, deflated: bool) -> Dollars {
if !deflated {
return self.steps.iter().map(|step| step.amount).sum();
}
self.steps
.iter()
.map(|step| self.optimized.deflate_in(step.year, step.amount))
.sum()
}
}
const UNBOUNDED: Dollars = 1_000_000_000_000;
const RATE_EPSILON: f64 = 1e-6;
pub fn optimize_conversions(
plan: &Plan,
tables: &TaxTables,
options: &OptimizeOptions,
bracket_rate: f64,
) -> Result<OptimizedLadder, Vec<Issue>> {
let options = &with_default_sources(plan, options);
let issues = check_options(plan, tables, options, Some(bracket_rate));
if !issues.is_empty() {
return Err(issues);
}
let baseline = project(plan, tables);
let (bare, from) = without_ladder(plan, tables, &baseline);
let (steps, optimized) = search_ladder(
&bare,
tables,
(options, bracket_rate),
&from,
&Progress::default(),
);
Ok(OptimizedLadder {
baseline,
ladder: SweptBracket {
rate: bracket_rate,
steps,
optimized,
},
})
}
pub fn sweep_brackets(
plan: &Plan,
tables: &TaxTables,
options: &OptimizeOptions,
progress: &Progress,
) -> Result<BracketSweep, RunError> {
let options = &with_default_sources(plan, options);
let issues = check_options(plan, tables, options, None);
if !issues.is_empty() {
return Err(RunError::Refused(issues));
}
if progress.is_cancelled() {
return Err(RunError::Cancelled);
}
let baseline = project(plan, tables);
let (bare, from) = without_ladder(plan, tables, &baseline);
let rates = fillable_rates(plan, tables);
let mut brackets = run_all(rates.len(), progress, |at| {
let rate = rates[at];
let (steps, optimized) = search_ladder(&bare, tables, (options, rate), &from, progress);
Some(SweptBracket {
rate,
steps,
optimized,
})
})?;
brackets.sort_by_cached_key(|bracket| rank_key(&bracket.optimized));
Ok(BracketSweep { baseline, brackets })
}
fn fillable_rates(plan: &Plan, tables: &TaxTables) -> Vec<f64> {
let params = tables.params_for(plan.plan.start_year, &plan_inflation(plan));
let mut rates: Vec<f64> = params
.brackets
.for_status(plan.household.filing)
.iter()
.map(|bracket| bracket.rate)
.collect();
rates.pop();
rates
}
fn with_default_sources(plan: &Plan, options: &OptimizeOptions) -> OptimizeOptions {
let mut options = options.clone();
if options.sources.is_empty() {
let owner = plan
.account(&options.destination)
.map(|account| &account.owner);
options.sources = plan
.accounts
.iter()
.filter(|account| {
account.treatment() == TreatmentClass::Deferred && Some(&account.owner) == owner
})
.map(|account| account.id.clone())
.collect();
}
options
}
fn without_ladder(plan: &Plan, tables: &TaxTables, baseline: &Projection) -> (Plan, Projection) {
let mut bare = plan.clone();
bare.conversions.retain(|conversion| !is_ladder(conversion));
let from = if bare.conversions.len() == plan.conversions.len() {
baseline.clone()
} else {
project(&bare, tables)
};
(bare, from)
}