Content
<p align="center">
<img src="docs/assets/edge-cortex-hero.svg" alt="Edge-Cortex: a wind turbine dissolving into a neuro-symbolic circuit core" width="100%">
</p>
<h1 align="center">Edge-Cortex</h1>
<p align="center"><b>A neuro-symbolic decision OS that runs a wind farm on its own, and proves every decision.</b></p>
<p align="center">
<a href="https://jtzanetatos.github.io/edge-cortex/"><img alt="Live demo" src="https://img.shields.io/badge/live_demo-offline_replay-27E0E8?style=flat-square&logo=githubpages&logoColor=071018"></a>
<a href="LICENSE"><img alt="License: AGPL v3" src="https://img.shields.io/badge/license-AGPLv3-b06cf5?style=flat-square&logo=gnu&logoColor=white"></a>
<img alt="Python" src="https://img.shields.io/badge/python-3.13-27E0E8?style=flat-square&logo=python&logoColor=white">
<img alt="PyTorch" src="https://img.shields.io/badge/PyTorch-2.12-b06cf5?style=flat-square&logo=pytorch&logoColor=white">
<img alt="ONNX Runtime" src="https://img.shields.io/badge/ONNX_Runtime-1.26-1FE0A0?style=flat-square&logo=onnx&logoColor=071018">
<img alt="DSPy" src="https://img.shields.io/badge/DSPy-3.2-b06cf5?style=flat-square">
<img alt="Ollama" src="https://img.shields.io/badge/Ollama-local_LLMs-27E0E8?style=flat-square&logo=ollama&logoColor=white">
<img alt="NiceGUI" src="https://img.shields.io/badge/NiceGUI-3.12-1FE0A0?style=flat-square">
<img alt="FLORIS" src="https://img.shields.io/badge/FLORIS-4.6-F5A623?style=flat-square">
<img alt="DuckDB" src="https://img.shields.io/badge/DuckDB-audit_log-F5A623?style=flat-square&logo=duckdb&logoColor=071018">
<br>
<img alt="Apple Silicon" src="https://img.shields.io/badge/tested-Apple_Silicon_M4_24GB-27E0E8?style=flat-square&logo=apple&logoColor=white">
<img alt="Linux CUDA" src="https://img.shields.io/badge/tested-Linux_·_RTX_5060_16GB-1FE0A0?style=flat-square&logo=nvidia&logoColor=071018">
<img alt="Status" src="https://img.shields.io/badge/status-competition_prototype-b06cf5?style=flat-square">
</p>
---
Edge-Cortex turns a wind farm into a system that **decides for itself**. Three neural
surrogates (failure, power, wake) feed an exact symbolic optimisation layer (LP / MILP /
payback gate); a **two-agent LLM pipeline** turns farm state into deterministic tool calls;
and an **autonomous watchdog** runs that pipeline with no human in the loop when the farm
state crosses a threshold. Every neural claim is cross-checked against **FLORIS physics**
before it is allowed to act. It all runs locally: small quantised LLMs on Ollama, surrogates
compiled to ONNX with custom Fourier operators, so there is no cloud dependency in the
control loop.
> **▶ Try it with no install:** an offline, fully client-side replay of the operations HUD
> lives at **[the live demo](https://jtzanetatos.github.io/edge-cortex/)**: no agents, no
> models, no backend, just a baked snapshot of the real replay week. Source in [`site/`](site/).
### Isolation is a first-class design property
The agents that **take actions** (Director → Operations) are **sealed off from every operator
and user input path**. There is no command box and no way for a human to inject an instruction
into the acting pipeline. Operators **observe**, and ask questions through a **separate,
read-only assistant**. The acting pipeline runs *only* on machine-written watchdog triggers.
This removes an entire class of abuse/attack surface: you cannot socially-engineer,
prompt-inject, or fat-finger an autonomous action into existence.
---
## What it does
- **Predicts failure** per turbine with a Liquid Neural Network (RUL + P(fail 7/30/60 d)),
and surfaces a **calibrated** conformal interval instead of a fake confidence number.
- **Optimises dispatch & wake steering** against live market prices with a trust-region LP
whose objective gradients *are* the FNO wake-coupling Jacobian + KAN own-power diagonal.
- **Acts on its own** when the state crosses a threshold, then **vetoes itself** two ways:
no plan executes if FLORIS disagrees on the sign of the gain (physics veto), or if a
**payback gate** decides the wind won't hold long enough to repay the actuator wear
(economic veto, the myopic step of a model-predictive controller).
- **Audits everything.** Every decision, human-driven or autonomous, is written to DuckDB
with replay-time and is queryable by the agent itself.
---
## Architecture
<p align="center">
<img src="docs/assets/architecture.svg" alt="Edge-Cortex architecture: the autonomous watchdog fires the Director then Operations pipeline, which calls the neural surrogates, the FLORIS physics oracle and the optimisers, emitting an EXECUTE or RECOMMEND decision with confidence into the DuckDB audit log and feeding back to SCADA. Below an isolation boundary, operators reach only a read-only assistant that reads live state and never commands the agents." width="100%">
</p>
A process-wide `ReplayClock` ties every component to the same simulated "now", so the
dashboard, the acting pipeline, and the watchdog all ingest ENTSO-E prices, Open-Meteo
weather, and Kelmarsh SCADA windows for the *same* moment in the demo timeline.
Three layers, each independently defensible and **bidirectionally coupled**: the neural side
*writes* the optimiser's objective, the symbolic side *governs and vetoes* the neural side:
| Layer | What | Why it is load-bearing |
|---|---|---|
| **Neuro** | LNN · KAN · FNO surrogates (ONNX, custom Fourier ops) | Fast enough to call *inside* an optimiser loop |
| **Symbolic** | Trust-region LP · day-ahead MILP · payback gate · FLORIS oracle | Exact optimisation, and a physics veto on the neural layer |
| **Agentic** | Director → Operations pipeline + autonomous watchdog | Translates intent into typed tool calls, governs *when* to act |
---
## Models
All served as ONNX via `dashboard/inference.py`. Weights are **not committed** (see
[Model weights](#3-model-weights-not-in-the-repo)); missing weights are non-fatal and fall
back to a clearly-badged synthetic source.
| Model | Type | Role |
|---|---|---|
| **LNN-RUL** | Liquid Time-Constant network | Per-turbine RUL + P(fail 7/30/60 d) with an attention readout; trained on ~28 M rows of Kelmarsh SCADA |
| **KAN-Power** | Kolmogorov–Arnold network | Analytical-symbolic `P(wind, yaw)`; supplies own-power gradients to the LP |
| **FNO-Wake** | Tensorised Fourier Neural Operator | 128×128 farm wake-velocity field; supplies cross-turbine coupling |
| **DCFF-MTAD** | Multivariate anomaly detector | Per-turbine anomaly scores vs a learned threshold |
| **FLORIS** | Engineering wake physics (not learned) | **Ground-truth validator** for every wake claim |
Two credibility details: the FNO is **physically anchored** (sampled half a rotor-diameter
upwind of each rotor and converted through the real NREL power curve, landing within ~9% of
FLORIS with sign- and magnitude-consistent steering deltas), and the LNN's uncertainty is a
**distribution-free 90% conformal interval**, surfaced rather than hidden.
---
## Autonomous functionality
A deterministic watchdog (`dashboard/autonomous_director.py`, **not** an LLM) watches the
replay clock for **9 triggers across 6 families**. When one fires it runs the same
Director → Operations pipeline on a background thread; if the result is `EXECUTE` **and**
confidence ≥ `0.70` the action stands and is tagged `⚡ EXECUTED`, otherwise it downgrades to a
logged `◆ RECOMMENDED`.
| Trigger | Family | Fires when |
|---|---|---|
| `rul_imminent` | Reliability | P(fail ≤ 7 d) > 0.70 for any turbine |
| `rul_warning` | Reliability | P(fail ≤ 30 d) > 0.60, newly crossed |
| `anomaly_rca` | Anomaly | a turbine's DCFF-MTAD score ≥ 85% of its threshold |
| `anomaly_correlated` | Anomaly | ≥ 2 turbines flagged at once |
| `price_spike` | Market | \|price − 24 h mean\| > 2σ |
| `wake_loss` | Wake | farm-wide wake deficit ≥ 6% |
| `wind_shift` | Weather | \|Δ direction\| > 30° within an hour |
| `ops_review` | Scheduled | periodic operational-review heartbeat |
| `daily_schedule` | Scheduled | once-per-replay-day planning pass |
Each trigger has its own debounce so a sustained condition fires once, and a single-flight
gate prevents pipeline calls from queueing. Every decision is logged to DuckDB, pushed to the
Recent Autonomous Actions strip, and rendered in the console with a purple `AUTONOMOUS` badge.
---
## The two agents + the assistant
- **Director** · reasoning LM (`qwen3.5:4b-mxfp8`, DSPy `ChainOfThought`, prompt-compiled).
Turns one directive into a strict, validated task spec (`intent` is one of 7 closed values).
It plans; it does not execute.
- **Operations** · tool-calling LM (same resident weights, DSPy `ReAct`). Executes the task
spec by orchestrating **22 tools across 13 MCP servers** in a Thought→Action→Observation
loop, then summarises citing tool-derived numbers only, emitting a structured
`EXECUTE`/`RECOMMEND` decision + calibrated confidence.
- **Read-only assistant** · a separate lightweight model (`lfm2.5-thinking:1.2b`) that answers
operator questions and writes reports, grounded in live state. **It takes no actions and has
no path into the acting pipeline.**
### MCP tool servers (13, stdio)
`inference` · `fno_wake_coupling` · `floris_sim` · `kan_linearization` · `schedule_milp` ·
`wake_steering_lp` · `dcff_mtad` · `causal_graph` · `data_analysis` · `entso_e` · `weather` ·
`logbook` · `pdf_export`.
---
## Tech stack
Python 3.13 · PyTorch 2.12 + Lightning · `neuraloperator` 2.0 · `ncps` (LNN) · ONNX Runtime
1.26 with a native custom Fourier spectral-conv operator · DSPy 3.2 + Ollama · FLORIS 4.6 ·
Polars 1.41 · DuckDB 1.5 · NiceGUI 3.12 (ECharts HUD). The offline demo in [`site/`](site/) is
vanilla HTML/CSS/JS + Apache ECharts, no build step.
---
## Prerequisites
| Component | Recommended | Notes |
|---|---|---|
| OS | macOS or Linux | Tested on **Apple Silicon M4 (24 GB)** and **Linux + NVIDIA RTX 5060 (16 GB VRAM)** |
| Python | 3.13 | [uv](https://docs.astral.sh/uv) recommended (or venv) |
| Ollama | latest | <https://ollama.com> |
| RAM / VRAM | ≥ 16 GB | `qwen3.5:4b-mxfp8` resident ~13 GB; `lfm2.5-thinking:1.2b` ~1 GB |
| Disk | ≥ 60 GB free | Kelmarsh parquet ~150 MB; LLM weights dominate |
| Network | required at first boot | ENTSO-E + Open-Meteo + Ollama model pull |
On Apple Silicon, Ollama uses the Metal GPU on the host. On Linux/NVIDIA, ONNX Runtime and
Ollama use CUDA. (Docker runs the app but **not** Ollama; see [Docker](#docker).)
---
## Setup
### 1. Python environment
```bash
# uv (https://docs.astral.sh/uv) creates the env and installs from requirements.txt
uv venv --python 3.13 # creates .venv
source .venv/bin/activate
uv pip install -r requirements.txt
```
Prefer not to activate a shell? `uv run <cmd>` runs anything inside the project env, e.g.
`uv run python main.py`.
### 2. Ollama models
```bash
ollama pull qwen3.5:4b-mxfp8 # Director + Operations brain (~13 GB, reused for both)
ollama pull lfm2.5-thinking:1.2b # read-only operator assistant (~1 GB)
ollama list
```
(`start.sh` pulls these automatically on first launch if missing.)
### 3. Model weights (not in the repo)
The ONNX weights and the multi-GB data tree are **git-ignored**: the repo ships source, not
artifacts. Export/train them locally into `weights/`:
```
weights/
├── lnn_rul_v1.onnx # scripts/train_lnn.py → export
├── kan_power_v1.onnx
├── fno_surrogate_v1.onnx # export/export_fno_custom_onnx.py
└── *_scaler.json
```
Missing weights are **non-fatal**: the corresponding panel falls back to a synthetic source and
shows a badge. Retrain the LNN from scratch on Kelmarsh with `python scripts/train_lnn.py`.
### 4. Data hydration
The ~47 GB data tree is not in the repo. Fetch it from the public sources (Kelmarsh → Zenodo,
FLORIS generated locally); see [`docs/DATA.md`](docs/DATA.md) for provenance and licensing.
```bash
python scripts/fetch_data.py --all # download + build the demo parquets
python scripts/fetch_data.py --dry-run --kelmarsh
```
### 5. Environment variables (`.env` in repo root)
```
OLLAMA_API_BASE=http://localhost:11434
ENTSOE_API_KEY=<your-entsoe-key> # free at https://transparency.entsoe.eu
# optional overrides (defaults in agents/llm.py + start.sh)
DIRECTOR_MODEL=qwen3.5:4b-mxfp8
OPERATIONS_MODEL=qwen3.5:4b-mxfp8
CHAT_MODEL=lfm2.5-thinking:1.2b
LLM_NUM_CTX=32768
```
Without an ENTSO-E key the market card falls back to a synthetic 24 h curve.
---
## Running
### Quick start (one command)
```bash
./start.sh
```
It probes Ollama (starting the daemon if needed), pulls + warms both models, activates `.venv`
if present, and launches `main.py`. Override the moving parts via env vars:
```bash
DASHBOARD_PORT=9000 ./start.sh
SKIP_WARM=1 ./start.sh # skip the warm-up chat call
NO_COLOR=1 ./start.sh # plain output for CI / logs
```
### Manual
```bash
python main.py # → http://127.0.0.1:8080
python main.py --host 0.0.0.0 --port 8080 # bind all interfaces
python main.py --skip-probe # fast UI iteration; skips the weight check
```
You should see the `EDGE-CORTEX · v2.1` boot banner, a model-artefact summary, and the
NiceGUI URL.
### Docker
The dashboard + MCP servers + ONNX stack are containerised; **Ollama runs on the host** (a
containerised LLM can't reach the Apple-Silicon Metal GPU). The container reaches host Ollama
via `host.docker.internal`.
```bash
ollama serve
ollama pull qwen3.5:4b-mxfp8 && ollama pull lfm2.5-thinking:1.2b
python scripts/fetch_data.py --all
cp .env.example .env # add your ENTSO-E key (optional)
docker compose up --build # dashboard → http://localhost:8080
```
`data/` and `weights/` are bind-mounted read-only; the DuckDB log and PDFs persist via
writable `database/` and `reports/` mounts.
---
## Operating the demo
The dashboard boots at **2023-11-20 08:00 UTC**, a week before the densest multi-turbine
failure window in the Kelmarsh dataset (2023-11-27). The replay clock advances every
wall-second; the speed controls (`1× / 10× / 60×`) set how much simulated time passes per real
second. Hit `10×` and let it run. Over the simulated week:
- Per-turbine RUL bars descend; the tactical chip flips amber on `watch`, red on `anomaly`.
- The **autonomy chip** pulses while a pipeline call runs; on completion a new card appears in
the Recent Autonomous Actions strip and a purple `AUTONOMOUS` entry in the console.
- The **Trajectory Terminal** flips from its synthetic seed to the live Director reasoning +
Operations trajectory + response for the most recent autonomous decision.
### Talking to the read-only assistant
Operators interact through the **read-only assistant**: it answers questions and writes
reports grounded in live state, but **cannot command the acting agents**. Examples:
| Ask | What you get |
|---|---|
| `What's the fleet doing right now?` | A grounded snapshot: which turbines are nominal / on watch, and why. |
| `Why did we just yaw T2 and T4?` | An explanation of the last autonomous decision (trigger, FLORIS check, payback ratio). |
| `Draft an operational report` | A generated narrative report from current farm state (Dynamic Report tab). |
Actions themselves come **only** from the watchdog. The demo is something you *watch*, not
something you *drive*.
### Tuning autonomous behaviour
Constants in `dashboard/autonomous_director.py` (env-overridable):
| Constant | Default | Effect |
|---|---|---|
| `RUL_IMMINENT_P7D_THRESHOLD` | `0.70` | Lower ⇒ fires earlier on rising failure risk |
| `RUL_WARNING_P30D_THRESHOLD` | `0.60` | Edge-triggered failure warning |
| `PRICE_SPIKE_Z_THRESHOLD` | `2.0` | σ above the 24 h rolling mean |
| `WAKE_LOSS_THRESHOLD` | `0.06` | Farm-wide deficit that fires a wake-steer |
| `WIND_SHIFT_DEG_THRESHOLD` | `30.0` | Degrees per replay-hour |
| `TRIGGER_DEBOUNCE_REPLAY_SEC` | `21600` (6 h) | How long a fired trigger stays suppressed (`AUTONOMY_DEBOUNCE_REPLAY_SEC`) |
| `EXECUTE_CONFIDENCE_THRESHOLD` | `0.70` | Above ⇒ `⚡ EXECUTED`, below ⇒ `◆ RECOMMENDED` |
### Inspecting the logbook
```bash
duckdb database/audit_log.db
> SELECT event_type, actor, replay_time, payload
FROM audit_log ORDER BY event_id DESC LIMIT 10;
```
---
## Repository layout
```
edge-cortex/
├── main.py Bootloader: registry probe → DuckDB → NiceGUI
├── agents/ Two-agent DSPy + Ollama pipeline + read-only assistant
│ ├── pipeline.py Director → Operations orchestration (acting pipeline)
│ ├── director.py ChainOfThought task-spec extractor
│ ├── operations.py ReAct tool-orchestrator + MCP wrappers
│ └── console_chat.py Read-only operator assistant (lfm2.5, no actions)
├── dashboard/
│ ├── app.py NiceGUI layout, panels, replay tick
│ ├── autonomous_director.py Watchdog + 9 triggers (the only pipeline caller)
│ ├── inference.py ONNX runtime pool (LNN / KAN / FNO)
│ ├── live_data.py LiveDataCache (ENTSO-E + Open-Meteo)
│ └── replay_clock.py Process-wide simulated-time singleton
├── mcp_servers/ 13 MCP tool servers (stdio)
├── models/ LNN / KAN / FNO network + Lightning modules + registry
├── export/ · onnx_ops/ Custom ONNX export + native spectral-conv operator
├── scripts/ Data generation, training, calibration, prompt optimisation
├── database/ DuckDB AuditLogger + schema
├── site/ Offline GitHub Pages demo (self-contained, no backend)
├── weights/ ONNX surrogates (git-ignored, exported locally)
└── data/ Kelmarsh parquet, NREL CSVs, datasheet PDFs (git-ignored)
```
---
## Synthetic fallbacks
Edge-Cortex stays usable through every degraded state: each panel falls back transparently
when its primary source is unavailable:
| Component | Primary | Fallback | Signal |
|---|---|---|---|
| Per-Turbine RUL | LNN ONNX | Synthetic pattern | synthetic-marker dots |
| FNO Wake Field | FNO ONNX | Analytical Bastankhah | (silent) |
| Day-Ahead Market | ENTSO-E | Synthetic 24 h curve | `SYNTH` chip |
| Spatial wind | Open-Meteo | Synthetic sine | source tag on label |
| Trajectory Terminal | last run | Synth seed | `SYNTH SEED` badge |
| Recent Events | DuckDB audit log | in-memory fallback | (silent) |
---
## Testing
```bash
uv run pytest -q # 16 test modules: agents, MCP servers, ONNX parity, payback gate, ...
uv run ruff check .
```
---
## License
Edge-Cortex is licensed under the **GNU Affero General Public License v3.0** (see
[`LICENSE`](LICENSE)). In short: you may use, study, modify, and self-host it, but if you
distribute it **or offer it over a network**, you must release your complete corresponding
source under the same AGPLv3 terms.
Copyright © 2026 jtzanetatos (tzanetatosi@gmail.com). The author retains full copyright.
**Commercial and proprietary use is reserved to the copyright holder.** The AGPL copyleft is
deliberate. Anyone building a commercial or closed-source product on Edge-Cortex must either
(a) comply fully with the AGPLv3, including publishing their complete corresponding source, or
(b) obtain a paid commercial license from the copyright holder. **Attribution alone does not
grant commercial rights.** No third party may sell, sublicense, host as a paid service, or
otherwise commercialise this software or a derivative without such a license. To arrange one,
contact the author.
---
## Acknowledgements
- **Kelmarsh SCADA** dataset: <https://zenodo.org/records/8252025>
- **NREL** reference turbine + power curve
- **Ollama** · **DSPy** · **NiceGUI** · **ncps** · **neuraloperator** · **FLORIS**
- ENTSO-E Transparency Platform · Open-Meteo
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
servers
Model Context Protocol Servers
servers
Model Context Protocol Servers
Time
A Model Context Protocol server for time and timezone conversions.