Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions aca_calc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""ACA Premium Tax Credit Calculator."""

__version__ = "0.1.0"
5 changes: 5 additions & 0 deletions aca_calc/calculations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""ACA Calculator calculation modules."""

from aca_calc.calculations.household import build_household_situation

__all__ = ["build_household_situation"]
126 changes: 126 additions & 0 deletions aca_calc/calculations/charts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Chart creation functions for ACA calculator."""

import numpy as np
import plotly.graph_objects as go
from policyengine_us import Simulation

from aca_calc.calculations.household import build_household_situation
from aca_calc.calculations.reforms import create_enhanced_ptc_reform


# PolicyEngine brand colors
COLORS = {
"primary": "#2C6496",
"secondary": "#39C6C0",
"green": "#28A745",
"gray": "#BDBDBD",
"blue_gradient": ["#D1E5F0", "#92C5DE", "#2166AC", "#053061"],
}


def add_logo_to_layout():
"""Add PolicyEngine logo to chart layout."""
import base64

try:
with open("blue.png", "rb") as f:
logo_base64 = base64.b64encode(f.read()).decode()
return {
"images": [
{
"source": f"data:image/png;base64,{logo_base64}",
"xref": "paper",
"yref": "paper",
"x": 1.01,
"y": -0.18,
"sizex": 0.10,
"sizey": 0.10,
"xanchor": "right",
"yanchor": "bottom",
}
]
}
except:
return {}


def create_ptc_charts(
age_head,
age_spouse,
dependent_ages,
state,
county=None,
zip_code=None,
income=None,
):
"""Create PTC comparison and difference charts.

Args:
age_head: Age of head of household
age_spouse: Age of spouse (None if not married)
dependent_ages: List/tuple of dependent ages
state: Two-letter state code
county: County name
zip_code: 5-digit ZIP code
income: Optional income to mark on chart

Returns:
tuple: (comparison_fig, delta_fig, benefit_info, income_range,
ptc_baseline_range, ptc_reform_range, slcsp, fpl, x_axis_max)
"""
# Convert tuple to list for household builder
dependent_ages = list(dependent_ages) if dependent_ages else []

# Build household with axes
base_household = build_household_situation(
age_head=age_head,
age_spouse=age_spouse,
dependent_ages=dependent_ages,
state=state,
county=county,
zip_code=zip_code,
year=2026,
with_axes=True,
)

try:
reform = create_enhanced_ptc_reform()

# Run simulations
sim_baseline = Simulation(situation=base_household)
sim_reform = Simulation(situation=base_household, reform=reform)

income_range = sim_baseline.calculate(
"employment_income", map_to="household", period=2026
)
ptc_range_baseline = sim_baseline.calculate(
"aca_ptc", map_to="household", period=2026
)
ptc_range_reform = sim_reform.calculate(
"aca_ptc", map_to="household", period=2026
)

# Calculate Medicaid and CHIP
medicaid_range = sim_baseline.calculate(
"medicaid_cost", map_to="household", period=2026
)
chip_range = sim_baseline.calculate(
"per_capita_chip", map_to="household", period=2026
)

# Find x-axis range
max_income_with_ptc = 200000
for i in range(len(ptc_range_reform) - 1, -1, -1):
if ptc_range_reform[i] > 0:
max_income_with_ptc = income_range[i]
break
x_axis_max = min(1000000, max_income_with_ptc * 1.1)

delta_range = ptc_range_reform - ptc_range_baseline

# TODO: Implement full chart creation
# For now, return None to keep refactoring incremental
return None, None, None, income_range, ptc_range_baseline, ptc_range_reform, 0, 0, x_axis_max

except Exception as e:
raise Exception(f"Chart creation error: {str(e)}") from e
99 changes: 99 additions & 0 deletions aca_calc/calculations/household.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
Household situation building utilities for PolicyEngine simulations.
"""


def build_household_situation(
age_head,
age_spouse,
dependent_ages,
state,
county=None,
zip_code=None,
year=2026,
with_axes=False,
):
"""Build a PolicyEngine household situation.

Args:
age_head: Age of head of household
age_spouse: Age of spouse (None if not married)
dependent_ages: List of dependent ages
state: Two-letter state code (e.g., "CA")
county: County name (e.g., "Los Angeles County")
zip_code: 5-digit ZIP code (required for LA County)
year: Year for simulation
with_axes: If True, add employment_income axis for income sweep

Returns:
dict: PolicyEngine situation dictionary
"""
situation = {
"people": {"you": {"age": {year: age_head}}},
"families": {"your family": {"members": ["you"]}},
"spm_units": {"your household": {"members": ["you"]}},
"tax_units": {"your tax unit": {"members": ["you"]}},
"households": {
"your household": {"members": ["you"], "state_name": {year: state}}
},
}

# Add axes if requested (for income sweeps)
if with_axes:
situation["axes"] = [
[
{
"name": "employment_income",
"count": 10_001,
"min": 0,
"max": 1000000,
"period": year,
}
]
]

# Add county if provided
if county:
county_pe_format = county.upper().replace(" ", "_") + "_" + state
situation["households"]["your household"]["county"] = {
year: county_pe_format
}

# Add ZIP code if provided (required for LA County)
if zip_code:
situation["households"]["your household"]["zip_code"] = {year: zip_code}

# Add spouse if married
if age_spouse:
situation["people"]["your partner"] = {"age": {year: age_spouse}}
situation["families"]["your family"]["members"].append("your partner")
situation["spm_units"]["your household"]["members"].append("your partner")
situation["tax_units"]["your tax unit"]["members"].append("your partner")
situation["households"]["your household"]["members"].append("your partner")
situation["marital_units"] = {
"your marital unit": {"members": ["you", "your partner"]}
}

# Add dependents
for i, dep_age in enumerate(dependent_ages):
if i == 0:
child_id = "your first dependent"
elif i == 1:
child_id = "your second dependent"
else:
child_id = f"dependent_{i+1}"

situation["people"][child_id] = {"age": {year: dep_age}}
situation["families"]["your family"]["members"].append(child_id)
situation["spm_units"]["your household"]["members"].append(child_id)
situation["tax_units"]["your tax unit"]["members"].append(child_id)
situation["households"]["your household"]["members"].append(child_id)

# Add child's marital unit
if "marital_units" not in situation:
situation["marital_units"] = {}
situation["marital_units"][f"{child_id}'s marital unit"] = {
"members": [child_id]
}

return situation
75 changes: 75 additions & 0 deletions aca_calc/calculations/ptc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Premium Tax Credit calculation functions."""

import copy
from policyengine_us import Simulation

from aca_calc.calculations.household import build_household_situation
from aca_calc.calculations.reforms import create_enhanced_ptc_reform


def calculate_ptc(
age_head,
age_spouse,
income,
dependent_ages,
state,
county_name=None,
zip_code=None,
use_reform=False,
):
"""Calculate PTC for baseline or IRA enhanced scenario using 2026 comparison.

Args:
age_head: Age of head of household
age_spouse: Age of spouse (None if not married)
income: Annual household income
dependent_ages: List of dependent ages
state: Two-letter state code
county_name: County name (e.g., "Travis County")
zip_code: 5-digit ZIP code (required for LA County)
use_reform: If True, use enhanced PTC reform

Returns:
tuple: (ptc, slcsp, fpl, fpl_pct)
"""
try:
# Build base household situation
situation = build_household_situation(
age_head=age_head,
age_spouse=age_spouse,
dependent_ages=dependent_ages,
state=state,
county=county_name,
zip_code=zip_code,
year=2026,
with_axes=False,
)

# Deep copy and inject income
sit = copy.deepcopy(situation)

# Split income between adults if married
if age_spouse:
sit["people"]["you"]["employment_income"] = {2026: income / 2}
sit["people"]["your partner"]["employment_income"] = {
2026: income / 2
}
else:
sit["people"]["you"]["employment_income"] = {2026: income}

# Create reform if requested
reform = create_enhanced_ptc_reform() if use_reform else None

# Run simulation
sim = Simulation(situation=sit, reform=reform)

ptc = sim.calculate("aca_ptc", map_to="household", period=2026)[0]
slcsp = sim.calculate("slcsp", map_to="household", period=2026)[0]
fpl = sim.calculate("tax_unit_fpg", period=2026)[0]
aca_magi_fraction = sim.calculate("aca_magi_fraction", period=2026)[0]
fpl_pct = aca_magi_fraction * 100

return float(max(0, ptc)), float(slcsp), float(fpl), float(fpl_pct)

except Exception as e:
raise Exception(f"PTC calculation error: {str(e)}") from e
40 changes: 40 additions & 0 deletions aca_calc/calculations/reforms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""PolicyEngine reform definitions for ACA scenarios."""

from policyengine_core.reforms import Reform


def create_enhanced_ptc_reform():
"""Create reform extending enhanced PTCs through 2026.

Returns:
Reform: PolicyEngine reform object
"""
return Reform.from_dict(
{
"gov.aca.ptc_phase_out_rate[0].amount": {
"2026-01-01.2100-12-31": 0
},
"gov.aca.ptc_phase_out_rate[1].amount": {
"2025-01-01.2100-12-31": 0
},
"gov.aca.ptc_phase_out_rate[2].amount": {
"2026-01-01.2100-12-31": 0
},
"gov.aca.ptc_phase_out_rate[3].amount": {
"2026-01-01.2100-12-31": 0.02
},
"gov.aca.ptc_phase_out_rate[4].amount": {
"2026-01-01.2100-12-31": 0.04
},
"gov.aca.ptc_phase_out_rate[5].amount": {
"2026-01-01.2100-12-31": 0.06
},
"gov.aca.ptc_phase_out_rate[6].amount": {
"2026-01-01.2100-12-31": 0.085
},
"gov.aca.ptc_income_eligibility[2].amount": {
"2026-01-01.2100-12-31": True
},
},
country_id="us",
)
Loading