feat: Initial backend optimization engine and React dashboard#1
Conversation
There was a problem hiding this comment.
Pull request overview
This PR bootstraps a full-stack “battery dispatch optimization” prototype: a FastAPI backend that optimizes battery charge/discharge schedules (via LP/PuLP) and a React (Vite + TS + Tailwind) dashboard that calls the backend and visualizes the resulting schedule.
Changes:
- Added FastAPI backend with optimization engine and live OpenElectricity-based simulation endpoint.
- Added React dashboard scaffolding and chart/KPI UI for schedule visualization.
- Added CI workflow and initial pytest-based backend tests.
Reviewed changes
Copilot reviewed 27 out of 36 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
main.py |
FastAPI app, Pydantic models, PuLP optimization, /optimize + /simulate endpoints |
pipeline.py |
Local script to fetch live SA1 prices and call the API |
optimizer.py |
Standalone CSV-based optimization script (PuLP) |
generate_data.py |
Synthetic price CSV generator |
market_data.csv |
Example 30-minute price series CSV |
tests/test_main.py |
Unit/integration/validation tests for the backend |
pyproject.toml |
Project metadata, dependencies, pytest pythonpath config |
requirements.txt |
uv-compiled pinned dependencies (used by Docker build) |
Dockerfile |
Container build for running the FastAPI app |
.github/workflows/backend-tests.yml |
GitHub Actions workflow running pytest via uv |
.python-version |
Pins local Python version (3.12) |
.gitignore |
Ignores venv, caches, .env |
.vscode/launch.json |
VS Code debug configuration for uvicorn |
README.md |
Root documentation links/references |
frontend/package.json |
Frontend dependencies and scripts |
frontend/vite.config.ts |
Vite config with React + Tailwind plugin |
frontend/tsconfig.json |
TS project references |
frontend/tsconfig.app.json |
TS config for app build |
frontend/tsconfig.node.json |
TS config for tooling (vite config) |
frontend/eslint.config.js |
ESLint flat config for TS/React |
frontend/index.html |
App HTML entrypoint |
frontend/src/main.tsx |
React root render |
frontend/src/App.tsx |
Dashboard UI, fetches /api/v1/simulate, renders Recharts chart + KPIs |
frontend/src/types.ts |
Shared request/response TypeScript interfaces |
frontend/src/mock_data.ts |
Mock schedule data for UI experimentation |
frontend/src/index.css |
Global styles + Tailwind import |
frontend/src/App.css |
Template CSS from starter |
frontend/public/favicon.svg |
Favicon asset |
frontend/public/icons.svg |
Icon sprite asset |
frontend/src/assets/vite.svg |
Vite logo asset |
frontend/src/assets/react.svg |
React logo asset |
frontend/src/assets/hero.png |
UI hero image asset |
frontend/README.md |
Default Vite template README |
frontend/.gitignore |
Frontend-specific ignore patterns |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| prob += soc[t] == (charge[t] * battery.efficiency - discharge[t]) * dt | ||
| else: | ||
| # Update SoC based on the previous timestep | ||
| prob += soc[t] == soc[t-1] + (charge[t] * battery.efficiency - discharge[t]) * dt |
There was a problem hiding this comment.
There is no terminal SoC constraint (e.g., SoC must return to the initial level). If spot prices go negative near the end of the horizon, the optimizer can finish with a high SoC and still report profit (getting paid to charge without ever discharging), which overstates arbitrage value. Consider enforcing a terminal SoC (soc[T-1] == 0) or adding an explicit terminal energy value/penalty.
| prob += soc[t] == soc[t-1] + (charge[t] * battery.efficiency - discharge[t]) * dt | |
| prob += soc[t] == soc[t-1] + (charge[t] * battery.efficiency - discharge[t]) * dt | |
| # Enforce terminal SoC to avoid overstating arbitrage value from ending with stored energy | |
| if T > 0: | |
| prob += soc[T - 1] == 0 |
| return calculate_optimal_dispatch(battery, market_data) | ||
|
|
||
| except Exception as e: | ||
| return {"status": "error", "message": str(e)} No newline at end of file |
There was a problem hiding this comment.
The /api/v1/simulate endpoint swallows exceptions and returns a JSON error payload with HTTP 200. This makes failures hard to detect on the client and breaks standard API semantics. Prefer raising an HTTPException with an appropriate status code (and keep error response shape consistent with other endpoints).
| return {"status": "error", "message": str(e)} | |
| # Propagate errors with an appropriate HTTP status code | |
| raise HTTPException(status_code=500, detail=str(e)) |
| assert result["total_profit_aud"] == 90.0 | ||
| assert result["schedule"][0]["charge_mw"] == 1.0 # Should charge in the first block |
There was a problem hiding this comment.
This test assumes the solver will charge specifically in the first interval (schedule[0]). Since the first two intervals have identical prices, there are multiple optimal solutions and the solver may choose to charge in the second block instead, causing flaky failures. Assert on aggregate behavior (e.g., total charged energy across cheap blocks) or allow either of the first two blocks to be charged.
| create_synthetic_prices() | ||
| print("Generated market data.csv!") | ||
|
|
||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
This script executes create_synthetic_prices() at import time, which will overwrite market_data.csv whenever the module is imported (including by tooling). Guard the execution with if __name__ == "__main__": so it only runs when invoked as a script.
| create_synthetic_prices() | |
| print("Generated market data.csv!") | |
| if __name__ == "__main__": | |
| create_synthetic_prices() | |
| print("Generated market data.csv!") |
| [reference](https://gemini.google.com/app/d42f0dfeac9b254a?is_sa=1&is_sa=1&android-min-version=301356232&ios-min-version=322.0&campaign_id=bkws&utm_source=sem&utm_medium=paid-media&utm_campaign=bkws&pt=9008&mt=8&ct=p-growth-sem-bkws&gclsrc=aw.ds&gad_source=1&gad_campaignid=22437964261&gbraid=0AAAAApk5BhlTX3MkpF7nIacpTUb7etnbN&gclid=Cj0KCQiAnJHMBhDAARIsABr7b84shuWqfx0UvOhc3MwNnDRCJJO5jpV4lSTTbBx42jMRPPgo7ersQ84aAgB9EALw_wcB) | ||
|
|
||
|
|
There was a problem hiding this comment.
The README contains a long Gemini reference link with tracking parameters that doesn’t describe how to run/build/test the project. Consider removing it or replacing with a short, relevant project reference section (and keep URLs minimal).
| [reference](https://gemini.google.com/app/d42f0dfeac9b254a?is_sa=1&is_sa=1&android-min-version=301356232&ios-min-version=322.0&campaign_id=bkws&utm_source=sem&utm_medium=paid-media&utm_campaign=bkws&pt=9008&mt=8&ct=p-growth-sem-bkws&gclsrc=aw.ds&gad_source=1&gad_campaignid=22437964261&gbraid=0AAAAApk5BhlTX3MkpF7nIacpTUb7etnbN&gclid=Cj0KCQiAnJHMBhDAARIsABr7b84shuWqfx0UvOhc3MwNnDRCJJO5jpV4lSTTbBx42jMRPPgo7ersQ84aAgB9EALw_wcB) | |
| ## Project reference | |
| To run the FastAPI application locally: | |
| 1. Create and activate a Python virtual environment. | |
| 2. Install dependencies: `pip install -r requirements.txt`. | |
| 3. Start the server: `uvicorn main:app --reload`. |
| if __name__ == "__main__": | ||
| run_pipeline() | ||
|
|
||
| # https://gemini.google.com/app/d42f0dfeac9b254a?is_sa=1&is_sa=1&android-min-version=301356232&ios-min-version=322.0&campaign_id=bkws&utm_source=sem&utm_medium=paid-media&utm_campaign=bkws&pt=9008&mt=8&ct=p-growth-sem-bkws&gclsrc=aw.ds&gad_source=1&gad_campaignid=22437964261&gbraid=0AAAAApk5BhlTX3MkpF7nIacpTUb7etnbN&gclid=Cj0KCQiAnJHMBhDAARIsABr7b84shuWqfx0UvOhc3MwNnDRCJJO5jpV4lSTTbBx42jMRPPgo7ersQ84aAgB9EALw_wcB No newline at end of file |
There was a problem hiding this comment.
This file includes a very long Gemini URL comment at the end. It’s unrelated to runtime behavior, adds noise to the repo, and contains lots of tracking/query params. Consider removing it (or moving references into documentation with a clean URL).
| import pytest | ||
| from fastapi.testclient import TestClient | ||
| from datetime import datetime, timezone | ||
| from main import app, calculate_optimal_dispatch, BatterySpecs, MarketInterval |
There was a problem hiding this comment.
pytest is imported but never used in this test module.
| "request": "launch", | ||
| "module": "uvicorn", | ||
| "args": [ | ||
| "main:app", // Replace 'main' with your file name and 'app' with your FastAPI instance name |
There was a problem hiding this comment.
This launch configuration file contains // comments, which makes it invalid strict JSON (it’s JSONC). Some tools that parse .vscode/launch.json as JSON may fail. Consider removing comments or renaming/formatting according to JSONC expectations if the repo relies on strict JSON tooling.
| "main:app", // Replace 'main' with your file name and 'app' with your FastAPI instance name | |
| "main:app", |
| @@ -0,0 +1,18 @@ | |||
| # 1. Use an official, lightweight Python image | |||
| FROM python:3.11-slim | |||
There was a problem hiding this comment.
The Docker image uses Python 3.11, but the project declares requires-python = ">=3.12" and CI installs 3.12. This mismatch can cause runtime incompatibilities and makes the Docker build diverge from tested environments. Update the base image to Python 3.12 (or relax the project requirement if 3.11 is intended).
| FROM python:3.11-slim | |
| FROM python:3.12-slim |
| const base_url = "http://localhost:8000/api/v1"; | ||
| try { | ||
| const payload = { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json" | ||
| }, | ||
| body: JSON.stringify({ | ||
| "capacity_mwh": capacity, | ||
| "max_mw": maxMw, | ||
| efficiency: 0.9 // hardcorded for efficiency for now | ||
| }) |
There was a problem hiding this comment.
The frontend hard-codes the backend base URL and efficiency. This makes deployments and local dev (non-default ports/hosts) harder and couples UI behavior to a single environment. Consider moving these to environment variables (e.g., Vite import.meta.env) and making efficiency user-configurable or clearly fixed in UI.
|
Good job (: |
Overview
This PR introduces the core Battery Dispatch Optimization engine and its accompanying interactive frontend dashboard. It establishes the full-stack architecture, connecting a mathematically optimized FastAPI backend to a React visualization layer using live Australian grid data.
Key Changes
Backend (FastAPI & Math Engine)
PuLPto calculate the maximum-profit battery charge/discharge schedule based on capacity, max power, and efficiency.OpenElectricityPython SDK andpandasto fetch live 5-minute spot prices for the SA1 (South Australia) region and resample them into 30-minute intervals./api/v1/simulateendpoint with Pydantic validation and CORS configured for the frontend.Frontend (React & Vite)
Rechartsgraph to simultaneously visualize the volatile electricity spot price (duck curve) and the battery's State of Charge (SoC) decisions.Infrastructure & Quality
backend-tests.yml) utilizinguvto automatically run the test suite on PRs and pushes tomain.pytestcoverage for the core math engine, API routing, and edge-case validation.Validation