Automating state RPS compliance reports

A compliance analyst closes an annual Renewable Portfolio Standard (RPS) report by hand, multiplies retail load by the statutory target in a spreadsheet, and reads a solar carve-out as satisfied — but the carve-out was met with generic Class I certificates that the commission will not accept for the solar tier, and the deficit only surfaces after the filing window closes. Automating the report removes that class of error by computing each tier’s obligation and its qualified retirements from the same settled ledger, in exact units, every time. This is a concrete step within State PUC Reporting Workflows, which frames how the reconciled output here feeds a commission filing; the anchor load it consumes is the audited product of the Settlement Calculation & Validation Engines.

The flow below shows the specific transformation this page implements: a settled-load snapshot and a certificate retirement export are reduced per tier and reconciled into a per-state compliance report.

RPS obligation-versus-retirement reconciliation Settled retail load feeds an obligation computation that multiplies load by the tiered statutory target. A certificate retirement export is filtered to qualified certificates per tier. The two per-tier quantities are reconciled into a surplus or deficit, then rendered as a per-state compliance report. Settled loadretail MWh, per year REC retirementsregistry export Obligationload × tier target Qualified RECsfilter by tier Reconcilesurplus / deficit Compliance reportper state, sealed

Prerequisites

  • Python packages: the standard library is sufficient for the core reconciliation — decimal for exact MWh arithmetic, hashlib and json for the sealed artifact, dataclasses and datetime. pandas is optional and used only to shape the settled-load snapshot upstream.
  • Data dependencies: a settled-load snapshot giving reconciled retail MWh for the compliance year at the state and tier grain (the output of the settlement engine, not a raw billing sum); a certificate retirement export from the applicable tracking registry (PJM-GATS, NEPOOL-GIS, WREGIS, or M-RETS) carrying serial range, vintage, fuel type, tier eligibility, and MWh per record; and a per-state obligation configuration giving each tier’s statutory target for the year and the set of eligible fuels.
  • Permissions / access: read-only credentials for the tracking registry’s retirement report, and read access to the settled ledger snapshot store. No write scope to the registry is required — this job computes and reconciles; certificate retirement itself is a separate, deliberately gated action.

Implementation

The obligation for a tier is the settled retail load multiplied by that tier’s statutory target percentage. For a state \(s\), compliance year \(y\), and tier \(t\):

$$Obligation_{s,y,t} = Load_{s,y} \times target_{s,y,t}$$

and the tier is compliant when qualified retired certificates meet or exceed it: \(RECs_{s,y,t} \ge Obligation_{s,y,t}\). Because a REC is one MWh of qualified generation and certificates are indivisible whole units, the obligation is rounded up to the next whole certificate — a fractional shortfall still requires one more certificate to cure. Every quantity below is a Decimal; no MWh figure ever touches binary float, so a report summed across many records reconciles to the unit.

from dataclasses import dataclass
from decimal import Decimal, ROUND_CEILING, ROUND_HALF_UP
from datetime import datetime, timezone
from typing import Iterable
import hashlib
import json


@dataclass(frozen=True)
class TierConfig:
    tier: str                 # e.g. "Class I" or "Solar carve-out"
    target: Decimal           # statutory fraction of retail load, this year
    eligible_fuels: frozenset # fuels that qualify for this tier


@dataclass(frozen=True)
class TierResult:
    tier: str
    obligation_mwh: Decimal
    retired_mwh: Decimal
    position_mwh: Decimal     # retired - obligation; negative == deficit
    compliant: bool


def compute_obligation(settled_load_mwh: Decimal, target: Decimal) -> Decimal:
    """Obligation = settled load x tier target, rounded UP to a whole certificate.

    RECs are indivisible one-MWh units, so any fractional obligation requires the
    next whole certificate to cure -- ROUND_CEILING, never truncation.
    """
    raw = settled_load_mwh * target
    return raw.quantize(Decimal("1"), rounding=ROUND_CEILING)


def sum_qualified_retirements(retirements: Iterable[dict], state_code: str,
                              cfg: TierConfig) -> Decimal:
    """Sum retired MWh that qualify for this state and tier, rejecting duplicates.

    A certificate counts only if it was retired against THIS state's obligation and
    carries an eligible fuel for the tier. Serial ranges are deduplicated so a
    double-listed retirement cannot silently mask a deficit.
    """
    total = Decimal("0")
    seen = set()
    for rec in retirements:
        if rec["retired_against_state"] != state_code:
            continue
        if rec["tier"] != cfg.tier or rec["fuel_type"] not in cfg.eligible_fuels:
            continue
        serial = rec["serial_range"]
        if serial in seen:
            raise ValueError(f"duplicate serial retirement in tier {cfg.tier}: {serial}")
        seen.add(serial)
        total += Decimal(str(rec["mwh"]))
    return total.quantize(Decimal("0.001"), rounding=ROUND_HALF_UP)


def reconcile_tier(settled_load_mwh: Decimal, state_code: str,
                   cfg: TierConfig, retirements: Iterable[dict]) -> TierResult:
    """Reconcile one tier's obligation against its qualified retirements."""
    obligation = compute_obligation(settled_load_mwh, cfg.target)
    retired = sum_qualified_retirements(retirements, state_code, cfg)
    position = (retired - obligation).quantize(Decimal("0.001"))
    return TierResult(
        tier=cfg.tier,
        obligation_mwh=obligation,
        retired_mwh=retired,
        position_mwh=position,
        compliant=position >= Decimal("0"),
    )


def build_state_rps_report(state_code: str, compliance_year: int,
                           settled_load_mwh: Decimal,
                           tier_configs: list[TierConfig],
                           retirements: list[dict], run_id: str) -> dict:
    """Produce a sealed, per-state RPS compliance report across every tier."""
    tier_results = [
        reconcile_tier(settled_load_mwh, state_code, cfg, retirements)
        for cfg in tier_configs
    ]
    payload = {
        "state_code": state_code,
        "compliance_year": compliance_year,
        "settled_load_mwh": str(settled_load_mwh),
        "run_id": run_id,
        "tiers": [
            {
                "tier": r.tier,
                "obligation_mwh": str(r.obligation_mwh),
                "retired_mwh": str(r.retired_mwh),
                "position_mwh": str(r.position_mwh),
                "compliant": r.compliant,
            }
            for r in tier_results
        ],
        "overall_compliant": all(r.compliant for r in tier_results),
    }
    # Seal the artifact so the transmitted numbers can be replayed and proven.
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
    payload["submission_hash"] = hashlib.sha256(canonical.encode()).hexdigest()
    payload["sealed_at"] = datetime.now(timezone.utc).isoformat()
    return payload

Each tier is reconciled independently, which is what catches the opening failure: a solar carve-out is computed against solar-eligible retirements only, so generic Class I certificates cannot paper over a solar deficit. The obligation rounds up to a whole certificate; the retirement sum stays at three-decimal MWh precision to match registry reporting, and the position is their exact difference. Because every accumulation runs through Decimal, a state that files thousands of retirement records reconciles to the same total on every run.

Verification

Confirm the report before it is sealed and transmitted:

  1. Structure and coverage. build_state_rps_report returns a payload whose tiers list has one entry per configured tier, each carrying obligation_mwh, retired_mwh, position_mwh, and a boolean compliant. A missing tier means a config gap, not compliance.
  2. Position identity. For every tier, position_mwh must equal retired_mwh - obligation_mwh to the quantized MWh, and overall_compliant must be the logical AND of the per-tier flags. This is the core check — a tier reading compliant while its position is negative signals a code fault.
  3. Load reconciliation. The settled_load_mwh in the report must reconcile back to the settlement ledger total for the state and year, proving the obligation was anchored to settled volume rather than a stray billing figure.
from decimal import Decimal

def verify_report(report: dict) -> None:
    """Assert per-tier position identity and overall compliance consistency."""
    computed_overall = True
    for tier in report["tiers"]:
        obligation = Decimal(tier["obligation_mwh"])
        retired = Decimal(tier["retired_mwh"])
        position = Decimal(tier["position_mwh"])
        assert (retired - obligation) == position, (
            f"position mismatch in {tier['tier']}: {retired - obligation} != {position}"
        )
        assert tier["compliant"] == (position >= Decimal("0")), (
            f"compliance flag inconsistent with position in {tier['tier']}"
        )
        computed_overall = computed_overall and tier["compliant"]
    assert report["overall_compliant"] == computed_overall, "overall flag inconsistent"

For a tamper-evident trail, the submission_hash recomputed over the canonical payload (excluding the hash and timestamp fields) must match the stored digest, so a replay of a historical filing reproduces both the numbers and their provenance. A deficit on any tier routes to the shortfall path — an Alternative Compliance Payment or a cure via additional retirements — rather than being silently rounded away.

Compliance Note

This report must be validated against the specific commission’s RPS rule, because the obligation base, the tier definitions, the eligible-fuel list, and the rounding convention are all set by state statute and commission order — for example a California CPUC RPS compliance report against WREGIS retirements, a Massachusetts DPU annual filing against NEPOOL-GIS, or a New Jersey BPU energy-year filing against PJM-GATS with a distinct solar (SREC) carve-out. The target percentages and eligible fuels encoded in TierConfig are versioned reference data pinned to the compliance year, never constants, so a re-run for an audit reproduces the historically correct obligation rather than today’s target applied retroactively. Retirement is the evidentiary act the commission accepts, so the report reconciles against the registry’s retirement export — filtered to certificates retired against this state — and every serial range is deduplicated to prevent a certificate counting toward two obligations. Retain the settled-load snapshot, the tier configuration version, the retirement export, and the sealed report together, so the filing satisfies the same traceability expectation that governs the broader settlement stack: every reported megawatt-hour ties to a settled interval and a published certificate record.

Frequently Asked Questions

Why round the obligation up to a whole certificate instead of to the nearest MWh?

A REC is an indivisible one-MWh instrument, so a fractional obligation cannot be met with a fractional certificate — any remainder requires the next whole certificate to cure. Rounding the obligation up with ROUND_CEILING reflects that: an obligation of 40,000.2 MWh needs 40,001 certificates, and rounding to nearest would understate the requirement by one certificate and produce a filing that reads compliant while it is one short.

How does per-tier reconciliation prevent a carve-out being met with the wrong certificates?

Each tier is reconciled against only the retirements whose fuel type is in that tier’s eligible set. A solar carve-out sums solar-eligible retirements exclusively, so generic Class I certificates are never counted toward it. Computing the tiers independently means a surplus in one tier cannot mask a deficit in another, which is the most common way a hand-built report passes internally but fails a commission review.

Why keep every MWh figure in Decimal rather than float?

Compliance quantities are reconciled to the certificate against a registry that also reports in exact units, and a state filing can aggregate thousands of retirement records. Binary floating point cannot represent most decimal fractions exactly, so summing that many records accumulates drift that can flip a rounding boundary and turn a compliant tier into an apparent deficit. Reducing every quantity with Decimal keeps the reconciliation exact and reproducible across runs.