
Nine Turbines Beat Ninety-Nine
Early on, our model told us that a single wind turbine would run at 73% of its maximum output, every hour of the year. No turbine on Earth does that. Good ones manage about 40%. We had built a tool to help state governments upgrade old wind farms, and on day one it was confidently wrong. This is the story of how we found out why, and what else turned out to be wrong along the way.
Part one: the problem is not wind
India has some of the best wind sites in the world. Muppandal, in Tamil Nadu, has winds of 8 to 12 metres a second. It also has hundreds of turbines built decades ago, small and low-powered, that make a fraction of what a modern one would on the same land.
The government has a policy for this. It is called repowering: take the old turbines down and put new, stronger ones up. The 2023 national policy even sets a test. The new project has to generate at least 1.5 times what the old one did.
The hard part is not the wind. It is the decision. An officer at a State Nodal Agency has to answer three questions with almost no tooling:
- Which sites should we do first?
- How do we place new turbines so they don't block each other's wind?
- Does this plan actually pass the policy?
The alternatives are not easy either. Offshore wind has the data, but the cost slows it down. New power lines are stuck behind land acquisition and clearances. The fastest new wind is the land and the grid that already exist.
So we built PawanSthal, for one cluster first: Muppandal–Aralvaimozhi in Kanyakumari.
Part two: you cannot repower what you cannot count
Our first problem was boring. We did not know which turbines were old.
OpenStreetMap has 2,581 turbines around Muppandal. Almost none of them say how big they are. There was no registry we could download.
But old turbines are small, and small turbines were built close together. Modern ones are huge and need space. So the gap to the nearest neighbour tells you the generation:
Python
# nearest neighbour distance, in metresif nn < 150: cls = "old"elif nn < 300: cls = "mid"else: cls = "modern"
It is crude. It is also the whole classifier. A registry upload can override any turbine, so if a state has real data, the tool takes it.
Part three: the wind that read too high
Now back to the 73%.
We used the Global Wind Atlas for wind speed and ten years of hourly NASA weather data for direction. Together with the PyWake wind simulator, they gave energy numbers for old and new turbines.
The old turbines looked fine. The new ones looked like science fiction. The Global Wind Atlas reads high in the narrow gap near Aralvaimozhi, and our model took it at its word.
We needed a reality check we could defend. The policy itself gives one: an old turbine runs at a 12.3% capacity factor in its worked example. So we scaled the wind speed by 0.78, until:
Turbine | Modelled capacity factor | Reality check |
|---|---|---|
Old, sub-MW | about 12.4% | policy example: 12.3% |
Existing modern | about 39% | typical good wind sites |
New layouts still model at 38–48%, because this really is India's best site. We say so on the page: treat the absolute numbers as indicative. The ratio between before and after is the number to trust.
Part four: turbines steal wind from each other
This is the part that makes repowering an optimisation problem and not a shopping list.
A turbine standing behind another gets less wind. Engineers call it a wake. Stand too close and you lose energy. Space everything far apart and you can't fit enough turbines. And you can't build near houses, roads or water.
So we let a computer search. A genetic algorithm tries thousands of layouts on the usable land only, scores each one with PyWake, keeps the best, mixes them, and repeats. In the top-scoring 3 km block:
| Today | After |
|---|---|---|
Turbines | 99 old and mid-age | 9 modern |
Capacity | 62 MW | 27 MW |
Net energy | 49 GWh a year | 108 GWh a year (2.2×) |
Nine turbines beat ninety-nine. Fewer machines, more than double the energy, and it passes the 1.5× test. The same page shows CO₂ avoided, homes powered, payback, and the policy's own compensation formula for the owners of the old turbines.
Part five: the scores that were too good
We inherited a classifier and a wind model from a teammate's earlier work. Their numbers looked great.
- The turbine classifier reported 99.7% accuracy.
- The wind model reported an R² of 0.97.
We got suspicious precisely because they were great. The classifier had no true labels. It had learned our own spacing rule back, which is why it scored so well. The honest name for its score is agreement, and that is what the page says now.
The wind model had been tested on a random split, with the wrong target. Neighbouring pixels leaked from training into testing. When we retrained it on the right target and held out whole 3 km blocks, R² fell to 0.55.
That is a modest model. We show it anyway, with the plain-language note that the energy model uses the Global Wind Atlas directly. A 0.55 we can defend is worth more than a 0.97 we can't.
Part six: the 30-second wall
We wanted officers to change an assumption, such as turbine size, height or distance from houses, and see a new layout immediately.
The full genetic algorithm takes about ten minutes. API Gateway cuts every request off at 30 seconds. No setting changes that.
So the design bent around the limit:
- The live version runs a smaller search with a 12-second budget.
- A small neural network (a surrogate) learned to predict energy from layout features. It scores a layout in 0.3 milliseconds, against 31 for the full simulation, with an average error of about 1.7%.
- The surrogate guides the search. The winning layout is always re-scored by PyWake, so the number you see is the real one.
A live recompute now takes about 16 seconds.
Part seven: the deploy that fought back
We expected deploying to be the easy part. It was a short series of walls.
The account. Our AWS account sat under an organisation policy that blocked most services in most regions. Probing showed one region, Sydney, where everything we needed was open. We moved there.
No Docker. The backend is a container on Lambda, and we had no Docker locally. So the image is built in the cloud: the script zips the source, CodeBuild builds it, and ECR stores it.
The image would not build. The first build tried to compile numpy from source. The Python 3.11 Lambda image is on Amazon Linux 2, whose C library is too old for current prebuilt wheels. Switching to the Python 3.12 image fixed it.
The image built but would not run. The first live request failed with a missing
libexpat.so.1 , needed by rasterio. Now the image build ends with an import test, so a missing library breaks the build and not a user:dockerfile
RUN python -c "import rasterio, geopandas, scipy, sklearn, py_wake, pandas"
The first request timed out. Importing the heavy libraries took longer than 30 seconds on a cold start. We moved the imports to Lambda's init phase and added an EventBridge rule that pings the function every five minutes.
At the end we ran the backend locally and on Lambda side by side and compared nine endpoints. The responses were byte-for-byte identical, including live recompute. The cloud version is only slower, 16 seconds against 11 to 14.
What it runs on, and what it costs
[Image]
- AWS Amplify Hosting serves the React and Leaflet web app, pushed by a deploy script.
- AWS Lambda (container image, 3 GB) runs FastAPI, PyWake, the optimiser and the spot checker.
- Amazon API Gateway is the public API, throttled to 5 requests a second.
- Amazon ECR and AWS CodeBuild build and store the image.
- Amazon S3 holds the zipped source for the build.
- AWS CloudFormation defines the whole backend as code.
- Amazon EventBridge keeps one container warm.
- Amazon CloudWatch Logs and AWS IAM handle logging and permissions.
Nothing runs when nobody is using it. A live recompute uses about 48 GB-seconds of Lambda compute, which at list price should be well under a cent. We haven't looked at a real bill yet, so check that number before trusting ours.
The app also works with the API switched off. Every page falls back to the results we committed, so a demo can't die because of a cold start.
This is also why AWS fits the next step. Muppandal is one cluster, and India has many. Lambda scales with demand, and we pay only for use. Planned next, and not built yet: Step Functions with S3 to run every cluster in parallel, Bedrock with Strands Agents for plain-English policy questions, and CloudFront to serve every state.
Three things I would tell myself on day one
1. A great score is a question, not an answer. Our 99.7% and 0.97 both turned out to be the model grading itself. Ask what could be leaking before you celebrate.
2. Name the hard limit before you design. The 30-second wall shaped our whole live design. We would have saved days by finding it on day one.
3. Make the demo survive the cloud. A committed fallback means a cold start or a quota can't take the project down in front of a judge.
PawanSthal is live at https://pawansthal.d30iqzvpnzrv47.amplifyapp.com . The code is open at https://github.com/harshit2008-netizen/AWS_PawanSathal . Data: Global Wind Atlas (DTU / World Bank), NASA POWER, OpenStreetMap contributors, PyWake (DTU Wind Energy), and the MNRE National Repowering & Life Extension Policy (2023). All results are modelled estimates for one cluster. The views here are the authors' own.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article