Solar PPA microgrid
Outputs below are real: the notebook runs against the
energypack's benchmark model, which CFDL validates against an independent reference. To run it yourself, see the Python SDK guide.
A solar-plus-storage microgrid with a PPA revenue contract, degradation, O&M escalation, ITC/PTC tax attributes and MACRS depreciation, financed with sculpted debt.
This notebook uses the benchmark model that CFDL validates against an independent reference to the penny (see benchmarks/).
from pathlib import Path
import cfdl_sdk
# This notebook reads a benchmark model and the pack definitions, both of which
# live in the repository, so locate its root. Searching a bounded set of
# ancestors means running from outside a checkout fails with an explanation
# rather than looping forever at the filesystem root.
def repo_root() -> Path:
here = Path.cwd().resolve()
for candidate in (here, *here.parents):
if (candidate / "Cargo.toml").exists() and (candidate / "packs").is_dir():
return candidate
raise RuntimeError(
"No CFDL checkout found above "
f"{here}. This notebook loads a model from benchmarks/ and pack "
"definitions from packs/, so it needs to run inside a clone of "
"https://github.com/bizarc/cfdl."
)
ROOT = repo_root()
PACKS = ROOT / "packs"Compile
Compile the model directory to IR.
model_dir = ROOT / "benchmarks/energy/solar_ppa_microgrid"
model = cfdl_sdk.compile(model_dir, packs_dir=PACKS)
print("streams:", len(model.ir["streams"]))streams: 7
Run
Run with the benchmark's configuration and apply the energy pack's domain metrics.
results = model.run(
config=str(model_dir / "run.json"),
pack="energy",
)
print("status:", results.status, "| warnings:", len(results.warnings))status: ok | warnings: 0
Cash flows
The engine returns per-period signed cash flows; cashflows() gives a wide DataFrame indexed by period.
cf = results.cashflows()
print('shape:', cf.shape)
cf.head()shape: (300, 8)
model.net_cash_flow stream.energy.capacity.revenue \
period
2026-01 -2.381296e+06 5000.0
2026-02 1.870377e+04 5000.0
2026-03 1.870377e+04 5000.0
2026-04 1.870377e+04 5000.0
2026-05 1.870377e+04 5000.0
stream.energy.capex.outlay stream.energy.debt.service \
period
2026-01 -2400000.0 -11462.896936
2026-02 0.0 -11462.896936
2026-03 0.0 -11462.896936
2026-04 0.0 -11462.896936
2026-05 0.0 -11462.896936
stream.energy.itc.credit stream.energy.om.expense \
period
2026-01 0.0 -5833.333333
2026-02 0.0 -5833.333333
2026-03 0.0 -5833.333333
2026-04 0.0 -5833.333333
2026-05 0.0 -5833.333333
stream.energy.ppa.revenue stream.energy.storage.margin
period
2026-01 29750.0 1250.0
2026-02 29750.0 1250.0
2026-03 29750.0 1250.0
2026-04 29750.0 1250.0
2026-05 29750.0 1250.0
# Requires the [viz] extra (pip install cfdl-sdk[viz]).
results.plot.cumulative()<Axes: xlabel='period', ylabel='cumulative amount'>

Metrics
Core metrics (NPV/IRR/MOIC/...) plus the pack's domain metrics, with their source labelled.
results.metrics_frame() metric value currency source
0 domain.energy.debt_service 2.751095e+06 USD domain:energy
1 domain.energy.dscr 3.708691e+00 None domain:energy
2 domain.energy.ebitda 1.020296e+07 USD domain:energy
3 domain.energy.opex 2.391043e+06 USD domain:energy
4 domain.energy.revenue 1.259400e+07 USD domain:energy
5 domain.energy.tax_benefits 7.200000e+05 USD domain:energy
6 entity.project.microgrid.total 5.771866e+06 USD core
7 model.irr 1.485950e-01 None core
8 model.moic 3.423834e+00 None core
9 model.npv 1.239647e+06 USD core
10 model.payback_periods 8.500000e+01 None core
11 model.payback_years 7.083333e+00 None core
12 model.total 5.771866e+06 USD core
13 model.wal_years 1.294588e+01 None core
14 run.annual_discount_rate 8.000000e-02 None core
15 run.periods_per_year 1.200000e+01 None core
16 stream.energy.capacity.revenue.total 1.500000e+06 USD core
17 stream.energy.capex.outlay.total -2.400000e+06 USD core
18 stream.energy.debt.service.total -2.751095e+06 USD core
19 stream.energy.itc.credit.total 7.200000e+05 USD core
20 stream.energy.om.expense.total -2.391043e+06 USD core
21 stream.energy.ppa.revenue.total 1.071900e+07 USD core
22 stream.energy.storage.margin.total 3.750000e+05 USD core
What-if
Re-run at a higher discount rate and compare NPV.
base = results.metrics()["model.npv"]
stressed = model.run(config={"deterministic": {"annual_discount_rate": 0.10}}, pack="energy")
print(f"NPV @ base: {base:,.0f}")
print(f"NPV @ 10%: {stressed.metrics()['model.npv']:,.0f}")NPV @ base: 1,239,647
NPV @ 10%: 757,036