Skip to content

Maximize Sizing Below a Specified Payback Period - #640

Draft
Bill-Becker wants to merge 9 commits into
masterfrom
min-payback
Draft

Bill-Becker wants to merge 9 commits into
masterfrom
min-payback

Conversation

@Bill-Becker

@Bill-Becker Bill-Becker commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

This pull request introduces a new feature to constrain technology sizing based on a maximum simple payback period, along with supporting changes throughout the codebase. The main changes include extending the financial model to support a max_simple_payback_years constraint, updating technology selection logic, adding a lexicographic (maximize-size, then minimize-lifecycle-cost) solve, and updating results reporting and testing.

Financial model and tech selection enhancements:

  • Added max_simple_payback_years and bau_year_one_operating_cost fields to the Financial struct, enabling users to specify a maximum allowed simple payback period for new technologies. bau_year_one_operating_cost comes from a prior BAU solve's results["Financial"]["year_one_operating_cost_before_tax_model"] and is required whenever max_simple_payback_years is set. (src/core/financial.jl)
  • Extended the Techs struct and related logic to include a maximize_size field, which tracks technologies eligible for maximization under the payback constraint (all techs except ExistingBoiler/ExistingChiller). (src/core/types.jl, src/core/techs.jl)
  • BAUScenario now clears both max_simple_payback_years and bau_year_one_operating_cost, so the BAU reference solve is never itself payback-constrained. (src/core/bau_scenario.jl)

Model building (build_reopt!):

  • Builds an undiscounted Year1OperatingCost expression (elec bill, CHP standby charges, fixed/variable/per-hour O&M, GHP and storage O&M, and fuel costs) the same way regardless of whether max_simple_payback_years is set, so a BAU solve's value(m[:Year1OperatingCost]) is directly comparable to the optimal model's.
  • When max_simple_payback_years is set, adds PaybackSavings (BAU year-one operating cost minus the model's year-one operating cost) and constrains InitialCapexNoIncentives <= max_simple_payback_years * PaybackSavings, with a small absolute tolerance so floating-point noise can't turn a satisfiable case (e.g. all new techs at size 0) into a false infeasibility.
  • Builds a MaximizeSizeObjective expression over dvSize for techs.maximize_size plus dvStoragePower/dvStorageEnergy for all storage when the payback constraint is active. It uses sizes (not production) so it behaves correctly for both intermittent (PV, Wind) and dispatchable techs.
  • build_reopt! no longer adds any size-maximizing term to the Costs objective expression — the previous -1e4 * size penalty-style incentive inside Costs was removed in favor of the lexicographic solve below. MaximizeProductionIncentive is retained as 0.0 so existing results/objective plumbing is unchanged.

Lexicographic solve (optimize_reopt!):

  • New optimize_reopt!, used by run_reopt(m, p), replaces the single optimize! call:
    1. If max_simple_payback_years is set and there is anything to maximize, maximize MaximizeSizeObjective subject to the payback constraint, then pin it with MaximizeSizeObjective >= max_size - tol.
    2. Minimize the usual Costs + ObjectivePenalties objective while holding that maximum size.
  • This guarantees the maximum size within the payback bound is chosen even when the lifecycle-cost-optimal size is smaller, without relying on a tuned penalty coefficient that could cause MIP numerical instability. Total solve time across both stages is reported as opt_time.
  • Multinode: run_reopt(m, ps) now throws an explicit error if any node sets max_simple_payback_years, since the feature is not supported for multinode solves. (src/core/reopt_multinode.jl)

Results:

  • Financial results report year_one_operating_cost_before_tax_model (the model's undiscounted year-one operating cost, used to seed bau_year_one_operating_cost) and retain maximize_tech_size_incentive_value (now always 0.0). (src/results/financial.jl)

Testing:

  • Tests for the max_simple_payback_years threshold behavior covering unconstrained, loose, and strict payback scenarios, that BAUScenario clears the payback inputs, and a high-lifecycle-cost case (1-year analysis, expensive PV) where the LCC-optimal size is 0 but the lexicographic solve still sizes PV up to a ~9-year payback. (test/runtests.jl)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical BAU workflow and sizing-maximization issues remain, and multinode support does not enforce the requested constraint.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity · 1 Medium severity

Open (4)
What changed in this PR

Adds maximum simple-payback constraints, technology sizing incentives, financial reporting, and related tests.

Changes:

  • Adds payback configuration and eligible-technology tracking.
  • Enforces payback limits and sizing incentives.
  • Updates multinode handling, reporting, and tests.
File Summary
test/​runtests.jl Adds payback sizing tests.
src/​results/​financial.jl Reports payback-related financial values.
src/​core/​types.jl Adds maximization metadata.
src/​core/​techs.jl Identifies eligible technologies.
src/​core/​reopt.jl Implements payback constraints and incentives; BAU handling, capex selection, and maximization logic require changes.
src/​core/​reopt_multinode.jl Multinode models currently ignore the payback constraint and incentive.
src/​core/​financial.jl Adds payback inputs; BAU fields must be cleared and inputs documented.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/core/financial.jl
min_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing,
max_initial_capital_costs_before_incentives::Union{Nothing,Real} = nothing,
max_simple_payback_years::Union{Nothing,Real} = nothing, # find max size/production of maximize_size techs while keeping simple payback <= this value
bau_year_one_operating_cost::Union{Nothing,Real} = nothing, # BAU results["Financial"]["year_one_operating_cost_before_tax_model"] (from a prior BAUInputs solve), required if max_simple_payback_years is set

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 93ff1e9.

Comment thread src/core/reopt.jl
Comment on lines +531 to +533
if !isnothing(p.s.financial.max_simple_payback_years)
if isnothing(p.s.financial.bau_year_one_operating_cost)
throw(@error("financial.bau_year_one_operating_cost must be set (from a prior BAU run's value(m[:Year1OperatingCost]), see results[\"Financial\"][\"year_one_operating_cost_before_tax_model\"]) to constrain max_simple_payback_years."))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 93ff1e9.

Comment thread src/core/reopt.jl Outdated
Comment on lines +551 to +555
m[:MaximizeProductionIncentive] = @expression(m,
-1.0e4 * (
sum(m[:dvSize][t] for t in p.techs.maximize_size; init=0.0) +
sum(m[:dvStoragePower][b] + m[:dvStorageEnergy][b] for b in storage_to_maximize; init=0.0)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 93ff1e9 with a lexicographic maximize-then-minimize solve.

Comment on lines +129 to +130
ex_name = "MaximizeProductionIncentive"*_n
m[Symbol(ex_name)] = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 93ff1e9 by rejecting financial.max_simple_payback_years in multinode solves.

Co-authored-by: Bill-Becker <42586683+Bill-Becker@users.noreply.github.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants