Implements the package from PLAN.md: config/data loading, the LP-first MILP-fallback rolling-horizon optimiser, battery degradation tracking, independent schedule validation, metrics, plots and the CLI, plus the matching test suite. Adds a Gitea Actions workflow (lint + tests) that posts a pass/fail notification to ntfy on every run, and a .gitignore for build/cache artefacts that had been tracked by mistake. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Shared fixtures: a spec matching Attachment 1 and helpers for tiny markets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pytest
|
||||
|
||||
from battery_dispatch.config import BatterySpec, RunConfig
|
||||
from battery_dispatch.markets import MARKET_1, MARKET_2, Market
|
||||
|
||||
# Equal to Attachment 1, but stated literally so the tests do not depend on the
|
||||
# workbook being present or unchanged.
|
||||
SPEC = BatterySpec(
|
||||
max_charge_mw=2.0,
|
||||
max_discharge_mw=2.0,
|
||||
capacity_mwh=4.0,
|
||||
charge_efficiency=0.95,
|
||||
discharge_efficiency=0.95,
|
||||
lifetime_years=10.0,
|
||||
lifetime_cycles=5000.0,
|
||||
degradation_pct_per_cycle=0.001,
|
||||
capex_gbp=500_000.0,
|
||||
fixed_opex_gbp_per_year=5_000.0,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def spec() -> BatterySpec:
|
||||
return SPEC
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config() -> RunConfig:
|
||||
return RunConfig()
|
||||
|
||||
|
||||
def markets_from(
|
||||
market_1_prices: list[float],
|
||||
market_2_prices: list[float] | None = None,
|
||||
) -> list[Market]:
|
||||
"""Build both markets from a half-hourly Market 1 price list.
|
||||
|
||||
``market_2_prices`` is given per *hour*; when omitted Market 2 is priced so
|
||||
low that it never trades, isolating Market 1 behaviour.
|
||||
"""
|
||||
n = len(market_1_prices)
|
||||
if market_2_prices is None:
|
||||
broadcast = np.zeros(n)
|
||||
else:
|
||||
assert n == 2 * len(market_2_prices), "one Market 2 price per hour"
|
||||
broadcast = np.repeat(np.asarray(market_2_prices, dtype=float), 2)
|
||||
|
||||
return [
|
||||
Market(MARKET_1, 1, np.asarray(market_1_prices, dtype=float)),
|
||||
Market(MARKET_2, 2, broadcast),
|
||||
]
|
||||
|
||||
|
||||
def prices_frame(
|
||||
market_1_prices: list[float],
|
||||
market_2_prices: list[float],
|
||||
start: str = "2018-01-01",
|
||||
) -> pd.DataFrame:
|
||||
"""A price frame shaped like the one ``load_prices`` returns."""
|
||||
n = len(market_1_prices)
|
||||
index = pd.date_range(start, periods=n, freq="30min")
|
||||
return pd.DataFrame(
|
||||
{
|
||||
"market_1_price": np.asarray(market_1_prices, dtype=float),
|
||||
"market_2_price": np.repeat(np.asarray(market_2_prices, dtype=float), 2),
|
||||
"hour_index": np.arange(n) // 2,
|
||||
},
|
||||
index=index,
|
||||
)
|
||||
Reference in New Issue
Block a user