CFDL

Solar PPA microgrid

Outputs below are real: the notebook runs against the energy pack'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'>

Chart produced by the preceding cell

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