Add battery dispatch implementation and Gitea CI pipeline
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>
This commit is contained in:
2026-09-24 13:32:06 +01:00
co-authored by Claude Sonnet 5
parent 54e877db9b
commit 298724a9d3
22 changed files with 3330 additions and 0 deletions
+75
View File
@@ -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,
)