Skip to content

Solver

collegeplan.solver

Required-savings solver using bisection root-finding.

solve_required_savings(children, assumptions, household_fund=None, target_funding_ratio=1.0, solve_mode='child_level', tolerance=1.0, max_iterations=100)

Find the annual contribution needed to reach a target funding ratio.

Parameters:

Name Type Description Default
children list[Child]

List of children to plan for.

required
assumptions Assumptions

Return and inflation assumptions.

required
household_fund HouseholdFund | None

Optional shared pool.

None
target_funding_ratio float

Desired funding level (1.0 = fully funded).

1.0
solve_mode str

"child_level" distributes contribution among children weighted by projected cost, "shared_pool" adds it to the household fund.

'child_level'
tolerance float

Convergence tolerance in dollars.

1.0
max_iterations int

Maximum bisection iterations.

100
Source code in src/collegeplan/solver.py
def solve_required_savings(
    children: list[Child],
    assumptions: Assumptions,
    household_fund: HouseholdFund | None = None,
    target_funding_ratio: float = 1.0,
    solve_mode: str = "child_level",
    tolerance: float = 1.0,
    max_iterations: int = 100,
) -> SavingsSolution:
    """Find the annual contribution needed to reach a target funding ratio.

    Args:
        children: List of children to plan for.
        assumptions: Return and inflation assumptions.
        household_fund: Optional shared pool.
        target_funding_ratio: Desired funding level (1.0 = fully funded).
        solve_mode: "child_level" distributes contribution among children
            weighted by projected cost, "shared_pool" adds it to the
            household fund.
        tolerance: Convergence tolerance in dollars.
        max_iterations: Maximum bisection iterations.
    """
    validate_plan(children, assumptions, household_fund)

    # Compute cost-based weights for child_level distribution
    weights = _cost_weights(children, assumptions) if solve_mode == "child_level" else None

    # Check if already funded with zero additional contribution
    current_ratio = _run_with_contribution(
        children, assumptions, household_fund, 0.0, solve_mode, weights
    )
    if current_ratio >= target_funding_ratio:
        return SavingsSolution(
            required_annual_contribution=0.0,
            required_monthly_contribution=0.0,
            per_child_suggestions={c.name: 0.0 for c in children},
            achieved_funding_ratio=current_ratio,
        )

    # Establish upper bound
    total_cost = sum(
        c.cost_profile.current_total_cost
        * (1 + c.cost_profile.annual_cost_growth) ** int(c.start_age - c.current_age)
        * c.attendance_years
        for c in children
    )
    min_years = max(1, min(int(c.start_age - c.current_age) for c in children))
    upper = total_cost / min_years

    # Verify upper bound is sufficient
    upper_ratio = _run_with_contribution(
        children, assumptions, household_fund, upper, solve_mode, weights
    )
    while upper_ratio < target_funding_ratio:
        upper *= 2
        upper_ratio = _run_with_contribution(
            children, assumptions, household_fund, upper, solve_mode, weights
        )
        if upper > total_cost * 10:
            raise SolverError("Cannot find a feasible contribution within reasonable bounds")

    # Bisection
    lo, hi = 0.0, upper
    for _ in range(max_iterations):
        mid = (lo + hi) / 2
        ratio = _run_with_contribution(
            children, assumptions, household_fund, mid, solve_mode, weights
        )
        if ratio < target_funding_ratio:
            lo = mid
        else:
            hi = mid
        if hi - lo < tolerance:
            break
    else:
        raise SolverError(f"Solver did not converge after {max_iterations} iterations")

    annual = (lo + hi) / 2
    monthly = annual / 12

    if weights is not None:
        per_child = {c.name: annual * weights[c.name] for c in children}
    else:
        n = len(children)
        per_child = {c.name: annual / n for c in children}

    # Verify achieved ratio
    achieved = _run_with_contribution(
        children, assumptions, household_fund, annual, solve_mode, weights
    )

    return SavingsSolution(
        required_annual_contribution=annual,
        required_monthly_contribution=monthly,
        per_child_suggestions=per_child,
        achieved_funding_ratio=achieved,
    )