pvperfsolar performance modeling

Python example

This script runs a small house and a 5 MWp tracker field through pvperf's REST API.

The script needs Python 3.9 or later and the requests package. Install it with pip install requests. Set the environment variable PVPERF_API_KEY to your key from the workbench's Account page, then run python pvperf_first_run.py.

"""A first run of a small house and of a tracker field through pvperf's REST API.

Needs Python 3.9 or later and the requests package (pip install requests).
Set PVPERF_API_KEY to your key from the workbench's Account page, then run:

    python pvperf_first_run.py
"""

import os

import requests

BASE_URL = os.environ.get("PVPERF_BASE_URL", "https://pvperf.com")
HEADERS = {"Authorization": "Bearer " + os.environ["PVPERF_API_KEY"]}


def post(path, body):
    """POST a JSON body to pvperf and return the answer. A refused request stops the script
    with pvperf's reason. A refused run is not charged."""
    r = requests.post(BASE_URL + path, json=body, headers=HEADERS, timeout=120)
    answer = r.json()
    if r.status_code >= 400:
        raise SystemExit(f"{r.status_code} {answer['code']}: {answer['detail']}")
    return answer


def show(name, answer):
    s = answer["summary"]
    print(f"{name}: {s['p_nom_dc_kwp']:,.1f} kWp, {s['e_grid_kwh']:,.0f} kWh to the grid a year")
    print(f"  specific yield {s['specific_yield_kwh_per_kwp']:,.0f} kWh/kWp")
    print(f"  performance ratio {s['performance_ratio']:.1%}")
    losses = [r for r in answer["loss_diagram"] if r["kind"] == "loss" and r["unit"] == "kWh/kWp"]
    for r in sorted(losses, key=lambda r: r["value"])[:3]:
        print(f"  {r['label']}: {r['value']:,.1f} kWh/kWp")
    billing = answer["billing"]
    print(f"  charged {billing['units']} unit(s), USD {billing['price_usd']:.3f}")


# 1. A house: 16 modules of 400 W flush on a south-facing roof at 25° in Springfield, Illinois.
#    The answer also carries the hourly power to the grid, p_grid_w, in W.
house = post(
    "/v1/run/simple",
    {
        "latitude": 39.80,
        "longitude": -89.65,
        "racking": "rooftop",
        "modules": 16,
        "module_power_w": 400,
        "tilt_deg": 25,
        "azimuth_deg": 180,
        "include_series": ["p_grid_w"],
    },
)
show("House", house)
for month in house["summary"]["monthly"]:
    print(f"  month {month['month']:2d}: {month['e_grid_kwh']:7,.0f} kWh")
hourly_w = house["series"]["p_grid_w"]
print(f"  {len(hourly_w)} hourly values, highest {max(hourly_w) / 1000:.2f} kW")

# 2. A tracker field: 5 MWp of single-axis trackers near Phoenix, Arizona.
field = post(
    "/v1/run/simple",
    {"latitude": 33.45, "longitude": -112.07, "racking": "single_axis", "system_capacity_kw": 5000},
)
show("Tracker field, ground cover ratio 0.40", field)

# 3. The same field with the rows further apart: change the full request the simple run
#    returned and send it to POST /v1/run.
request = field["request"]
request["array"]["gcr"] = 0.33
wider = post("/v1/run", request)
show("Tracker field, ground cover ratio 0.33", wider)

What it prints

The script makes three runs. Each answer's billing block gives the units and the price of its run.

The requests

A refused request answers with a problem document. Its code and detail say why, and field names the field to fix. A run that fails is not charged.

A run predicted to take more than 40 s answers 202 with status running. GET /v1/runs/{run_id} returns it when it is done. Each run of this script answers in under 15 s.

The TypeScript example makes the same runs.