CI / test (push) Failing after 45m4s
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>
76 lines
2.1 KiB
Python
76 lines
2.1 KiB
Python
"""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,
|
|
)
|