Community Energy Flex is a decision-support system for scheduling flexible household electricity tasks. It combines half-hourly carbon data, tariff inputs, task constraints, and rule-based or linear-programming optimisers to recommend feasible run windows with explicit baselines, provenance, and caveats.
It is a portfolio demonstration, not a control system or a savings guarantee. The case-study, Power BI, and synthetic stress-test figures use illustrative household data; they are not measured customer outcomes.
Open the web app · API documentation · Read the worked example
- A typed Python domain model for tasks, half-hour slots, tariffs, schedules, and baselines.
- Rule-based and LP/MILP scheduling under time, energy, and overlap constraints. The MILP earns its place through one thing the rule-based path structurally cannot express: a peak-load limit that couples every task in the same half-hour slot.
- A FastAPI contract that reports the actual carbon and price source, whether it is live, and any fallback reason.
- A Next.js interface that keeps fallback/sample status visible instead of presenting it as live data.
- A robustness indicator that describes sensitivity to current inputs. It is a heuristic, not a calibrated probability.
- Conditional ex-post scenario analysis. Because task adherence is not observed, this is a synthetic stress test rather than realised-savings evidence.
- Text, Excel, PDF, dbt, Dagster, and Power BI reporting paths. Snowflake is a bootstrap DDL
script (
warehouse/snowflake_setup.sql) plus a second dbt profile target; nothing in this repository has been built against a live Snowflake account. Status labels the whole warehouse row built-local / synthetic-demo.
Fresh action reports are blocked when optimisation fails or required provenance
is absent. Deterministic fixture mode (CEF_FIXTURE_MODE=1) uses labelled
synthetic-household inputs for CI and demonstrations; it requires no paid or
live external API call. All dashboard screenshots and KPIs are illustrative,
conditional ex-post where applicable, and not a savings guarantee.
The MILP is a real MILP, not a renamed greedy loop.
optimisation/linear_programming.py
builds a pulp.LpProblem with one binary variable per (task, feasible start), a
must-run-once constraint per task, per-task deadlines, and — the reason to reach for an LP at
all — a peak-load constraint that caps the total kW drawn in any half-hour slot across every
task at once. The rule-based optimiser schedules each task independently and cannot express
that coupling. CBC solves it; any non-Optimal status raises InfeasibleScheduleError
instead of returning a partial schedule. When the shared limit pushes a task off its
standalone-best slot, the recommendation caps its robustness and says why. Six dedicated
tests cover the coupling, the infeasible case, and parity with the rule-based optimiser when
no shared limit applies. It is a library path under test, not a product surface: the API, the
Streamlit app, and the daily pipeline all call the rule-based optimiser, and
Status records the MILP as built-local.
The public claims are a CI gate.
tests/test_public_claims.py reads the README, case study,
retro, status, and both public web pages, and fails the build if retired claims reappear or
if the README stops stating the synthetic-evidence boundary. Marketing copy is the surface
most likely to drift ahead of the evidence, so it is tested like anything else.
One enforced data contract. fct_daily_savings sets contract: enforced: true with a
declared data_type on all 33 columns, so a renamed column or a changed type fails
dbt build rather than quietly reshaping the Power BI star. Four exposures — FastAPI,
Next.js, Power BI, and the action report — are declared at maturity: high, so the
downstream blast radius of a model change is in the lineage rather than in someone's head.
One fixture reconciles four runtimes.
data/fixtures/reporting_contract_v1.json is
consumed by the dbt seed generator, the Python tests, the API contract tests, and the Next.js
client tests. The same reporting shape is therefore asserted in SQL, Python, and TypeScript
from a single committed source.
Orchestration is deliberately thin. Three Dagster assets, one job, one schedule. The
assets wrap pipeline/daily.py and hold no
business logic, so the pipeline is unit-tested as plain functions and the scheduler stays
swappable. See the Dagster pipeline notes.
CI runs the Python suite on 3.11, 3.12, and 3.13, alongside a typecheck and test pass on
the Next.js client and a full DuckDB dbt build of the reporting contract.
The public endpoints keep their original hostnames; those URLs are stable deployment identifiers, not the current product name.
| Surface | URL | Notes |
|---|---|---|
| Web app | after-midnight-beta.vercel.app | Next.js on Vercel |
| API | community-energy-flex-api.fly.dev | FastAPI on Fly.io |
The API attempts the GB Carbon Intensity forecast for supported regions. If that source is unavailable, the response names the fallback profile and explains why it was used. Northern Ireland uses a labelled typical profile; it is not advertised as a live forecast.
Python 3.11–3.13 is supported.
python -m venv .venv
# Windows: .venv\Scripts\activate
# macOS/Linux: source .venv/bin/activate
pip install -e ".[dev,reports,api,app,optim]"
python -m pytest
python -m ruff check .Run the API and web client in separate terminals:
uvicorn community_energy_api.main:app --app-dir api --reload
cd web
npm install
npm run devA Streamlit interface is also included:
streamlit run app/streamlit_app.pyflowchart LR
C[Carbon curve] --> P[Half-hour planning slots]
T[Tariff] --> P
U[Task constraints] --> O[Rule-based or LP/MILP optimiser]
P --> O
O --> S[Recommended schedule]
S --> R[Baseline comparison, robustness, caveat]
R --> A[Web, API and reports]
For each task, the engine enumerates feasible starts, estimates cost and carbon for each window, applies the selected objective, and compares the recommendation with an explicit preferred-start baseline. Standing charges are excluded because moving a task does not change them.
The robustness indicator combines decisiveness, forecast horizon, data-source quality, and tariff quality. It is deliberately labelled as an indicator rather than confidence: it has not been calibrated against observed recommendation outcomes.
See Methodology for the equations and limitations.
The public contract exposes structured source metadata:
carbon_source,carbon_source_label,is_live_forecast, retrieval/valid timestamps, andfallback_reason;price_source,price_source_label,price_is_live, andprice_unavailable_reason;supports_live_forecastas a regional capability flag, separate from whether a particular response actually used live data.
The synthetic retro workflow recomputes the scheduled and baseline windows against altered carbon curves. It does not observe whether a household followed the recommendation, so its outputs are named conditional ex-post savings and schedule_adherence_observed is false.
Project claims and evidence boundaries are tracked in the claim ledger. Current operating state and freeze scope are in status.
Both dashboard images use synthetic-household, illustrative planning data. KPI
differences are conditional ex-post where applicable and are not a savings
guarantee. The model, DAX, theme, and reproducible seed path are in
powerbi/; see the dashboard guide.
| Path | Responsibility |
|---|---|
src/community_energy_flex/ |
Domain model, data sources, optimisation, monitoring, reporting |
api/community_energy_api/ |
FastAPI contract and service adapter |
web/ |
Next.js public interface |
tests/, api/tests/ |
Unit, contract, fallback, robustness, and export tests |
dbt_energy/, warehouse/ |
Analytics transformations and Snowflake bootstrap |
orchestration/ |
Dagster assets |
powerbi/ |
Dashboard model, measures, theme, and guidance |
docs/evidence/credibility-closeout/ |
Versioned closeout evidence |
| Document | Purpose |
|---|---|
| Status | Current scope, limitations, and feature freeze |
| Claim ledger | Public claims and their evidence status |
| Data sources | Source semantics and fallback rules |
| Methodology | Baseline, objectives, robustness, and conditional ex-post analysis |
| Architecture | Module boundaries and request flow |
| Metric catalogue | Units, signs, owners, and publication conditions |
| Reporting lineage | Core, Dagster, dbt and consumer boundaries |
| Runbook | Local operation and failure modes |
| Future research | Work intentionally deferred beyond v0.2.0 |
| Security | Responsible disclosure |
Version 0.2.1 continues the v0.2.0 credibility closeout; it remains a feature-freeze release, with post-release correctness and honesty fixes. Maintenance is limited to correctness, dependency and security updates, and deployment reliability; new research claims require new observed evidence and an explicit scope decision.
Product development stopped on 2026-07-27. An evidence scan found the scheduling function already served — by statute for EV charging, by supplier-dispatched tariffs, and by free tools built on the same two public APIs this project uses. The reasoning and the primary sources are recorded in status. One strand continues: a forecast-vintage archive capturing what the GB carbon-intensity forecast said and when, because that data cannot be recovered afterwards. It is a capture job answering a research question, not a product, and it has produced no result yet.
MIT © 2026 Cheng-Yuan King.


