vpp-ai-platform/skills-py/vpp_skills/numeric.py
Thomas Bayes 8796faca63 M2: skill contracts, Python skill service, L2 eval harness with baseline
- packages/domain: ForecastRequest, BidOptimizationRequest/Result,
  ReportRequest, SkillReport (+ golden and invalid fixtures, exported to
  contracts/ and regenerated as pydantic models).
- skills-py/vpp_skills: FastAPI service with versioned registry; load/PV/
  price forecasts (same-day-type EWM point forecast, conformal residual
  quantiles — coverage test as acceptance gate); bid-optimization MILP on
  HiGHS (binary block participation, hard ledger energy bounds, exact
  Decimal fit of the rounded curve inside the bounds, revenue distribution
  over quantile paths); report generator whose every figure is a
  {tool_call_id, path} reference, with a verifier. 48 tests incl. hypothesis
  property test that bids respect ledger constraints.
- packages/services: LedgerService.dayAheadBounds (the P7 cascade band
  handed to the optimizer); Decimal resolved once for CJS/ESM interop.
- packages/evals: L2 metrics (MAPE, nRMSE, coverage, direction accuracy,
  naive/hindsight revenue baselines), HTTP skill client, rolling-origin
  harness that pushes each bid through the real ledger, CLI with
  --check/--write-baseline; committed baseline on the SYNTHETIC dataset
  (no historical Hubei data yet — baselines measure the harness, not KPI).
- CI: evals job boots the skill service and fails on baseline digest drift.
- docs/open-questions: A6 (flexibility marginal cost = offer floor); A4/B6
  wired as placeholders. README/CLAUDE.md status → M2 done, M3 next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoYoGYzHkFyv3ALenkRPhA
2026-09-02 06:29:08 -04:00

56 lines
1.7 KiB
Python

"""Decimal-string discipline (docs/11 §3.3) at the skill boundary.
Contracts carry money/energy/prices as fixed-point decimal *strings*. Inside a
skill we compute with numpy floats, then quantize back to strings with an
explicit scale — and any headline figure (revenue, energy) is re-derived with
``decimal.Decimal`` from the already-quantized curves, so what a report cites
is exactly reproducible from the curves it cites.
"""
from __future__ import annotations
from decimal import ROUND_HALF_EVEN, Decimal
from typing import Iterable
import numpy as np
INTERVALS = 96
INTERVAL_HOURS = Decimal("0.25")
SCALE_MW = 3
SCALE_MWH = 3
SCALE_PRICE = 2
SCALE_MONEY = 2
def to_array(values: Iterable[str]) -> np.ndarray:
return np.array([float(Decimal(v)) for v in values], dtype=float)
def quantize(x: float | Decimal, scale: int) -> str:
q = Decimal(1).scaleb(-scale)
d = x if isinstance(x, Decimal) else Decimal(repr(float(x)))
out = d.quantize(q, rounding=ROUND_HALF_EVEN)
if out == 0:
out = abs(out) # normalise "-0.000"
return format(out, "f")
def curve(values: np.ndarray | list[str], date: str, scale: int) -> dict:
if isinstance(values, np.ndarray):
vals = [quantize(float(v), scale) for v in values.tolist()]
else:
vals = list(values)
if len(vals) != INTERVALS:
raise ValueError(f"curve must have {INTERVALS} values, got {len(vals)}")
return {"interval_minutes": 15, "date": date, "values": vals}
def dsum(values: Iterable[str]) -> Decimal:
return sum((Decimal(v) for v in values), Decimal(0))
def dot(a: Iterable[str], b: Iterable[str]) -> Decimal:
"""Exact Σ a_i·b_i over decimal strings."""
return sum((Decimal(x) * Decimal(y) for x, y in zip(a, b, strict=True)), Decimal(0))