Alternatives and benefits (Python)

import math
import hecfdapy as fda
import matplotlib.pyplot as plt

The question

Is a flood-risk project worth building? HEC-FDA answers with equivalent annual damage (EqAD) reduced: compute EqAD without the project, compute it again with the project, and subtract. This example builds both sides from the ground up – a scenario, then an alternative, then the benefits comparison – chaining scenario_results(), alternative_ead(), and alternative_comparison() exactly the way a real HEC-FDA study does.

The annualization math alone

Before chaining the whole pipeline, look at the annualization step by itself. eqad() takes an EAD at a base year and an EAD at a future year and folds them into one equivalent annual number:

fda.eqad(35000, 2023, 50000, 2072, 50, 0.07)
38835.309867432676

That reproduces the upstream ComputeEEAD_Test oracle, 38835.31. Three steps produce it:

  1. Interpolate – linearly interpolate EAD between the base year and the most likely future year, one value per year of the analysis period. If the future year is reached before the period ends, every later year holds flat at the future value.
  2. Discount – convert each year’s EAD to present value at the given discount rate.
  3. Annualize (PVIFA) – divide the summed present value by the present-value-interest-factor of an annuity over the period, turning a lump sum back into one equivalent annual payment.

Here the base year is 2023, the future year is 2072, and the period of analysis is exactly 50 years – so the future year lands on the last year of the period and every year gets interpolated, with no flat tail:

base_value, base_year = 35000, 2023
future_value, future_year = 50000, 2072
period, rate = 50, 0.07

years = [base_year + i for i in range(period)]
interpolated = [base_value + (future_value - base_value) * i / (future_year - base_year)
               for i in range(period)]
discounted = [interpolated[i] / (1 + rate) ** (i + 1) for i in range(period)]
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(years, interpolated, color="#6b705c", linewidth=2, label="interpolated EAD")
ax.plot(years, discounted, color="#cb997e", linewidth=2, label="discounted to present value")
ax.set(xlabel="year", ylabel="EAD ($)")
ax.legend(frameon=False)

Summing the discounted column and dividing by the annuity factor reproduces the same 38835.31 eqad() returned above:

pvifa = (1 - 1 / (1 + rate) ** period) / rate
sum(discounted) / pvifa
38835.30986743268

Without-project alternative

Now the full pipeline. A HEC-FDA alternative needs a base-year and a future-year scenario, each computed from an impact-area simulation the same way the EAD-simulation example did. This construct is the upstream AlternativeResults_Test case: a Uniform(0, 100000) flow frequency, a linear flow-stage curve from (0, 0) to (100000, 30), and a 3-point stage-damage curve. The future-year damages are exactly double the base-year damages – the project has done nothing yet, so this is the without-project condition.

without_base_sim = dict(
    impact_area_id=1,
    flow_frequency=dict(dist="Uniform", params=[0, 100000, 1000]),
    flow_stage=dict(x=[0, 100000], dist="Uniform", params=[[0, 0, 10], [0, 30, 10]]),
    stage_damage=[dict(
        x=[0, 15, 30], dist="Uniform",
        params=[[0, 0, 10], [0, 600000, 1], [0, 600000, 1]],
        damage_category="residential", asset_category="structure",
    )],
)
without_future_sim = dict(without_base_sim, stage_damage=[dict(
    without_base_sim["stage_damage"][0],
    params=[[0, 0, 10], [0, 1200000, 10], [0, 1200000, 10]],
)])

Run each year through scenario_results(), then annualize both into one alternative with alternative_ead():

without_base = fda.scenario_results([without_base_sim], min_iterations=1, max_iterations=1,
                                    deterministic=True)
without_future = fda.scenario_results([without_future_sim], min_iterations=1, max_iterations=1,
                                      deterministic=True)

without = fda.alternative_ead(
    without_base, without_future,
    base_year=2023, future_year=2072, period_of_analysis=50, discount_rate=0.0275,
    alternative_id=1,
)
without["mean_eqad"], without["base_year_ead"], without["future_year_ead"]
(208213.8040325943, 149999.99849999996, 299999.9969999999)

Mean EqAD comes out to 208213.80, the AlternativeResults_Test oracle for this exact construct (period 50, base year 2023, future year 2072, discount rate 2.75%), with a base-year EAD near 150000 and a future-year EAD near 300000 – twice as much damage once the future-year curve takes over.

With-project alternative

A project reduces damage. Here that project is a levee: 0% failure probability below stage 7.4999, 100% failure probability at 7.5 and above, sitting at a top_of_levee_elevation of 7.5. Everything else – the flow frequency, the flow-stage curve, the stage-damage curve, the base/future years, discount rate, and period – stays exactly the same as the without-project case:

levee_spec = dict(
    x=[0, 7.4999, 7.5], dist="Deterministic", params=[[0], [0], [1]],
    damage_category="residential", asset_category="structure",
    top_of_levee_elevation=7.5,
)
with_base_sim = dict(without_base_sim, levee=levee_spec)
with_future_sim = dict(without_future_sim, levee=levee_spec)

with_base = fda.scenario_results([with_base_sim], min_iterations=1, max_iterations=1,
                                 deterministic=True)
with_future = fda.scenario_results([with_future_sim], min_iterations=1, max_iterations=1,
                                   deterministic=True)

with_project = fda.alternative_ead(
    with_base, with_future,
    base_year=2023, future_year=2072, period_of_analysis=50, discount_rate=0.0275,
    alternative_id=2,
)
with_project["mean_eqad"]
161365.6976567101

Mean EqAD drops to 161365.70 – lower than the without-project 208213.80, because the levee holds back damage below stage 7.5 in both the base and future year.

Benefits

The benefit of the project is the damage the levee prevents. alternative_comparison() subtracts the with-project alternative’s damage distributions from the without-project alternative’s and reports the reduction:

benefits = fda.alternative_comparison(without, with_project)
benefits["reduced"]
[{'alternative_id': 2,
  'eqad_reduced': 46848.10637588418,
  'base_year_ead_reduced': 33750.000000049986,
  'future_year_ead_reduced': 67500.00000009997,
  'with_project_eqad': 161365.6976567101}]

The base-year EAD is reduced by 33750 and the future-year EAD by 67500 – these are the AlternativeComparisonReportShould-style oracle values for this construct, and they are exactly what a levee holding back stages below 7.5 should prevent: a fixed share of both the base and future damage, scaled by how much of each curve sits under the levee’s protection.

An asymmetric risk row

HEC-FDA 2.1 preserves a risk row that exists on only one side of a comparison. This compact accounting case adds non-failure damage only to the with-project condition. It is a numerical identity check, not a proposed project design, so the added damage produces a negative Non_Fail reduction.

role_curve = dict(
    x=[10, 11, 13, 17, 17.5], dist="Deterministic",
    params=[[0], [6000], [14000], [150000], [500000]],
    damage_category="residential", asset_category="Structure",
)
role_sim = dict(
    impact_area_id=1,
    flow_frequency=dict(dist="Uniform", params=[0, 1000, 5]),
    flow_stage=dict(
        x=[0, 250, 500, 750, 1000], dist="Deterministic",
        params=[[10], [11], [13], [17], [17.5]],
    ),
    stage_damage=[role_curve],
    levee=dict(
        x=[10, 11, 13, 17, 17.5], dist="Deterministic",
        params=[[0], [0], [0.1], [0.8], [1]],
        damage_category="residential", asset_category="Structure",
        top_of_levee_elevation=17,
    ),
)
role_sim_with = dict(role_sim, non_failure_stage_damage=[role_curve])
def make_role_alternative(simulation, alternative_id):
    base = fda.scenario_results([simulation], 1, 1, True)
    future = fda.scenario_results([simulation], 1, 1, True)
    return fda.alternative_ead(
        base, future, base_year=2023, future_year=2072,
        period_of_analysis=50, discount_rate=0.0275,
        alternative_id=alternative_id,
    )


role_without = make_role_alternative(role_sim, 11)
role_with = make_role_alternative(role_sim_with, 12)
role_benefits = fda.alternative_comparison(role_without, role_with)
role_benefits["reduced_by_risk"]
[{'alternative_id': 12,
  'risk_type': 'Fail',
  'eqad_reduced': 0.0,
  'base_year_ead_reduced': 0.0,
  'future_year_ead_reduced': 0.0},
 {'alternative_id': 12,
  'risk_type': 'Non_Fail',
  'eqad_reduced': -12150.000599999996,
  'base_year_ead_reduced': -12150.000599999996,
  'future_year_ead_reduced': -12150.000599999996}]
assert [row["risk_type"] for row in role_benefits["reduced_by_risk"]] == [
    "Fail", "Non_Fail"
]
assert math.isclose(
    role_benefits["reduced"][0]["eqad_reduced"],
    sum(row["eqad_reduced"] for row in role_benefits["reduced_by_risk"]),
)
assert math.isclose(
    role_without["mean_eqad"] - role_with["mean_eqad"],
    role_benefits["reduced"][0]["eqad_reduced"],
)

The exact-risk rows sum to the legacy aggregate. A missing counterpart is represented by a zero row with the present category and risk identity, so the asymmetric Non_Fail result is retained.

Single-use handles and validation

scenario_results() and alternative_ead() both return a "handle" alongside their summary values. Each handle is single-use: alternative_ead() takes ownership of the scenario handles it is given, and alternative_comparison() takes ownership of the alternative handles it is given. A handle already consumed cannot be reused – recompute the scenario or alternative if you need it again. That is why this example computes without_base/without_future and with_base/with_future as four separate scenario calls rather than trying to share one.

None of the numbers above are made up. 38835.31, 208213.80, 161365.70, 33750, and 67500 are the same literals the dotnet oracle gate (make oracles) checks against the real HEC.FDA.Model.Alternative/AlternativeComparisonReport, and this example chains Scenario.Compute -> Alternative.AnnualizationCompute -> AlternativeComparisonReport.ComputeAlternativeComparisonReport exactly the way the upstream AlternativeComparisonReportShould-style tests do – the full stage-frequency -> stage-damage -> EAD -> EqAD -> benefits pipeline, validated end to end against the real HEC-FDA.