diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..a40c12046 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,84 @@ +# Agent Rules for the evcc optimizer + +This file provides guidance to AI coding agents working in this repository. + +## Project Overview + +- The optimizer computes cost optimal home energy schedules for evcc: battery charging and discharging, grid import and export, given price, solar, and demand forecasts. +- The core is a Mixed Integer Linear Program (MILP) built with PuLP and solved with CBC, exposed over an HTTP API (Flask plus flask-restx). +- A small Go CLI client (`cmd/`, generated `client/`) calls the API and renders the result as tables and charts. +- Python 3.13 managed with `uv`. Go 1.24. + +## Essential Commands + +All Python commands run through `uv`. Use `uv run python`, never bare `python`, it is not on PATH. + +- `make test` runs the Python test suite (`uv run pytest`) +- `make lint` formats and lints (`autopep8` then `ruff check --fix`) +- `make run` starts the API locally on port 7050 (`uv run python -m optimizer.app`) +- `make build` regenerates the Go client from the OpenAPI spec (`go generate ./...`) +- `make install` syncs dependencies (`uv sync`) +- `make loadtest` runs Locust load tests against a local instance + +## Architecture + +- `src/optimizer/optimizer.py` builds and solves the MILP. `Optimizer` assembles the variables, objective, and constraints, then `solve()` returns the result dict. +- `src/optimizer/app.py` is the Flask API. It parses the request into the dataclasses (`GridConfig`, `BatteryConfig`, `TimeSeriesData`, `OptimizationStrategy`), runs the optimizer, and marshals the response. The optimization endpoint is `POST /optimize/charge-schedule`. +- `src/optimizer/settings.py` holds runtime settings (solver threads, time limit) via pydantic-settings with the `OPTIMIZER_` env prefix. +- `openapi.yaml` is the source of truth for the API contract. +- `cmd/client.go` is the Go CLI client. `client/client.gen.go` is generated from `openapi.yaml` and must not be edited by hand. +- `tests/` holds the test suite, `test_cases/*.json` hold data driven scenarios. + +## Domain Knowledge + +The model is a maximization problem. Read these before changing it: + +- The objective is maximized. Costs and penalties enter as negative terms, so every penalty coefficient must be strictly positive to actually penalize. +- Energy is in Wh, power limits in W, prices per Wh. Time steps carry individual durations `dt` in seconds, so convert power to energy with `dt / 3600`. +- Penalty coefficients scale from a positive floor, `np.max([max_import_price, 0.1e-3])`. This keeps penalties positive even when market prices are zero or negative. +- Charging and discharging strategies are cost-neutral tie-breakers. They add tiny soft terms (coefficient around `min_import_price * 1e-6`) that only decide between economically equal solutions. They are intentionally excluded from `get_clean_objective_value()`, which recomputes the real economic value without strategy incentives or penalties. +- `get_clean_objective_value()` measures battery value as `(s[T-1] - s[0]) * p_a`, but `s[0]` already includes the first time step's charging, so energy charged in the first step is not counted as a gain. The optimization objective itself uses the absolute final state of charge, `s[-1] * p_a`. Two solutions that are equal in the real objective can therefore report different clean values. Keep optional charging off the first time step when designing cost-neutrality scenarios. +- Grid limits are soft: exceeding `p_max_imp` or `p_max_exp` is penalized rather than forbidden, so an over constrained request reports the violation instead of returning infeasible. + +## Python Coding Standards + +- Line length is 160 (`ruff` and `autopep8` are configured to match). Run `make lint` before committing. +- ruff rule sets in use: `F`, `I`, `E`, `W`, `PL`. Respect import ordering (`I`). +- Reuse the NumPy helpers already present in the model rather than hand rolling loops. +- Type hint new public functions and dataclass fields. + +## API and Code Generation + +- Change the API by editing `openapi.yaml` and the matching flask-restx models in `app.py` together, then run `make build` to regenerate the Go client. +- Never edit `client/client.gen.go` by hand. The generator config is `tools/cfg.yaml`, invoked through `tools/generate.go`. + +## Testing + +- The suite runs with `uv run pytest`. +- `test_cases/*.json` are loaded by `tests/test_app.py`. Each file holds a `request` and an optional `expected_response`, and the test asserts the optimizer status and, with `numpy.isclose`, the objective value. Add a scenario by dropping in a JSON file. +- For behavior the data driven harness cannot assert, such as a specific charge schedule, construct an `Optimizer` directly in a dedicated test module and assert on the returned dict. +- Cover both the success and the limit violation paths. + +## Writing Style + +- No em dashes in comments, commit messages, or docs. Use periods, commas, or colons. +- Project name is `evcc`, always lowercase. +- Uppercase acronyms in prose: MILP, API, HTTP, JWT, SoC. +- Commit subjects follow conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, `infra:`, then a short description with no trailing period. Reference the issue when relevant, for example `fix: use max() for penalty scaling floor (closes #75)`. + +## Comment Style + +- Prefer self documenting code. Comment the why, not the what. +- Default to no comment. Add one only for a non obvious constraint, invariant, or surprising behavior, and keep it to one or two lines. +- Do not reference the current task, PR, or issue in code comments. Git history covers that. + +## Pull Request Descriptions + +Structure PR descriptions in this order. No headlines. Be concise. + +1. References first line: link the related issue or PR (`closes #71`, `fixes #123`, `pairs with evcc-io/evcc#456`). A PR should almost always reference an issue, only skip for trivial fixes. +2. Intro: one or a few sentences framing what the PR does and why. The full problem belongs in the linked issue. +3. Bullet list: the most significant changes or user facing implications, most significant first. +4. TODO section only if open points remain. + +Avoid file paths, line numbers, or code listings copied from the diff. Include a code snippet only when it conveys a contract more clearly than prose. No testing checklists, no co-author footers, no generator footers.