Skip to content

Repository files navigation

SunMinute

A solar energy simulator that shows a full day of production in one minute.

Live: sunminute.app

Enter how many solar panels you are considering. The application computes a full day of generation from real solar physics, then plays it back as a 60-second 3D timelapse — the sun following its true path for your city, panels tracking or fixed, shadows moving across the roof — with a live power gauge, and finishes with daily energy, yearly savings in local currency, CO₂ avoided and payback.

Every input is optional. One number is enough to get a complete result.


Why this exists

Solar calculators are abundant and they all produce the same artefact: a form that returns a table. That works for engineers. It does not work for the person actually deciding whether to spend a month's salary on a rooftop system, who does not think in kilowatt-hours and has no way to judge whether the numbers in front of them are honest.

The gap is not computational. It is a comprehension gap. SunMinute closes it by making generation something you watch rather than something you read, while keeping the numbers underneath rigorous enough to withstand an engineer's scrutiny.


Architecture

Browser (app.html)                    Server (FastAPI)
┌────────────────────────┐            ┌───────────────────────────┐
│ Input form             │  POST      │ /simulate                 │
│ (all fields optional)  │ ─────────► │                           │
│                        │            │  ├─ geocode city  ────────┼──► Open-Meteo
│ 60s timeline driver    │            │  ├─ live weather  ────────┼──► Open-Meteo
│  ├─ Three.js scene     │  JSON      │  ├─ terrain relief ───────┼──► Open-Meteo
│  ├─ power gauge        │ ◄───────── │  ├─ tariff lookup         │
│  ├─ kWh accumulator    │            │  └─ solar_engine ─────────┼──► pvlib
│  └─ clock              │            │       (physics)           │
│                        │            │                           │
│ Results + PDF          │            │ /s/<slug>  white-label    │
└────────────────────────┘            └───────────────────────────┘

One timeline, one source of truth. The sun's position, the panel angle, the gauge needle, the accumulating energy counter and the on-screen clock are all driven by the same array of 5-minute samples returned by the physics engine. Nothing in the animation is decorative or approximated separately, so the picture and the numbers cannot drift apart.

The engine knows nothing about the web. solar_engine.py is a pure computation module with a dataclass in and a dictionary out. It runs standalone from the command line, which is how it was validated before any UI existed. The API layer adds geocoding, weather and white-labelling around it without reaching inside.

File Responsibility
solar_engine.py Physics. Sun position, irradiance, cell temperature, losses, tracking, savings, projections.
solar_api.py HTTP layer. Geocoding, live weather, terrain detection, white-label routing.
tariffs.py Electricity price and currency table, ~50 countries.
app.html Entire frontend: form, gauge, 3D scene, results, PDF. No build step.
installers.html Landing page for the commercial white-label offering.
partners.json White-label configuration. Adding a partner requires no code change.

The physics

Generation is computed with pvlib, the reference Python library maintained against NREL's models, rather than a hand-rolled approximation:

  • Solar position and clear-sky irradiance (Ineichen model)
  • Plane-of-array irradiance for the panel's tilt and azimuth
  • Single-axis tracker geometry when tracking is selected
  • Faiman cell temperature model — panels lose roughly 0.35% of output per °C above 25°C, which dominates real performance in hot climates and is the most common omission in naive calculators
  • Standard loss stack: soiling, wiring, mismatch, inverter efficiency
  • Optional inverter clipping, which flattens the midday peak on over-panelled systems

The resulting performance ratio is 81.8%, measured rather than assumed, against an industry-standard band of 75–85%. Annual yield for Multan, Pakistan comes out at 1,745 kWh per kWp, inside the known regional range of 1,600–1,800. Results were cross-checked against NREL PVWatts during development.

Auxiliary data (geocoding, live weather, terrain elevation) comes from Open-Meteo, which requires no API key. Every external call has a fallback: if the network fails, the tool degrades to built-in tables and a clear-sky day rather than failing.


Two bugs worth describing

Both were found by controlled testing rather than by looking at the screen, and both are the kind that produce plausible wrong answers rather than obvious breakage.

Today's weather was contaminating the annual projection

Running the demo on a cloudy afternoon reduced the customer's yearly savings estimate, because the monthly projection loop reused the live cloud factor for all twelve months. A second instance of the same fault applied today's maximum temperature to every month, so an August demo made January's panels run hot.

Isolating it required a controlled experiment: one configuration, one variable changed.

SAME system, SAME city, SAME date. Only the weather mode changes:
  'Sunny day'        -> 10,019 kWh/year
  "Today's weather"  ->  8,522 kWh/year   (18% understated)

The fix zeroes both live values for the duration of the projection loop and restores them afterwards, so today's conditions affect today's figures only. Verified by re-running until both modes returned an identical annual figure while the daily figures still differed correctly.

The general lesson: live measurements and long-range projections must not share state. A demo that understates savings on the exact day a customer is least convinced is worse than one that fails loudly.

Solar panels rendering invisibly

Panels vanished from the 3D scene while their mounting hardware rendered normally. No console error, and the scene logic tested clean in a headless harness — instance matrices were finite, objects were present in the scene graph, and the tracker angles swept correctly from 60° east through flat at noon to 60° west.

The panels were the only objects built with THREE.InstancedMesh. Instanced rendering was failing silently on the target GPU. Replacing instancing with ordinary meshes sharing geometry and materials fixed it with no meaningful performance cost at the scene sizes involved.

The general lesson: when one class of object fails and everything else works, the difference between them is the bug — not the logic they have in common. A user screenshot resolved this faster than any amount of code reading.


Frontend notes

No build step. The entire frontend is one HTML file with inline CSS and JavaScript, and Three.js loaded from a CDN with a local fallback. No bundler, no package manager, no node_modules. It can be opened directly from disk. For a tool whose users are on low-cost phones in markets with expensive data, the absence of a toolchain is a feature.

Device-aware rendering. The application measures CPU cores, available memory and screen size, then selects full 3D, a reduced "lite" mode (no shadows, lower pixel ratio, fewer scene objects), or a 2D fallback that keeps the gauge and all numbers. Users can override the choice. Measured effect of lite mode: 81 meshes down to 57, pixel ratio 2 down to 1, shadow mapping off.

Scenery from data, not guesswork. The backdrop is chosen by sampling terrain elevation at nine points around the location and recent precipitation totals. Ground colour and vegetation follow dryness; hill and mountain silhouettes follow relief. These are deliberately independent — an early version keyed both to elevation and rendered Riyadh as green hills with pine forests. An installer in Multan showing a customer a landscape with mountains has lost credibility before the numbers appear.

Shareable state. Every run encodes its configuration into the URL, so a result can be sent over WhatsApp and reopened identically by the recipient.


The commercial layer

The product is free for the public and monetised through white-label licensing to solar installers. Each partner receives a branded link at /s/<slug> that applies their company name, logo, accent colour, contact details, and their market's tariff and currency — with a WhatsApp button on the results screen that opens a message pre-filled with the customer's system size and expected output.

Onboarding a partner is a JSON edit. The file is watched for changes and reloaded without a restart, and setting "active": false suspends a partner immediately.

In this public repository the partner entries are fictional. The production deployment supplies real contact details through server environment variables, which are not stored in version control.


Running it

pip install -r requirements.txt
python -m uvicorn solar_api:app --reload
Route
/ The application
/for-installers Commercial landing page
/s/demo White-labelled example
/?debug=1 Diagnostic overlay during simulation
/docs Interactive API documentation

The physics engine also runs on its own, which is the fastest way to inspect it:

python solar_engine.py --panels 10 --city multan --tracking
python solar_engine.py --panels 10 --city multan --cloud 0.4 --json

Deployment is a single container:

docker build -t sunminute .
docker run -d -p 80:8000 sunminute

The live site runs this image behind Cloudflare.


Stack

Python · FastAPI · pvlib · pandas · NumPy · Three.js · Docker · Open-Meteo · Cloudflare


Design decisions and their trade-offs

Stylised 3D over satellite imagery. Photorealistic tiles were evaluated and rejected: per-view licensing costs that scale with traffic, restrictions on compositing custom geometry into the scene, and imagery that is low-resolution and years out of date across most of the target markets. A clean stylised scene reads as deliberate; a blurry satellite roof reads as cheap.

Estimates, stated as estimates. Every result page and PDF carries a plain disclaimer. Customers make purchasing decisions on these numbers, which is exactly why the engine is a validated library rather than a spreadsheet formula, and why the disclaimer stays.

Optional inputs with real defaults. Literacy and technical familiarity vary widely across the target markets. Tilt defaults to latitude, azimuth to the equator, tariff to a country table, and the location's own weather is fetched automatically — so a single number produces a complete, defensible answer, while an engineer can still specify all twelve parameters.


Roadmap

The engine's interface — a configuration in, a result dictionary out — is designed to accommodate additional physical models under the same one-minute presentation. Battery storage, bill-based sizing (entering a monthly electricity bill rather than a panel count, which matches how customers in South Asia actually think about it), country-specific net metering rules, and a Capacitor wrapper for Android are the current candidates.


Built by Irfan Jamal.