Skip to content

Commit 66b6aee

Browse files
committed
Add dev doc file for payback with max sizes.
1 parent a12fa8b commit 66b6aee

1 file changed

Lines changed: 82 additions & 0 deletions

File tree

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# `max_simple_payback_years` "maximize size" feature (`src/core/reopt.jl`)
2+
3+
> Dev notes — not yet wired into `docs/make.jl` nav, so this page isn't published to
4+
> the doc site. Promote it (add to `make.jl`) once the design has settled.
5+
6+
## Design intent
7+
8+
Among solutions satisfying `simple_payback <= max_simple_payback_years`, push
9+
toward the largest capacity/size, not just the cost-optimal size — capturing extra
10+
value from generation/energy even when the incremental capex/opex isn't as
11+
cost-effective as the smaller optimal size.
12+
13+
This is implemented as a two-stage lexicographic solve in `optimize_reopt!`:
14+
1. Maximize `MaximizeSizeObjective` subject to the `max_simple_payback_years` constraint.
15+
2. Lock that maximum (within a small tolerance) and minimize lifecycle cost (`Costs`)
16+
while holding it, so the specific tech mix chosen for that size is the cheapest one.
17+
18+
## Why size-based (`dvSize`/`dvStoragePower`), not production-based
19+
20+
- Chosen deliberately over incentivizing production (`dvRatedProduction`), after
21+
weighing pros/cons.
22+
- Size-based avoids the degenerate case: bigger nameplate capacity with no more
23+
energy produced (pure added cost, no value).
24+
- Production-based has the mirror-image risk for dispatchable/fuel-burning techs
25+
(Generator, CHP): the optimizer can satisfy a production incentive by just
26+
dispatching more during less cost-effective/uneconomical hours, without growing
27+
size at all — real fuel/O&M costs per kWh already compete against any artificial
28+
per-kWh incentive, so it either does nothing (incentive too weak) or distorts
29+
dispatch (incentive too strong) instead of reliably growing size.
30+
- For intermittent techs (PV, Wind) size and production are ~proportional anyway,
31+
so the choice barely matters there; the divergence is specific to dispatchable
32+
techs, which is why size was generalized to work for `p.techs.maximize_size`
33+
(all techs except ExistingBoiler/ExistingChiller) plus storage power capacity.
34+
- A possible future "maximize production" option/toggle was explicitly deferred as
35+
a separate feature — not implemented, no current plan to add it.
36+
37+
## `dvStorageEnergy` excluded from `MaximizeSizeObjective`
38+
39+
`MaximizeSizeObjective` sums `dvSize[t]` (`maximize_size` techs) plus
40+
`dvStoragePower[b]` only — `dvStorageEnergy[b]` (kWh) is intentionally excluded.
41+
42+
`add_storage_size_constraints` ties energy to power via duration bounds:
43+
44+
```julia
45+
dvStorageEnergy[b] <= dvStoragePower[b] * max_duration_hours
46+
dvStorageEnergy[b] >= dvStoragePower[b] * min_duration_hours
47+
```
48+
49+
If kWh were included in the stage-1 objective, stage 1 would push energy to the
50+
duration ceiling "for free" (more objective units, no cost weighting), consuming
51+
real capex against the payback budget and crowding out PV/battery-kW growth, even
52+
when that duration wasn't economically justified.
53+
54+
With kWh excluded, stage 1 only maximizes nameplate power/capacity (consistent
55+
across PV/Wind/CHP/battery-kW); stage 2 picks battery kWh/duration purely on
56+
economics, bounded only by the existing min/max duration constraints. This applies
57+
to all storage types in `p.s.storage.types.all` (elec/hot/cold), not just
58+
`ElectricStorage`, for consistency, even though the duration-ratio constraint
59+
itself only exists for `ElectricStorage`.
60+
61+
## Related: NPV>=0 vs simple-payback approach
62+
63+
- Old stale branch `NatLabRockies/REopt.jl@max_size_positive_npv` (604 commits
64+
behind master) implements a genuinely different, NPV-like mechanism:
65+
`Costs <= bau_lcc + MaximizeProductionIncentive` (full discounted LCC comparison,
66+
using `p.s.financial.bau_lcc`) combined with a production-based incentive
67+
(`ProductionByTech`, coefficient -10). This is the likely ancestor of the
68+
`MaximizeProductionIncentive` naming in current code.
69+
- Current codebase uses `bau_year_one_operating_cost` + `max_simple_payback_years`
70+
(undiscounted single-year payback), which is a different/stricter criterion than
71+
full lifecycle NPV.
72+
- NPV>=0 is intended to remain a SEPARATE, not-yet-implemented feature — do not
73+
conflate with the current payback-years-based maximize-size constraint.
74+
75+
## Known numerical gotcha (already fixed)
76+
77+
`Year1OperatingCost` (JuMP expression) vs externally-computed
78+
`bau_year_one_operating_cost` (Float64) can differ by ~1e-7 floating point noise;
79+
amplified by the `max_simple_payback_years` multiplier this can flip a
80+
truly-feasible constraint to INFEASIBLE. Fixed via a `payback_tol` epsilon added
81+
to the RHS of the payback constraint (see `reopt.jl` comments around
82+
`PaybackSavings`/`payback_tol`).

0 commit comments

Comments
 (0)