solar-spa/.wiki/Validation-and-Benchmarks.md
Aric Camarata fb0c14e761 v2.0.0: TypeScript rewrite with WASM recompilation
Complete rewrite of the package from plain JavaScript to TypeScript,
compiled to dual CJS/ESM via tsup. The NREL SPA C source is recompiled
to WASM with Emscripten using SINGLE_FILE base64 inlining, eliminating
bundler path-resolution issues.

Changes:
- Rewrite JS wrapper in TypeScript with full type definitions
- Recompile WASM with -O3 -flto, 1MB fixed memory, no filesystem
- Add input validation with descriptive error messages
- Add spaFormatted() for HH:MM:SS time strings
- Add formatTime() utility and init() for eager WASM loading
- Add SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL function code exports
- Dual CJS/ESM output via tsup with proper exports map
- Test suite: 68 ESM + 13 CJS assertions
- 100-scenario validation suite across 7 categories
- GitHub Wiki with 8 documentation pages
- CI workflow: Node 20/22/24 matrix, typecheck, pack-check
- NREL attribution in LICENSE and README per their license terms
- Minimum Node.js 20
2026-02-25 10:35:24 -05:00

11 KiB

Validation and Benchmarks

Pre-release validation of the solar-spa v2.0.0 WASM implementation. All results were generated by validate.mjs in the repository root.

Overview

100 scenarios covering seven categories:

Category Scenarios Description
Cities worldwide 1-40 20 cities across every continent, summer and winter solstice
Boundary conditions 41-55 Poles, equator, date line, extreme elevation, date range limits
Polar regions 56-65 Polar day, polar night, midnight sun, Antarctic stations
Time edge cases 66-75 Midnight, dawn, dusk, leap year, fractional seconds
Function code consistency 76-80 All four function codes produce identical zenith/azimuth
Atmospheric conditions 81-90 Pressure, temperature, refraction, vacuum, high altitude
Historical/future dates 91-100 Year -2000 to 6000, Gregorian switch, Apollo era

Result: 100/100 passed.

Category 1: Cities Worldwide (1-40)

20 major cities tested at both the June 21 and December 21 solstices, 2025, local noon. Each scenario validates that the zenith angle falls within a physically reasonable range for the latitude and season.

# City Season Zenith Azimuth Time
1 New York Summer 21.12 140.47 242us
2 New York Winter 65.35 166.30 168us
3 London Summer 30.52 150.96 110us
4 London Winter 75.98 166.15 110us
5 Tokyo Summer 12.77 197.73 102us
6 Tokyo Winter 59.29 185.50 99us
7 Sydney Summer 57.29 359.16 100us
8 Sydney Winter 10.54 351.37 112us
9 Cairo Summer 6.64 186.17 115us
10 Cairo Winter 53.49 181.94 101us
11 Mumbai Summer 10.35 63.31 109us
12 Mumbai Winter 43.43 167.76 96us
13 Sao Paulo Summer 47.02 2.64 94us
14 Sao Paulo Winter 1.10 84.39 137us
15 Moscow Summer 32.82 166.65 71us
16 Moscow Winter 79.33 173.55 56us
17 Beijing Summer 16.81 167.09 54us
18 Beijing Winter 63.38 176.82 50us
19 Nairobi Summer 26.11 18.24 56us
20 Nairobi Winter 23.37 161.93 55us
21 Reykjavik Summer 43.28 149.34 49us
22 Reykjavik Winter 88.81 160.36 47us
23 Singapore Summer 27.34 34.86 34us
24 Singapore Winter 29.10 149.34 25us
25 Cape Town Summer 58.47 12.97 22us
26 Cape Town Winter 14.29 45.72 22us
27 Buenos Aires Summer 59.49 14.77 22us
28 Buenos Aires Winter 15.87 48.72 21us
29 Dubai Summer 5.04 109.42 22us
30 Dubai Winter 48.80 174.81 25us
31 Toronto Summer 25.98 134.65 22us
32 Toronto Winter 69.27 161.43 24us
33 Mexico City Summer 9.80 64.18 23us
34 Mexico City Winter 43.69 168.40 23us
35 Seoul Summer 15.88 150.42 46us
36 Seoul Winter 61.38 172.14 31us
37 Rome Summer 23.76 135.39 26us
38 Rome Winter 67.18 163.05 24us
39 Anchorage Summer 43.13 137.26 25us
40 Anchorage Winter 87.67 153.13 23us

Note: "Summer" and "Winter" refer to Northern Hemisphere seasons. For Southern Hemisphere cities (Sydney, Sao Paulo, Cape Town, Buenos Aires, Nairobi), the seasons are reversed. A high zenith in "summer" (June) for Sydney is correct because June is winter there.

Category 2: Boundary Conditions (41-55)

Tests at the mathematical limits of the algorithm's input domain.

# Scenario Zenith Note
41 North Pole, June solstice 66.53 Midnight sun: zenith < 90
42 North Pole, Dec solstice 113.44 Polar night: zenith > 90
43 South Pole, Dec solstice 66.53 Midnight sun (southern summer)
44 South Pole, June solstice 113.44 Polar night (southern winter)
45 Equator, March equinox 1.84 Near-overhead sun
46 Equator, Sept equinox 1.84 Near-overhead sun
47 Equator, June solstice 23.44 Sun 23.44 north of equator
48 Equator, Dec solstice 23.44 Sun 23.44 south of equator
49 Date line +180 23.44 Longitude boundary
50 Date line -180 23.43 Longitude boundary
51 Mt Everest (8849m) 4.55 Thin atmosphere (314 mbar, -20C)
52 Dead Sea (-430m) 11.95 Dense atmosphere (1065 mbar, 40C)
53 Year -2000 7.90 Earliest valid date
54 Year 6000 7.27 Latest valid date
55 Year 6001 throws Correctly rejects out-of-range year

The equinox results confirm the algorithm's accuracy: a zenith of 1.84 degrees at the equator on the March equinox at solar noon on the prime meridian is consistent with the small angular offset between the vernal equinox and the actual date (March 20 vs. the instant the sun crosses the celestial equator).

Category 3: Polar Regions (56-65)

Polar day/night conditions test the algorithm's handling of non-standard sunrise/sunset scenarios.

# Location Condition Zenith
56 Tromso, Norway (69.6N) Polar day (June) 46.70
57 Tromso, Norway Polar night (Dec) 93.14
58 Murmansk, Russia (69.0N) Polar day (June) 46.12
59 Murmansk, Russia Polar night (Dec) 92.77
60 Utqiagvik, AK (71.3N) Polar day (June) 52.32
61 Utqiagvik, AK Polar night (Dec) 95.90
62 McMurdo Station (-77.9S) Summer (Dec) 55.95
63 McMurdo Station Winter (June) 101.62
64 Svalbard (78.2N) Midnight sun (June, 00:00) 77.90
65 South Pole Station (-90S) Summer (Jan) 67.01

Scenario 64 is notable: at Svalbard at midnight on the June solstice, the sun is still 12.1 degrees above the horizon (90 - 77.9 = 12.1). This is correct for 78N latitude during continuous polar daylight.

Category 4: Time Edge Cases (66-75)

# Scenario Zenith Note
66 Exact midnight UTC (London, June) 105.05 Sun well below horizon
67 Dawn, 5 AM summer London 88.47 Near horizon
68 Dusk, 9 PM summer London 87.93 Near horizon
69 Solar noon NYC (13:00 EDT) 17.28 Azimuth 181.6 (nearly due south)
70 UTC midnight Jan 1, equator 156.99 Deep below horizon
71 Fractional seconds 18.24 Handles sub-minute times
72 End of day 23:59 114.40 Late night
73 Feb 29 leap year (2024) 48.33 Leap day handled correctly
74 Prime meridian equator noon 1.84 Most symmetric case
75 New Year's Eve midnight 162.37 Deep below horizon

Category 5: Function Code Consistency (76-80)

All four function codes (SPA_ZA, SPA_ZA_INC, SPA_ZA_RTS, SPA_ALL) produce identical zenith and azimuth values within 0.01 degree tolerance. This confirms that the function code parameter only affects which outputs are computed, not the core position algorithm.

# Test Result
76 SPA_ZA matches SPA_ALL zenith identical
77 SPA_ZA_INC matches SPA_ALL zenith, azimuth, incidence identical
78 SPA_ZA_RTS matches SPA_ALL zenith, azimuth identical
79 SPA_ALL fields all finite All 9 numeric fields populated
80 azimuth = (azimuth_astro + 180) % 360 Navigational/astronomical consistency

Category 6: Atmospheric Conditions (81-90)

# Condition Zenith Note
81 Standard atmosphere (1013.25 mbar, 15C) 21.12 Reference baseline
82 Low pressure (300 mbar, -30C, 9000m) 21.12 High altitude conditions
83 High pressure (1100 mbar) 21.12 Dense atmosphere
84 Extreme cold (-40C) 43.28 Reykjavik test
85 Extreme heat (+50C) 5.04 Dubai test
86 Zero pressure (vacuum) 21.13 No refraction correction
87 Custom refraction (0 deg) 21.12 Override default 0.5667
88 Custom refraction (2 deg) 21.12 Large refraction override
89 Pressure effect on zenith varies Low != high pressure confirmed
90 High elevation + low pressure 4.55 Everest base camp

Scenario 89 confirms that different atmospheric pressures produce measurably different zenith values. The difference is small (sub-arc-second at high solar elevations) but present, which validates that the atmospheric refraction correction is active and working.

Category 7: Historical and Future Dates (91-100)

The SPA is valid for years -2000 to 6000. These scenarios confirm correct behavior across the full range, with era-appropriate delta_t values.

# Year Context Zenith delta_t
91 1000 CE Medieval era 17.25 1574s
92 1582 Gregorian calendar switch 50.37 120s
93 1900 Turn of century 25.45 -3s
94 1969 Apollo 11 era, Cape Canaveral 10.31 40s
95 2050 Near future 21.13 93s
96 2100 Far future 74.88 200s
97 3000 Distant future 12.77 0s
98 5000 Deep future 0.67 0s
99 -1000 (1001 BCE) Ancient Athens 15.07 0s
100 -2000 (earliest valid) Ancient Cairo, winter solstice 53.00 0s

Delta_t values for historical dates follow published estimates. For dates beyond ~2050, delta_t is not well predicted. The scenarios use conservative values to avoid introducing error from the correction itself.

Performance Benchmarks

Measured on Apple Silicon (Node.js), single-threaded.

Per-Call Latency

From the 100 validation scenarios:

Metric Value
Min 7us
Max 242us
Mean 46us
Median 33us
P95 110us
P99 168us

The first call is slowest (242us) because it includes WASM module initialization. Subsequent calls settle into the 20-40us range for SPA_ALL computations.

Batch Throughput

10,000 consecutive calls to the same location and date:

Function Code Time Throughput
SPA_ALL 201ms ~50,000 calls/sec
SPA_ZA 46ms ~219,000 calls/sec

SPA_ZA is roughly 4x faster than SPA_ALL because it skips the sunrise/sunset iterative solver, which requires three full position evaluations per call.

Initialization

First-time WASM module initialization takes approximately 5-15ms depending on hardware and Node.js version. Subsequent init() calls return immediately (0.0ms). The module is initialized lazily on the first spa() call, or eagerly via init().

Comparison to Native C

The NREL reference C implementation, compiled natively with -O2, runs a single SPA_ALL call in approximately 5-10us on comparable hardware. The WASM version at ~20us (steady state, warm cache) represents roughly a 2-3x overhead from the WASM sandbox, JavaScript FFI, and memory copy for the result struct.

For most applications, the absolute latency difference (10-15us per call) is not measurable. The overhead becomes relevant only at extreme volumes (millions of calls per second), at which point native C would be the correct choice regardless.

Running the Validation Suite

git clone https://github.com/acamarata/solar-spa.git
cd solar-spa
npm install
npm run build
node validate.mjs

The suite takes approximately 3 seconds to run, including the 20,000-call throughput benchmark.


Home · Performance · API Reference · NREL SPA Algorithm