I spent years in the renewable energy sector doing exactly this kind of work by hand: pulling SCADA data off real offshore turbines, building power curves, hunting for the sensor that's lying to you. So when I set out to build a portfolio project to sharpen a specific set of AI-engineering skills — LangGraph, RAG, agentic workflows, MLOps — I didn't want a toy dataset. I wanted the real thing.
From met masts to language models
My path through wind energy went in the same direction the wind's data flows. I started in wind resource assessment and meteorological instrumentation: met masts, cup and sonic anemometers, wind vanes and lidar, measuring the wind at a site before any turbine stands there, and learning how many ways an instrument can be wrong. From there I moved to numerical modelling of the wind resource, where a few months of measurements are extended across a whole site and over the long term with flow and weather models. Then the turbines were built and the question changed from "how much wind is there?" to "how much of it is each machine actually turning into energy?", and I spent years on SCADA data from operating wind farms: power curves, availability, and the turbine whose numbers do not add up.
For a long time those were three separate jobs, with separate tools and separate people. What changed recently is the fourth layer: large language models and agents that can read the outputs of all three and explain them in plain words. Windward is the first time I have put all four in one tool:
- Instrumentation is why the agent distrusts a nacelle anemometer that sits behind a spinning rotor, and why a fourth dataset in this project was worth chasing for its real on-site met mast.
- Numerical modelling is the forecast input: ERA5 reanalysis, the output of a global weather model, used instead of the turbines' own anemometers because a model is what you actually have at forecast time.
- SCADA analysis is the
diagnosestep: binned power curves, the power coefficient against the Betz limit, and the industry's own long-term assessment methods. - LLMs are the layer that ties them together: an agent that retrieves the real fault history, reasons over the numbers and writes the field report I used to write by hand.
This is the write-up of Windward, and specifically of one moment about halfway through building it, when the agent flagged something that turned out to be real. The live dashboard is linked at the bottom of this post.
The setup: real turbines, not synthetic curves
Windward forecasts wind farm production and then reasons about how efficiently each turbine is actually extracting energy from the wind that's available to it. Three real, openly licensed SCADA datasets sit underneath everything — two from Cubico Sustainable Investments (Kelmarsh, Penmanshiel) and one from RES, on behalf of TRIG (Hill of Towie), each published via Zenodo under CC BY 4.0, and each in a different historian export format the pipeline has to normalize:
The forecasting target is real production; the forecasting input is real historical weather from Open-Meteo's ERA5 archive — deliberately not the turbines' own anemometers, because at forecast time an anemometer reading from the future doesn't exist. That distinction matters more than it sounds: it's the difference between a model that works in a demo and one that would actually work operationally.
Why gradient boosting, not a neural net
The forecasting model is a plain scikit-learn GradientBoostingRegressor (200 estimators, depth 4, learning rate 0.05) trained on seven physically-grounded features — wind speed, its cube (power scales with v³), direction, temperature, pressure, price, hour-of-day. For a tabular problem this size, gradient-boosted trees are the right default: they capture the non-linear power curve and interaction effects without a GPU, and they train on a laptop in seconds.
On held-out real data it lands at R² 0.73–0.81 across all three farms (Kelmarsh 0.73, Penmanshiel 0.81, Hill of Towie 0.74). That's a meaningfully harder number than an early synthetic-data prototype's R² 0.95 — real SCADA carries curtailment, wake losses and downtime the model has to learn around, which is the entire point of using real data instead of a clean synthetic curve.
Where the agent comes in
The forecast alone is just a number. The interesting part is what happens next: a LangGraph agent takes that forecast, compares it against what actually happened, computes real physics on top of it, retrieves grounding context from a real fault-event log, and writes a plain-English field report.
flowchart LR
ingest["ingest\nreal weather + production"] --> forecast["forecast\nregistered MLflow model"]
forecast --> diagnose["diagnose\npower curves, Cp, anomalies"]
diagnose --> rag["rag\nFAISS retrieval"]
diagnose --> multimodal["multimodal\nvision-LLM blade check"]
rag --> recommend["recommend\nworst-turbine rule"]
multimodal --> recommend
recommend --> explain["explain\nAmazon Nova Lite"]
The diagnose node is where the real engineering happens. For every turbine it bins the real SCADA into an IEC 61400-12-1–style power curve (0.5 m/s buckets) and computes the power coefficient Cp — actual power output divided by the kinetic power actually available in the wind hitting the rotor. Cp has a hard physical ceiling: the Betz limit, 16/27 ≈ 0.593, derived by Albert Betz in 1919. No turbine, no matter how well engineered, can cross it.
The agent didn't just print the number and move on. It flagged it as an anomaly, reasoned about the likely cause (nacelle anemometer bias — the sensor sits downstream of the spinning rotor, which is a known source of wind-speed measurement error), and said so explicitly in its field report rather than reporting a fabricated "the turbine is extracting more energy than physically possible" as if that were a real finding. That's the difference between a dashboard that shows you a number and an agent that understands what the number means.
Grounding it in something real: RAG over an actual fault log
The SCADA archives don't just contain production numbers — they contain the turbines' real status/fault event logs: timestamped Stop and Warning codes with human-readable messages ("Low gearbox oil pressure," "Timeout brake closed"). Rather than inventing a maintenance-manual corpus, I built the RAG stack over this real data: the ~60 longest-duration real fault events per farm, embedded with Amazon Titan Text Embeddings v2, indexed in FAISS, retrieved through LangChain.
Ask the agent "what causes a Cp reading above the Betz limit?" and it retrieves the actual methodology note and answers correctly, citing the source. Ask it which turbine has had the most maintenance interventions, and it can point at real event counts, not a guess.
A second, independent RAG stack — LlamaIndex, a different framework, over a different corpus shape (structured turbine spec metadata rather than incident narratives) — sits alongside it, deliberately, to exercise both toolchains rather than picking one and stopping.
The stack, briefly
What it costs to run
This is the part most portfolio projects skip, and it's the part I actually care about getting right: the whole thing — three farms, per-farm models, a RAG index each, a persistent live API — runs for under $0.10 a month, because (as the next two sections cover) it ended up hosted on infrastructure that was already running rather than anything new.
What grew since the first draft of this post
A few more pieces went in after this was first written, each closing a gap between "the roadmap says it's real" and it actually being real:
- A third farm. Hill of Towie (Aberdeenshire, Scotland) — 21 turbines, 48.3 MW, RES's own historian export via Zenodo (CC BY 4.0), a completely different file layout from the Kelmarsh/Penmanshiel Greenbyte export. Trained and registered the same way as the other two: MAE 4.29 MW, R² 0.74, live on its own dashboard tab with the full agent pipeline — diagnosis, RAG, multimodal, recommendation, explanation — running against it exactly as it does for the other two farms.
- A second RAG stack. LlamaIndex, deliberately a different framework over a different corpus — structured turbine spec metadata rather than incident narratives. Asked it Kelmarsh 3's hub height: 68.5m, matching the source data exactly.
- Real multimodal. Sourced one openly-licensed inspection photo (Wikimedia Commons, CC BY-SA 3.0) and ran it through Amazon Nova Lite's vision input via the Bedrock Converse API. It correctly declined to invent damage on a photo that isn't a damage close-up, rather than hallucinating a finding to seem useful.
- Langfuse tracing, wired into every agent run — a real no-op today pending a free Langfuse signup, but the integration code is live, not a stub.
- DynamoDB, actually written to now: every dashboard export persists a real session record (farm, timestamp, capacity factor, recommendation) — verified with a live table scan.
- A chat panel on the dashboard, grounded in the live data. It started as a Claude Artifact using Claude's on-page assistant capability, because a published Artifact's sandbox can't call an external API at all — that was the only way to get something interactive working inside it. But that capability bills the viewer's own Claude usage, not mine, which is fine for a portfolio piece and wrong for a service. So once the plan changed to "keep this running and eventually sell it," the dashboard itself moved off the Artifact entirely: it's now a real static frontend served by the same FastAPI app, with an
/analysis/{farm_id}/askendpoint that calls Bedrock directly — my infrastructure, my cost (fractions of a cent per question on Nova Lite), answerable by anyone who opens the page.
A fourth example, and a dead link on someone else's site
The next real dataset I went looking for was EDP's own "Wind Farm 1" open data — specifically because it has something the three UK farms above don't: a real on-site meteorological mast alongside the turbine SCADA and fault logbook, all for the same site. I found it. I also found that EDP's own download buttons for it are currently broken on their live site — "Download data" and "Download variables" both route to an internal admin path that returns Access Denied, confirmed with a real browser, not just a dead curl. Not a dataset problem; a site bug on infrastructure I don't control.
The same underlying farm turned out to be re-published, properly licensed and citable, inside a newer academic benchmark: CARE-to-Compare (Zenodo, CC BY-SA 4.0, published alongside a 2024 paper on wind turbine fault detection). Its "Wind Farm A" is EDP's Portugal site — but anonymized: no coordinates, no rated power or rotor diameter, no real calendar timestamps, and power readings rescaled to a normalized fraction rather than real kW, specifically so the source farm can't be identified. That rules it out for the forecast pipeline above, which needs real coordinates to join real weather against. What it does have: 22 independent, real, labeled case studies — 11 with an actual root-cause description (gearbox failure, generator bearing failure, transformer failure, hydraulic group) — genuinely better ground-truth anomaly labels than any of the other three farms carry.
So it became its own thing rather than a fourth entry forced into the same farm abstraction: its own loader, its own RAG corpus and FAISS index, its own /edp/* API routes, its own dashboard tab — real power curves and status-code breakdowns per case study, and a RAG-grounded "ask the agent" box over the real fault descriptions. No forecast, no LangGraph run, no MLflow model, because there's no real location to forecast against. Live on its own tab at the dashboard linked below.
Separately, since I'd been pointed at Yu Ding's Data Science for Wind Energy, I tried applying one of its published methods — a Gaussian-process-based statistical test (dswe.ComparePCurve) for whether two turbines' power curves differ significantly — to put a real p-value behind the anemometer-bias displacement check above, instead of just reporting a raw shift. Installed the real package, ran it against real Kelmarsh data, and hit two reproducible bugs in the published PyPI release against current numpy/scipy. Documented in the repo, not shipped: a broken dependency patched over quietly is worse than an honest "evaluated, not pursued."
Then the project's purpose changed, so I deployed it for real
The original plan was to keep running this the same lightweight, low-commitment way it ran during development — fine for a portfolio piece. That logic stopped applying the moment this stopped being a skill demo and started being something I intend to keep running and eventually combine with an energy-price-prediction model and commercialize. A commercial service doesn't get to run on infrastructure sized for a demo — it needs a real home, on infrastructure I actually operate day to day.
So I stood up the real thing: self-hosted MLflow (a small systemd service, SQLite backend, S3-backed artifact store), and the FastAPI + agent service deployed as a real, persistent Docker container — both running on forwardforecasting-dev, an EC2 instance I already had, which previously auto-shut-down after 30 minutes idle (a cost-saving habit from when it was a personal dev sandbox). That automation is now disabled for this project specifically, because a service that stops itself isn't a service.
curl https://windward.forwardforecasting.eu/forecast -X POST -d '{"farm_id":"kelmarsh","horizon_hours":6}' — real HTTPS, real Let's Encrypt cert, real model inference, publicly reachable, running continuously for under $0.10/month.
One genuine bug along the way, the kind you only find by actually deploying: the container couldn't reach the MLflow server on 127.0.0.1:5000, even though that's exactly where it was listening — because inside a Docker container, 127.0.0.1 is the container's own loopback, not the host's. The fix was network_mode: host, which for a single-container deployment on infrastructure I already control was simpler than the alternative (a Docker bridge network plus an explicit gateway route).
Measuring the farm the way the industry does: OpenOA
Everything above reads the farms from the turbines' point of view. The questions an owner or a lender actually asks are different: how much energy will this site make in a normal year, and how sure are you? How much is lost in the cables? How much to wakes? The National Laboratory of the Rockies (formerly NREL) publishes an open-source library for exactly that, OpenOA, along with the papers behind its methods. I ran four of its analyses on all three farms, over the full 2016-2021 data for the two Greenbyte farms rather than the single year the forecasting uses.
Three findings were worth the effort.
The obvious meter was the wrong one. Each Greenbyte dataset ships two candidate grid meters. One of them recorded slightly more energy at the grid connection than the turbines produced, which no real meter can do. The substation unit next to it showed a plausible ~1% loss, so that is the one the analysis uses.
The papers' claims mostly held up on UK data. A 2021 study found that fitting the yield regression on daily data with temperature, instead of the industry's monthly linear fit, cuts its uncertainty. On these farms it did: Kelmarsh went from ±1.17% to ±0.95%, Penmanshiel from ±1.51% to ±1.10%. A 2020 paper showed that adding uncertainty components as a root-sum-of-squares, the usual shortcut, assumes they are independent when they are not. Here the shortcut understated Kelmarsh's total and slightly overstated Penmanshiel's: not reliably in either direction, which is the real argument for running the full Monte Carlo.
The agent's top recommendation needed a caveat. The agent had been sending a crew to Kelmarsh 6: lowest capacity factor in the fleet, under its neighbors 15% of the time. OpenOA puts its long-term wake loss at 21.7%, against 7.8% for the farm. A good part of its shortfall is where it stands, not what is wrong with it, and the recommendation now says so and points at the turbine's outage history first.
Not everything survived contact with real data. OpenOA's yaw-misalignment method, which its own authors mark as unvalidated, gave every Kelmarsh turbine the same suspicious offset that swung by up to 34° between wind speeds, so those results are reported as inconclusive. On Hill of Towie it could not run at all: that export has no wind-vane channel, and the substitute signal barely moves. Three Penmanshiel turbines did produce consistent, tight estimates (+3.7° to +4.7°) and are flagged for a vane check. OpenOA itself runs offline in its own environment, because it pins older versions of the libraries the live models depend on. The service only reads its results.
Asking about one turbine
The "ask the agent" box used to answer for the farm as a whole. It now takes questions about single turbines too: which turbine performs best? when was turbine 3 last maintained? which one loses most to wakes? It answers from the real status logs and the OpenOA results. Kelmarsh 3's last maintenance stop was 9 November 2016, 12:15 to 13:18, a manual on-site stop. The same tools are exposed on the MCP server, so an external agent can ask them too.
Testing it turned up one more real-data trap. A single stoppage is often logged as several alarms at once (Kelmarsh 1's three generator fans each raised one), so simply adding up alarm durations put Kelmarsh 1 at 1,156 hours of forced outage in 2016. Counted as overlapping time, the real figure is 507.7.
Try it
The live dashboard at windward.forwardforecasting.eu lets you switch between farms, see the actual-vs-forecast production, the power curves against the Betz limit, the agent's field report for each, a real vision-LLM blade inspection, and a live "ask the agent" box grounded in that farm's own data — served straight off the EC2 instance, no sandboxed viewer-billed capability involved. A fourth tab, EDP Wind Farm A, swaps the forecast for 22 real labeled fault case studies. (The Spain day-ahead price model that used to live on this page moved out into its own project, energy-trader, once it grew a battery optimizer to go with it.) Or hit the API directly: curl https://windward.forwardforecasting.eu/forecast -X POST -d '{"farm_id":"kelmarsh","horizon_hours":6}'.
See it live
Real data, real models, real anomaly, real deployment — not a slide deck.
Open windward.forwardforecasting.eu →About the cover image: the turbines are linked by thin gold data-lines rather than shown in isolation — a visual stand-in for what the agent actually does: read every turbine's SCADA signal together, not one at a time, which is how it caught Kelmarsh 5's impossible Cp reading in the first place. ImageModelstability.stable-image-core-v1:1 · AWS Bedrock (us-west-2)Prompt“Editorial illustration of a row of wind turbines on rolling hills at dusk, deep navy sky, thin glowing gold data-connection lines linking each turbine like a neural network overlay, minimalist flat vector illustration style, clean linework, no text, no logos, atmospheric and technical mood”