Skip to content

subatomic-hamster/radiance

Repository files navigation

Radiance — Solar Intelligence Platform

2026 HenHacks Submission

An AI-powered solar analysis platform that evaluates any rooftop's solar potential, models 25-year financial returns, and surfaces equity insights across entire cities. The financial calculation engine is implemented in Pharo Smalltalk and called as a live microservice from the Python/FastAPI backend.


Table of Contents


What It Does

  1. Rooftop analysis — draw a polygon on the map or drop a pin; the platform fetches satellite imagery, runs a SegFormer-B0 computer-vision model to detect the usable roof area, and calculates how much solar energy that surface can generate per year.
  2. Financial modelling — given the roof's energy output, a 25-year model projects installation cost, electricity savings, payback period, net profit, and ROI. This step runs in Pharo Smalltalk.
  3. Equity mapping — a Deck.gl heatmap overlays census-derived equity scores so city planners can see which neighbourhoods would benefit most from solar incentives.
  4. City-scale scanning — a Celery job fans out across all addresses in a bounding box and ranks every building by solar ROI and equity score.

Architecture Overview

Browser (React + Vite)
        │  REST / WebSocket
        ▼
FastAPI  :8000
        │
        ├─── solar_pipeline.py   (11-step analysis orchestrator)
        │         │
        │         │  Step 6: financial model
        │         ▼
        │    pharo_bridge.py  ──── HTTP POST ────▶  Pharo :8001
        │         │                                  SolarFinancialService
        │         │  (if Pharo unreachable)           ZnServer + STONJSON
        │         ▼
        │    financial_model.py  (Python fallback, identical maths)
        │
        ├─── NASA POWER API  (irradiance data)
        ├─── Google Maps API (satellite imagery, geocoding)
        ├─── SegFormer-B0    (roof segmentation, Modal endpoint)
        └─── Gemini / Claude (LLM insights)

The Pharo Financial Microservice

What It Accomplishes

The 25-year solar financial model — the core output of every analysis — is implemented as a standalone Pharo Smalltalk 13 HTTP microservice. It receives a JSON payload describing a solar installation, runs the full financial computation, and returns a JSON response that the Python pipeline integrates directly into the analysis result shown to the user.

The computed outputs are:

Field Description
system_cost_net Net installation cost after the 30 % federal ITC (USD)
first_year_savings_gross Year-1 electricity bill savings (USD)
first_year_savings_net Year-1 savings minus operational costs (USD)
payback_period_years Years until cumulative savings recover the net cost
total_net_savings_25_years 25-year total savings minus all operational costs (USD)
net_profit_25_years 25-year total savings minus cost of the system (USD)
roi_25_years 25-year return on investment (%)

A second endpoint (/financial/breakdown) returns the full year-by-year table — 25 rows each containing generation (kWh), gross savings, maintenance cost, dust-cleaning cost, and net savings — accounting for panel degradation (0.5 %/yr) and electricity price inflation (2.5 %/yr).

How It Works

The service is two Smalltalk classes loaded into a headless Pharo 13 image at startup.

SolarFinancialModel (pharo/src/SolarFinancialModel.st)

A pure computation class with no I/O. All arithmetic mirrors api/services/financial_model.py exactly so that cross-language parity tests can assert numeric equality within a $1 rounding tolerance.

model := SolarFinancialModel new
    systemSizeKw: 6.0;
    annualGenerationKwh: 8400;
    electricityRate: 0.15;
    installCostPerWatt: 2.80;
    annualMaintenanceRate: 0.012;
    dustCleaningsPerYear: 2;
    dustCleaningCostPerKw: 14.0;
    yourself.

result    := model calculate.       "→ Dictionary with 7 summary fields"
breakdown := model yearlyBreakdown. "→ OrderedCollection of 25 Dictionaries"

SolarFinancialService (pharo/src/SolarFinancialService.st)

An HTTP wrapper around SolarFinancialModel. It uses only built-in Pharo 13 classes — no external packages are loaded:

  • ZnServer — Zinc HTTP server (ships with the Pharo 13 image)
  • ZnDispatcherDelegate — URL-to-handler routing
  • STONJSON — JSON serialisation/deserialisation (ships with the Pharo 13 image)
"Route table — /financial/breakdown is mapped before /financial because
 the dispatcher does exact-path matching on the string key."
server delegate: (ZnDispatcherDelegate new
    map: '/health'              to: [ :req :resp | self handleHealth: req response: resp ];
    map: '/financial/breakdown' to: [ :req :resp | self handleBreakdown: req response: resp ];
    map: '/financial'           to: [ :req :resp | self handleFinancial: req response: resp ];
    yourself).

Each handler parses the request body with STONJSON fromString:, constructs a SolarFinancialModel, runs the calculation, serialises the result with STONJSON toString:, and writes the JSON into the ZnResponse object that ZnDispatcherDelegate provides to each block.

Startup script (pharo/scripts/load_and_serve.st)

The script that PharoConsole.exe executes at launch. It uses CodeImporter evaluateFileNamed: to file-in the two source files, then calls SolarFinancialService startDefault, which reads the PHARO_SERVICE_PORT environment variable (default 8001) and starts the server. The image then runs as a live HTTP server for as long as the process is alive.

CodeImporter evaluateFileNamed: (FileSystem workingDirectory / 'pharo' / 'src' / 'SolarFinancialModel.st') fullName.
CodeImporter evaluateFileNamed: (FileSystem workingDirectory / 'pharo' / 'src' / 'SolarFinancialService.st') fullName.
(Smalltalk at: #SolarFinancialService) startDefault.

The Bridge Pattern

api/services/pharo_bridge.py is the async Python client that connects FastAPI to Pharo. It is a thin httpx.AsyncClient wrapper with no blocking calls.

FastAPI request
      │
      ▼
_calculate_financial_with_fallback()    ← solar_pipeline.py, Step 6
      │
      ▼
pharo_bridge.calculate_financial_outlook_pharo()
      │
      │  POST http://localhost:8001/financial
      │  { "system_size_kw": 6.0, "annual_generation_kwh": 8400, ... }
      │
      ▼
Pharo SolarFinancialService
      │
      │  200 OK  { "system_cost_net": 11760, "payback_period_years": 9, ... }
      │
      ▼
dict returned → pipeline continues with Pharo data

Configuration is entirely via environment variables — no hardcoded addresses:

Variable Default Purpose
PHARO_SERVICE_HOST localhost Host where Pharo is running
PHARO_SERVICE_PORT 8001 Port Pharo listens on
PHARO_SERVICE_TIMEOUT 5.0 Request timeout in seconds

Pharo-Preferred, Python-Fallback

The bridge never raises an exception. If Pharo is unreachable (process not started, wrong port, timeout), pharo_bridge.py returns None and solar_pipeline.py transparently falls back to financial_model.py — the identical maths implemented in Python:

async def _calculate_financial_with_fallback(...) -> dict:
    result = await pharo_bridge.calculate_financial_outlook_pharo(...)
    if result is not None:
        return result                        # ← Pharo path (preferred)
    return calculate_financial_outlook(...)  # ← Python fallback (silent)

This means:

  • The application works correctly in CI and environments without Pharo installed.
  • Starting Pharo is opt-in; omitting it degrades gracefully with no user-visible difference.
  • Production deployments can run Pharo alongside the API container to exercise the genuine Smalltalk path.

API Endpoints

All three endpoints are served directly by the Pharo process.

GET /health

{ "status": "ok", "version": "1.0" }

POST /financial

Request body:

{
  "system_size_kw": 6.0,
  "annual_generation_kwh": 8400,
  "electricity_rate": 0.15,
  "install_cost_per_watt": 2.80,
  "annual_maintenance_rate": 0.012,
  "dust_cleanings_per_year": 2,
  "dust_cleaning_cost_per_kw": 14.0,
  "include_federal_itc": true
}

Response:

{
  "system_cost_net": 11760,
  "first_year_savings_gross": 1260,
  "first_year_savings_net": 890,
  "payback_period_years": 9,
  "total_net_savings_25_years": 31056,
  "net_profit_25_years": 19296,
  "roi_25_years": 164.1
}

POST /financial/breakdown

Same request body as /financial. Returns a 25-element array, one entry per year:

[
  {
    "year": 1,
    "generation_kwh": 8400,
    "electricity_rate": 0.15,
    "gross_savings": 1260,
    "maintenance_cost": 202,
    "dust_mitigation_cost": 168,
    "net_savings": 890
  },
  { "year": 2, ... },
  ...
]

Running the Full Stack

Prerequisites

  • Python 3.11+pip install -r requirements.txt
  • Node.js 18+cd frontend && npm install
  • Pharo 13 VM and image — included in pharo/

1. Start the Pharo Microservice

Run from the repository root so that FileSystem workingDirectory resolves the pharo/src/ paths correctly:

# Windows
pharo\vm\PharoConsole.exe pharo\image\Pharo13.0-SNAPSHOT-64bit-f201357c22.image st pharo\scripts\load_and_serve.st

# macOS / Linux (adjust binary name for your platform)
./pharo/vm/pharo pharo/image/Pharo13.0-SNAPSHOT-64bit-f201357c22.image st pharo/scripts/load_and_serve.st

Verify it is up:

curl http://localhost:8001/health
# → {"status":"ok","version":"1.0"}

To run on a different port set PHARO_SERVICE_PORT before the command and set the same value in .env so the bridge finds it:

PHARO_SERVICE_PORT=9001 pharo\vm\PharoConsole.exe ...

2. Start the FastAPI Backend

python -m uvicorn api.main:app --reload --port 8000

The backend automatically detects Pharo at localhost:8001 on startup. If Pharo is not running the Python fallback is used silently — no configuration change needed.

3. Start the Frontend

cd frontend
npm run dev
# → http://localhost:5173  (or 5174 if 5173 is busy)

Running Tests

Unit tests (no Pharo required — safe for CI)

python -m pytest tests/api/ -m "not pharo_integration" -q
# 229 passed

Pharo parity tests (requires Pharo running on port 8001)

python -m pytest tests/api/test_pharo_financial.py -m pharo_integration -v
# 9 passed

These 9 tests start the Pharo service, call each endpoint, and assert the Smalltalk output equals the Python reference output within a $1 tolerance for per-year values and a $5 tolerance for 25-year cumulative totals (floating-point rounding accumulates across 25 loop iterations).

Full backend suite

python -m pytest tests/api/ -v
# 238 passed

Frontend

cd frontend
npm run typecheck   # tsc --noEmit (strict mode)
npm run lint        # ESLint
npm run test        # Vitest unit tests

Environment Variables

Copy .env.example to .env. Pharo-specific variables:

Variable Default Description
PHARO_SERVICE_HOST localhost Host where Pharo is running
PHARO_SERVICE_PORT 8001 Port Pharo listens on
PHARO_SERVICE_TIMEOUT 5.0 HTTP timeout (seconds)

Other required variables (see .env.example for the full list):

Variable Description
GOOGLE_MAPS_API_KEY Satellite imagery + map rendering
GOOGLE_GEOCODING_API_KEY Address-to-coordinates lookup
NASA_POWER_API_BASE_URL Solar irradiance data
ANTHROPIC_API_KEY Claude Sonnet 4 (policy briefs, grant summaries)
GOOGLE_AI_API_KEY Gemini 2.5 Flash (real-time insights)
NREL_API_KEY EIA electricity rate lookup

Project Structure

/
├── api/                        # FastAPI backend
│   ├── services/
│   │   ├── solar_pipeline.py   # 11-step analysis orchestrator
│   │   ├── pharo_bridge.py     # Async HTTP client → Pharo microservice
│   │   ├── financial_model.py  # Python fallback (identical maths)
│   │   ├── segmentation.py     # SegFormer-B0 roof CV
│   │   ├── irradiance.py       # NASA POWER integration
│   │   └── equity_scorer.py    # Radiance equity index
│   ├── routers/                # FastAPI route handlers
│   ├── models/                 # Pydantic request/response schemas
│   └── main.py
│
├── pharo/                      # Pharo Smalltalk financial microservice
│   ├── src/
│   │   ├── SolarFinancialModel.st    # Pure 25-year financial calculator
│   │   └── SolarFinancialService.st  # ZnServer HTTP wrapper
│   ├── scripts/
│   │   ├── load_and_serve.st         # Headless startup script
│   │   └── run_tests.st              # SUnit runner
│   ├── tests/
│   │   └── SolarFinancialModelTest.st
│   ├── image/                        # Pharo 13 image + clean backup zip
│   └── vm/                           # PharoConsole.exe
│
├── frontend/                   # React 18 + Vite + TypeScript
│   └── src/
│       ├── components/         # Map, dashboard, equity, city scanner
│       ├── hooks/              # TanStack Query data hooks
│       └── stores/             # Zustand global state
│
├── tests/
│   └── api/
│       └── test_pharo_financial.py   # 22 tests: 13 unit + 9 Pharo parity
│
├── workers/                    # Celery task workers
├── infra/                      # Docker Compose + Kubernetes manifests
└── requirements.txt

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors