BTM economics — PV + BESS at a 100 MW DC anchor
v3 update (2026-05-08): Fixed a partial-year averaging bug in
btm_economics.evaluate that biased every NPV downward by ~52 M RM uniformly
(MYT 2025 partial year of 7 hours / 1 MWh was being included in the
avg_annual_savings_rm denominator alongside 5 full years). Net effect:
PV-only NPV @ 60 MW jumps from +30.5 → +82.7 M; IRR 9.9% → 13.0%; payback
9 → 8 years. PV+BESS uniformly +52 M but still net negative. Tornado
recalibrated against new LP base (pv_calibration 0.9322 → 1.1186,
bess_md_capture 0.5045 → 0.6611). Aggregator and trigger constants updated
(PV per-MW: 432 → 532 kRM/yr; BESS-incremental per-MWh: 81 → 100 kRM/yr).
Strategic conclusions unchanged. See § “v3 update — partial-year fix” below.
v2 update (2026-05-07): Added cooling-CDH temperature sensitivity to the DC load (PUE rises 0.008/°C above 25°C). Net effect: +6.6 to +8.1 M NPV across scenarios; PV-only IRR moves 9.5% → 9.9%. Modest model-fidelity improvement; strategic conclusions unchanged. See § “v2 update — cooling CDH” below.
v1 update (2026-05-07): BESS dispatch upgraded from heuristic to LP-optimal (CBC, monthly LP, MD-aware). The structural conclusion holds: BESS at 5M RM/MWh is NPV-negative even with optimal dispatch. See § “v1 update — LP results” below for the head-to-head and the cleaner explanation.
Generated: 2026-05-08 (v3 partial-year corrected). Inputs: 5y full-year silver.weather_hourly (2020-2024 calendar years; partial year 2025 dropped), Tariff E3 (HV ETOU industrial proxy), TNB ICPT historical surcharge, placeholder capex curves.
Site assumptions
Section titled “Site assumptions”| Field | Value |
|---|---|
| Announced IT capacity | 100 MW |
| Utilization | 80% |
| PUE | 1.4 |
| Gross load (constant, 7×24) | 112 MW |
| Tariff | E3 (HV ETOU industrial proxy) |
| Energy peak rate | 0.337 RM/kWh + ICPT |
| Energy off-peak rate | 0.202 RM/kWh + ICPT |
| MD charge | 35.50 RM/kW/month (peak-window only) |
| Discount rate | 8% |
| Horizon | 20 years |
| PV capex | 3.5M RM/MW (~ USD 700/kW) |
| BESS capex | 5.0M RM/MWh (~ USD 1,000/kWh) |
| BESS RTE | 88% |
| BESS sizing | 4-h duration; power = PV/4 (i.e., 30 MW PV → 7.5 MW × 4 h = 30 MWh) |
Sensitivity sweep (mean savings over 2020-2024 weather, RM millions)
Section titled “Sensitivity sweep (mean savings over 2020-2024 weather, RM millions)”ICPT period-aligned (mix of historical -2 to +20 sen)
Section titled “ICPT period-aligned (mix of historical -2 to +20 sen)”| PV (MW) | BESS (MWh) | CapEx (RMm) | Annual save (RMm) | NPV (RMm) | IRR | Payback |
|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | n/a | — |
| 30 | 0 | 105 | 13.33 | +15.6 | 9.9% | 9 yr |
| 30 | 30 | 255 | 16.47 | −168 | −3.3% | >20 |
| 60 | 0 | 210 | 25.87 | +23.4 | 9.5% | 9 yr |
| 60 | 60 | 510 | 32.03 | −344 | −3.7% | >20 |
| 90 | 0 | 315 | 38.41 | +31.2 | 9.3% | 9 yr |
| 90 | 90 | 765 | 47.56 | −522 | −3.9% | >20 |
ICPT held at +16 sen/kWh (current 2024-H2 level)
Section titled “ICPT held at +16 sen/kWh (current 2024-H2 level)”| PV (MW) | BESS (MWh) | CapEx (RMm) | Annual save (RMm) | NPV (RMm) | IRR | Payback |
|---|---|---|---|---|---|---|
| 30 | 0 | 105 | 16.35 | +45.2 | 13.4% | 7 yr |
| 30 | 30 | 255 | 19.41 | −139 | −0.9% | >20 |
| 60 | 0 | 210 | 31.91 | +82.7 | 13.0% | 8 yr |
| 60 | 60 | 510 | 37.92 | −287 | −1.2% | >20 |
| 90 | 0 | 315 | 47.47 | +120.1 | 12.8% | 8 yr |
| 90 | 90 | 765 | 56.40 | −435 | −1.3% | >20 |
Five read-throughs
Section titled “Five read-throughs”1. PV alone clears the IRR hurdle once ICPT > ~10 sen/kWh
Section titled “1. PV alone clears the IRR hurdle once ICPT > ~10 sen/kWh”Without ICPT or with the historical 2020-2021 rebate baked in, PV NPV is modestly positive at 8% WACC (period-aligned IRR ~10%). Once ICPT settles at the post-2022 industrial level (16+ sen/kWh), PV alone returns 13.0% IRR with an 8-year payback (60 MW base case) — comfortably bankable for project-finance equity returns.
The IRR doesn’t change with PV size (linear scaling — no economies or diseconomies in this model). Real projects would see modest economies via fixed dev costs and diseconomies via interconnection complexity at >50 MW.
2. BESS is severely value-negative under the v0 dispatch heuristic — and the failure mode is structural
Section titled “2. BESS is severely value-negative under the v0 dispatch heuristic — and the failure mode is structural”Adding 30/60/90 MWh BESS only adds 0.5–1.6 RM-million of annual savings, nowhere near the 75–135 RM-million capex. The IRR drops from +6/9% (PV alone) to −6/−9%. This is not a BESS economics problem — it’s a dispatch problem.
The heuristic discharges BESS during the early ETOU peak hours (14–18 MYT) when PV is also active. By 18:00 MYT BESS is empty, but PV has dropped to zero. The 18:00–22:00 ETOU peak hours then run at full grid import (~112 MW gross), so MD ends up unchanged from the no-BESS case. BESS captures only the energy spread (~1.2× peak/off-peak with RTE 0.88 → ~6.9k RM per cycle × ~365 cycles ≈ 2.5M RM/yr), not the MD avoided cost (which would be worth ~6.4M RM/yr per 15 MW shaved at 35.50 RM/kW/mo).
Fix in v1: smarter dispatch — reserve a fraction of BESS energy for the last-peak hours (18–22 MYT). Could be rule-based (“never let SoC drop below 50% before 18:00”) or LP-optimised. Estimated lift: 4–5 RM-million per year per 15 MW BESS power.
3. The “no surplus to charge from PV” trap is real for under-sized PV
Section titled “3. The “no surplus to charge from PV” trap is real for under-sized PV”A 100 MW DC has 112 MW gross load. Even 90 MW PV at peak generation produces ~70 MW (after PR), still less than load. PV surplus = 0 always. Hence BESS charge has to come from off-peak grid (which the v0 dispatch now does). For PV ≥ load (i.e., > 130–150 MW for a 112 MW gross site, which doesn’t fit on typical DC rooftop/land), the surplus-to-BESS path opens up — but at that scale we hit grid export PPA constraints and curtailment instead.
Implication for design: sizing PV at 30–60% of DC gross load is the sweet spot. >70% just adds curtailment without changing economics.
4. The avoided-cost margin is large enough that base PV economics don’t need ENEGEM
Section titled “4. The avoided-cost margin is large enough that base PV economics don’t need ENEGEM”A 60 MW PV system at 16 sen/kWh ICPT saves 25.9M RM/yr against 210M RM capex — i.e., the BTM avoided-cost case alone delivers 9.5% IRR before any cross-border export, green-attribute resale, or VPP service. Adding ENEGEM as upside (even at conservative 0.5–1 RM/kWh assumed export net margin) only swings the curve up, not the sign.
This validates the cross-comparison report’s recommendation: lead the commercial case with BTM avoided cost; treat ENEGEM and VPP service revenue as upside, not the foundation.
5. ICPT trajectory is the single biggest external risk — and the post-2022 step is sticky
Section titled “5. ICPT trajectory is the single biggest external risk — and the post-2022 step is sticky”The +16 sen/kWh case is +35.8M NPV at 60 MW; the period-aligned case (which weights heavily by 2020–2021 rebate years) is −25.5M NPV. A 10 sen move in ICPT changes annual savings by ~9.8M RM at 60 MW PV, about a 20% NPV swing.
Government subsidy framework keeps domestic ICPT at zero, but industrial ICPT has been positive every period since 2022-H2. The risk to this case is policy reversion (e.g., subsidy expansion to cover industrial), not fuel prices. Recommend explicit policy-risk hedge in the commercial structure — e.g., a tariff-floor clause in the BTM PPA.
v3 update — partial-year fix (2026-05-08)
Section titled “v3 update — partial-year fix (2026-05-08)”What broke
Section titled “What broke”btm_economics.evaluate averaged savings_rm across the per-year annual
list. Because the 5-year weather window (UTC 2020-01-01 .. 2024-12-31) leaks
~7 hours into MYT 2025 (UTC+8 offset), the LP wrote 6 annual rows for every
scenario: 5 full years + a “year 2025” with 1 MWh PV and 470 RM savings.
Averaging over 6 rows instead of 5 dragged every per-year mean down by ~17%.
The bias was uniform across scenarios:
| Scenario @ ICPT 16 sen | All-rows mean save | Full-year mean save | NPV bias |
|---|---|---|---|
| 60 MW PV alone | 26.59 | 31.91 | −52.2 |
| 60 MW PV + 60 MWh BESS | 31.60 | 37.92 | −52.1 |
| 90 MW PV alone | 39.56 | 47.47 | −78.0 |
The bug existed since stage 4 v0; the v1 (LP) and v2 (cooling-CDH) refactors
preserved it. Caught when the PPA pricing auto-derive infrastructure
(derive_ppa_inputs_from_btm) used full-year-only denominators and surfaced
a 17% delta vs the report’s stated tenant ceiling.
The fix
Section titled “The fix”Filter years to those with ≥4000 hours of weather data before LP simulation.
Year 2025 (7 hours) is dropped; years 2020–2024 retained.
year_hours = {y: weather.filter(pl.col("year_myt") == y).height for y in years}years = [y for y in years if year_hours[y] >= MIN_HOURS_PER_FULL_YEAR]Side-by-side: v2 (biased) vs v3 (corrected)
Section titled “Side-by-side: v2 (biased) vs v3 (corrected)”| Scenario @ ICPT 16 sen | v2 NPV | v3 NPV | Δ NPV | v2 IRR | v3 IRR |
|---|---|---|---|---|---|
| 30 MW PV alone | +18.4 | +45.2 | +26.8 | 10.3% | 13.4% |
| 60 MW PV alone | +30.5 | +82.7 | +52.2 | 9.9% | 13.0% |
| 90 MW PV alone | +42.4 | +120.1 | +77.7 | 9.7% | 12.8% |
| 60 MW PV + 60 MWh BESS | −349 | −287 | +62 | n/a | n/a |
| 90 MW PV + 90 MWh BESS | −527 | −435 | +92 | n/a | n/a |
Strategic implications
Section titled “Strategic implications”Quantitative magnitudes are larger; qualitative conclusions unchanged. PV alone is now comfortably above any reasonable hurdle rate (13% IRR vs typical 10% project-finance bar). BESS still NPV-negative by ~290 M at the 60+60 MWh size; trigger curve for BESS has shifted earlier by 1 year for the “Strong VPP stack” and “24/7 CFE premium” scenarios.
The v3 commercial framing strengthens: PV-anchor PPA + carbon-attribute bundle is comfortably investable; BESS remains conditional on VPP service contract.
Capital-stack implications (added 2026-05-08): The PPA-tariff IRR sweep
in ppa_pricing.json shows the developer-floor crosses the tenant ceiling
between 12% and 15% target IRR. At 15% IRR the floor (454 RM/MWh) exceeds
the LP-derived tenant avoided cost (406 RM/MWh) — vanilla kWh-only PPAs
become structurally infeasible. Bundled-carbon PPAs (ceiling 696 RM/MWh
under hyperscaler internal carbon at USD 100/tCO2) accommodate up to 18%
IRR comfortably. At 15% IRR the developer floor exceeds the LP-derived tenant ceiling — vanilla kWh-only PPAs become structurally infeasible; equity-heavy structures must bundle carbon to close. Debt-heavy structures at 8% effective discount have full headroom on either path.
Known modeling simplifications (2026-05-08 audit)
Section titled “Known modeling simplifications (2026-05-08 audit)”The dispatch_lp module was audited end-to-end and is free of boundary or
partial-year bugs (UTC↔MYT month-edge handling correct; per-year LP runs
isolated; SoC reset between years is a deliberate simplification with
negligible NPV impact). Two modeling choices remain noteworthy:
-
PV degradation extrapolation past year 5. The LP simulation runs over the 5-year weather window (2020-2024) with intra-window degradation applied. For the 20-year NPV horizon, years 6-20 use the 5-year-mean savings flat, rather than continuing to degrade at 0.5%/yr. Replacing with proper continuing-degradation reduces PV-only NPV by ~7.7 M RM (-9%, from +82.7 to ~+75.0 M). Conclusion: PV-alone is still bankable under the more conservative model (still > 8% project-finance hurdle), but the bargaining-range cliff at 13-15% IRR shifts down by ~1-2pp. Carbon-stacked structures unaffected (their +211 M margin absorbs the shift trivially).
-
Year-end BESS SoC discarded between years. The LP solves each year independently, starting BESS at SoC=0. End-of-year SoC is not carried forward. Order of magnitude: at 60 MWh BESS × 4 boundary years × 0.5 typical end-of-year SoC × peak rate = ~16 kRM/MWh missed value across the 20-yr horizon. Negligible (<0.01% of NPV).
These are documented for transparency rather than fixed because (a) neither changes the strategic conclusions and (b) the cascade-update work across reports outweighs the analytical gain.
Defense-in-depth follow-ups
Section titled “Defense-in-depth follow-ups”To prevent re-occurrence, post-v3 the codebase has:
- Runtime auto-derive in CLI commands: aggregator, trigger, ppa-pricing,
risk all read
btm_economics_dc100.jsonat runtime instead of using hardcoded constants. - Static module-constant guards: pytest asserts the per-MW PV and
per-MWh BESS-incremental fallback constants in
aggregator.pyandtrigger_curve.pystay within 5% of LP-derived values. - Drift checker (
jb-vpp model check-summary --strict): 15 narrative citations (in EXECUTIVE_SUMMARY.md and other reports) verified against live JSON every CI run.
v2 update — cooling CDH
Section titled “v2 update — cooling CDH”DC load model upgraded from flat 7×24 to temperature-dependent:
with /°C, °C, base PUE = 1.4. At JB’s 5y mean ambient (26.94 °C), effective PUE is 1.415 (+1.1%); at 32 °C hot-month peak, PUE = 1.456 (+4%).
Side-by-side: flat-load vs cooling-CDH (LP dispatch, 5y avg)
Section titled “Side-by-side: flat-load vs cooling-CDH (LP dispatch, 5y avg)”| Scenario @ ICPT 16 sen | Flat save | Cooling save | Δ save | Flat NPV | Cooling NPV | Δ NPV |
|---|---|---|---|---|---|---|
| 30 MW PV alone | 12.96 | 13.62 | +0.66 | +11.9 | +18.4 | +6.5 |
| 30 MW PV + 30 MWh BESS | 15.39 | 16.18 | +0.79 | −178 | −171 | +7.8 |
| 60 MW PV alone | 25.92 | 26.59 | +0.67 | +23.9 | +30.5 | +6.6 |
| 60 MW PV + 60 MWh BESS | 30.77 | 31.60 | +0.83 | −357 | −349 | +8.1 |
| 90 MW PV alone | 38.88 | 39.56 | +0.67 | +35.8 | +42.4 | +6.6 |
| 90 MW PV + 90 MWh BESS | 46.16 | 47.00 | +0.84 | −535 | −527 | +8.2 |
(All RM millions/yr unless noted. NPV improvements concentrate on high-CDH peak hours where PV and BESS displace expensive grid kWh.)
What the cooling lift actually adds
Section titled “What the cooling lift actually adds”- Per-MW PV: +11 kRM/yr (+2.6% over flat baseline). Cooling load is biased toward afternoon hours, which overlap the ETOU peak window (14–22 MYT) — those incremental kWh are displaced at peak rates.
- Per-MWh BESS: +14 kRM/yr (+17% over flat baseline incremental). Higher peak demand → more MD to shave → BESS captures more.
- PV-only IRR: 9.5% → 9.9%. Crosses the 10% hurdle that some corporate sponsors apply.
Strategic implications
Section titled “Strategic implications”The cooling refinement is model fidelity, not a strategic shift. The dominant conclusions (PV bankable solo, BESS gap requires capex fall + service contract) are unchanged. The main commercial use: show ~+10 M NPV uplift in the v2 deck vs v1 to demonstrate model maturation and quantify how the project benefits from operating in hot markets. Useful framing for hyperscaler-anchor commercial discussions because it’s exactly the math the tenant will run for their own BTM case.
v1 update — LP results
Section titled “v1 update — LP results”The v0 heuristic discharged BESS in early peak hours and ran out by 18–22 MYT, delivering near-zero MD value. v1 replaces the heuristic with an LP-optimal monthly dispatch (pulp + CBC) that knows about the MD term in the objective. Result: same scenarios, better dispatch.
| Scenario @ ICPT 16 sen | Heuristic save | LP save | Lift | LP NPV | LP IRR |
|---|---|---|---|---|---|
| 30 MW PV alone | 12.96 | 12.96 | — | +11.9 | 9.5% |
| 30 MW PV + 30 MWh BESS | 13.48 | 15.39 | +14% | −178 | −4.3% |
| 60 MW PV alone | 25.92 | 25.92 | — | +23.9 | 9.5% |
| 60 MW PV + 60 MWh BESS | 26.95 | 30.77 | +14% | −357 | −4.3% |
| 90 MW PV alone | 38.88 | 38.88 | — | +35.8 | 9.5% |
| 90 MW PV + 90 MWh BESS | 40.43 | 46.16 | +14% | −535 | −4.3% |
(All savings in RM millions/yr; NPV in RM millions; LP results from CBC solver with MD constraint enforced over the ETOU peak window.)
What the LP actually does that the heuristic didn’t
Section titled “What the LP actually does that the heuristic didn’t”- Spreads discharge across the entire 8-hour peak window instead of front-loading. With a 30 MWh / 7.5 MW BESS (60 MW PV scenario sized to PV/4 power), heuristic discharges 4 hours at 7.5 MW then sits empty. LP discharges 3.5 MW continuously across all 8 peak hours, shaving 3.5 MW of MD instead of 0 MW.
- Charges from off-peak grid when PV surplus is unavailable. v0 had already added this rule, but LP optimises the timing — it charges in the cheapest off-peak hours, not the first available ones.
- Curtails PV when needed (rare at this PV/load ratio, but happens).
Why BESS still loses money at any size
Section titled “Why BESS still loses money at any size”The annual incremental savings BESS delivers per MWh capacity is ~80 kRM/MWh/yr under LP dispatch (60 MW PV scenario: 4.85 M RM extra ÷ 60 MWh = 81 kRM/MWh/yr). Decomposing:
- Energy arbitrage: ~28 MWh delivered per cycle × 250 weekday cycles × spread (peak − off-peak − ICPT cancels out at first order) × RTE = ~830 kRM/MWh/yr per cycled MWh. Already capped — can’t cycle more than once a day.
- MD reduction: 3.5 MW shaved × 35.50 RM/kW/mo × 12 = 5.0 M RM/yr per 3.5 MW of constant-rate MD shave = ~167 kRM/MWh/yr if BESS is sized exactly 4-h relative to MD rate.
Total LP capture: ~80 kRM/MWh/yr (lower than my back-of-envelope because weekend hours don’t contribute — only ~250 cycle-weekdays/yr useful).
Capex 5,000 kRM/MWh ÷ 80 kRM/MWh/yr = 62-year simple payback. With 8% WACC and 20-year horizon, NPV breakeven needs ~510 kRM/MWh/yr in savings, or 6.4× the current capture. Three paths to closing the gap:
- BESS capex 5M → 1.6M RM/MWh (USD 320/kWh, possible by 2028–2030 per BNEF curves)
- VPP service revenue stack on top — frequency response (DRC), capacity payments under future markets, ENEGEM dispatch on the MY → SG leg. Conservatively need ~430 kRM/MWh/yr from non-BTM sources to close the gap at current capex. That’s a steep ask but not impossible if multiple small revenue streams stack.
- Increase peak/off-peak spread — would require ICPT > ~40 sen/kWh sustained, which is the high-fuel-cost stress scenario, not the base case.
Verdict
Section titled “Verdict”LP dispatch is a 14% lift but does not change the structural bottom line: at 5 M RM/MWh installed cost and 100 MW DC scale, BESS is value-destroying on a pure BTM avoided-cost basis. The path to BESS-positive is one of:
- Wait for capex to fall (timing-dependent, beyond our control)
- Stack additional revenue streams via VPP service contracts
- Find a customer with a much larger peak/off-peak spread or a tariff with capacity payments
For the JB anchor commercial case today, the recommendation stays: fund PV alone, structure BESS as a future option contract that triggers when capex falls or a VPP service contract anchors it.
What v1 needs to fix
Section titled “What v1 needs to fix”| Deficiency | Impact | Fix |
|---|---|---|
| BESS dispatch under-utilises MD reduction | −5 to −6 M RM/yr per scenario | LP-optimised or “save SoC for late peak” heuristic |
| Flat DC load ignores cooling auxiliary CDH variation | ~3% diurnal underestimate | Multiply by (1 + 0.012 × CDH_t) from POWER T |
| Tariff E3 rate values are placeholder | ±20% absolute number error | Verify against current TNB PDF |
| ICPT 2025-H1 onwards is placeholder | Scenario-only matters for forward NPV | Update yaml each 6-month cycle |
| PV degradation flat 0.5%/yr | 1–2% NPV underestimate | Match warranty curve from EPC quote |
| No grid-export revenue line | Misses occasional ENEGEM upside | Add bess_can_export flag and a USEP price feed (blocked) |
| No real DC anchor data — using stub | Can’t compare vs actual sites | Pull from dc_tracker/jb_data_centers.yaml once populated |
Bottom-line VPP design takeaway (v3)
Section titled “Bottom-line VPP design takeaway (v3)”For a 100 MW JB data-center anchor under current ICPT levels (post-v3 numbers):
-
A 60 MW BTM PV system is the core bankable position. ~210M RM capex, 31.9M RM/yr savings, 8-year payback, 13.0% IRR, +83 M RM NPV at 8% WACC. Independent of ENEGEM / VPP service / green-attribute monetisation.
-
BESS as configured today is value-destroying. Don’t add BESS unless either (a) the dispatch is LP-optimised to capture MD reduction, or (b) BESS capex falls 30–40% (USD 600/kWh range), or (c) a VPP service revenue stream (e.g., ENEGEM dispatch) is contracted.
-
PV size sweet spot is 50–70% of gross load. Above this, curtailment eats marginal returns; below this, the absolute savings are too small to justify development cost.
-
The commercial structure should isolate the BTM PV case from the BESS / ENEGEM / VPP case — bank the PV, fund the rest as upside-tracking options. This matches the actual economics produced by the model.