mod check;
mod fill;
mod ladder;
mod targets;
pub use ladder::{LADDER_ID_PREFIX, apply_ladder, is_ladder, ladder_overlay};
use serde::Serialize;
use crate::params::TaxTables;
use crate::plan::{Dollars, Issue, Plan};
use crate::project::{Projection, plan_inflation, project};
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 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);
Ok(OptimizedLadder {
baseline,
ladder: SweptBracket {
rate: bracket_rate,
steps,
optimized,
},
})
}
pub fn sweep_brackets(
plan: &Plan,
tables: &TaxTables,
options: &OptimizeOptions,
) -> Result<BracketSweep, Vec<Issue>> {
let issues = check_options(plan, tables, options, None);
if !issues.is_empty() {
return Err(issues);
}
let baseline = project(plan, tables);
let (bare, from) = without_ladder(plan, tables, &baseline);
let params = tables.params_for(plan.plan.start_year, &plan_inflation(plan));
let rates: Vec<f64> = params
.brackets
.for_status(plan.household.filing)
.iter()
.map(|bracket| bracket.rate)
.collect();
let brackets = rates
.iter()
.take(rates.len().saturating_sub(1))
.map(|&rate| {
let (steps, optimized) = search_ladder(&bare, tables, options, rate, &from);
SweptBracket {
rate,
steps,
optimized,
}
})
.collect();
Ok(BracketSweep { baseline, brackets })
}
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)
}