style: fix prettier table formatting in wiki

This commit is contained in:
Aric Camarata 2026-03-08 17:30:56 -04:00
parent f21be803fe
commit a57b4502b2
7 changed files with 256 additions and 258 deletions

View file

@ -6,49 +6,49 @@ Returns a `Promise<SpaResult>` with raw numeric values.
### Parameters
| Name | Type | Description |
| --- | --- | --- |
| `date` | `Date` | Date and time for the calculation |
| `latitude` | `number` | Observer latitude, -90 to 90 (negative = south) |
| Name | Type | Description |
| ----------- | -------- | ------------------------------------------------- |
| `date` | `Date` | Date and time for the calculation |
| `latitude` | `number` | Observer latitude, -90 to 90 (negative = south) |
| `longitude` | `number` | Observer longitude, -180 to 180 (negative = west) |
| `options` | `object` | Optional. See below |
| `options` | `object` | Optional. See below |
### Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `timezone` | `number` | auto | Hours from UTC. Auto-detected from the Date object if omitted |
| `elevation` | `number` | `0` | Meters above sea level |
| `pressure` | `number` | `1013.25` | Atmospheric pressure in millibars |
| `temperature` | `number` | `15` | Temperature in Celsius |
| `delta_ut1` | `number` | `0` | UT1-UTC correction in seconds |
| `delta_t` | `number` | `67` | TT-UTC difference in seconds |
| `slope` | `number` | `0` | Surface slope in degrees from horizontal |
| `azm_rotation` | `number` | `0` | Surface azimuth rotation in degrees from south |
| `atmos_refract` | `number` | `0.5667` | Atmospheric refraction in degrees |
| `function` | `number` | `3` | SPA function code (see below) |
| Option | Type | Default | Description |
| --------------- | -------- | --------- | ------------------------------------------------------------- |
| `timezone` | `number` | auto | Hours from UTC. Auto-detected from the Date object if omitted |
| `elevation` | `number` | `0` | Meters above sea level |
| `pressure` | `number` | `1013.25` | Atmospheric pressure in millibars |
| `temperature` | `number` | `15` | Temperature in Celsius |
| `delta_ut1` | `number` | `0` | UT1-UTC correction in seconds |
| `delta_t` | `number` | `67` | TT-UTC difference in seconds |
| `slope` | `number` | `0` | Surface slope in degrees from horizontal |
| `azm_rotation` | `number` | `0` | Surface azimuth rotation in degrees from south |
| `atmos_refract` | `number` | `0.5667` | Atmospheric refraction in degrees |
| `function` | `number` | `3` | SPA function code (see below) |
### Result Fields
| Field | Type | Unit | Description |
| --- | --- | --- | --- |
| `zenith` | `number` | degrees | Topocentric zenith angle (0 = directly overhead) |
| `azimuth` | `number` | degrees | Topocentric azimuth, eastward from north (navigational convention) |
| `azimuth_astro` | `number` | degrees | Topocentric azimuth, westward from south (astronomical convention) |
| `incidence` | `number` | degrees | Surface incidence angle |
| `sunrise` | `number` | fractional hours | Local sunrise time |
| `sunset` | `number` | fractional hours | Local sunset time |
| `suntransit` | `number` | fractional hours | Solar noon (sun transit) |
| `sun_transit_alt` | `number` | degrees | Sun altitude at transit |
| `eot` | `number` | minutes | Equation of time |
| `error_code` | `number` | integer | 0 on success |
| Field | Type | Unit | Description |
| ----------------- | -------- | ---------------- | ------------------------------------------------------------------ |
| `zenith` | `number` | degrees | Topocentric zenith angle (0 = directly overhead) |
| `azimuth` | `number` | degrees | Topocentric azimuth, eastward from north (navigational convention) |
| `azimuth_astro` | `number` | degrees | Topocentric azimuth, westward from south (astronomical convention) |
| `incidence` | `number` | degrees | Surface incidence angle |
| `sunrise` | `number` | fractional hours | Local sunrise time |
| `sunset` | `number` | fractional hours | Local sunset time |
| `suntransit` | `number` | fractional hours | Solar noon (sun transit) |
| `sun_transit_alt` | `number` | degrees | Sun altitude at transit |
| `eot` | `number` | minutes | Equation of time |
| `error_code` | `number` | integer | 0 on success |
### Timezone Auto-detection
When `timezone` is omitted, the value is derived from the `Date` object's local timezone offset:
```js
timezone = -(date.getTimezoneOffset() / 60)
timezone = -(date.getTimezoneOffset() / 60);
```
This works correctly in most cases. Provide an explicit value when computing for a location whose timezone differs from the machine's local timezone.
@ -72,16 +72,14 @@ A null result pointer (WASM memory allocation failure) also throws.
Same parameters and behavior as `spa()`. Returns a result object with the same fields, but `sunrise`, `sunset`, and `suntransit` are `HH:MM:SS` strings instead of fractional hours. During polar day or polar night, these strings are `"N/A"`:
```js
const result = await spaFormatted(
new Date(2025, 5, 21, 12, 0, 0),
40.7128, -74.006,
{ timezone: -4 }
);
const result = await spaFormatted(new Date(2025, 5, 21, 12, 0, 0), 40.7128, -74.006, {
timezone: -4,
});
console.log(result.sunrise); // "05:25:12"
console.log(result.sunset); // "20:30:42"
console.log(result.sunrise); // "05:25:12"
console.log(result.sunset); // "20:30:42"
console.log(result.suntransit); // "12:57:54"
console.log(result.zenith); // 27.08 (still a number)
console.log(result.zenith); // 27.08 (still a number)
```
## `formatTime(hours)`
@ -89,9 +87,9 @@ console.log(result.zenith); // 27.08 (still a number)
Converts fractional hours to an `HH:MM:SS` string. Returns `"N/A"` for non-finite or negative values, which occur during polar day or polar night when sunrise or sunset does not happen. Values at or above 24 hours wrap to the next day (e.g., 24.5 becomes `"00:30:00"`).
```js
formatTime(6.5); // "06:30:00"
formatTime(12); // "12:00:00"
formatTime(Infinity); // "N/A"
formatTime(6.5); // "06:30:00"
formatTime(12); // "12:00:00"
formatTime(Infinity); // "N/A"
```
## `init()`
@ -116,12 +114,12 @@ Calling `init()` multiple times is safe. The second and subsequent calls return
The `function` option controls which outputs the SPA computes. Lower codes skip the rise/transit/set and incidence calculations, which are the most expensive part.
| Constant | Value | Computes |
| --- | --- | --- |
| `SPA_ZA` | `0` | Zenith and azimuth only |
| `SPA_ZA_INC` | `1` | Zenith, azimuth, and incidence angle |
| `SPA_ZA_RTS` | `2` | Zenith, azimuth, and rise/transit/set times |
| `SPA_ALL` | `3` | All output values (default) |
| Constant | Value | Computes |
| ------------ | ----- | ------------------------------------------- |
| `SPA_ZA` | `0` | Zenith and azimuth only |
| `SPA_ZA_INC` | `1` | Zenith, azimuth, and incidence angle |
| `SPA_ZA_RTS` | `2` | Zenith, azimuth, and rise/transit/set times |
| `SPA_ALL` | `3` | All output values (default) |
```js
import { spa, SPA_ZA } from 'solar-spa';

View file

@ -22,23 +22,23 @@ The C source is compiled with Emscripten to produce a single JavaScript file tha
### Build flags
| Flag | Purpose |
| --- | --- |
| `-O3 -flto` | Maximum optimization with link-time optimization. The compiler inlines across translation units and eliminates dead code |
| `--no-entry` | No `main()` function exists. The module exposes only the exported wrapper functions |
| `-sSINGLE_FILE=1` | Inlines the WASM binary as a base64 string inside the JavaScript file. Eliminates the `.wasm` file entirely |
| `-sMODULARIZE=1` | Wraps the output in a factory function (`createSpaModule()`) instead of executing immediately. Prevents global `Module` pollution |
| `-sEXPORT_NAME=createSpaModule` | Names the factory function |
| `-sEXPORTED_FUNCTIONS` | Exposes `_spa_calculate_wrapper`, `_spa_free_result`, `_malloc`, and `_free` to JavaScript |
| `-sEXPORTED_RUNTIME_METHODS` | Makes `cwrap` and `getValue` available on the module instance |
| `-sNO_FILESYSTEM=1` | Excludes the virtual filesystem API. SPA does not read files. Saves ~15KB |
| `-sINITIAL_MEMORY=1048576` | 1MB fixed memory. SPA allocates one 80-byte struct per call, so this is more than sufficient |
| `-sALLOW_MEMORY_GROWTH=0` | Disables dynamic memory growth. Fixed memory avoids the overhead of growable ArrayBuffers and detached buffer checks |
| `-sSTACK_SIZE=65536` | 64KB stack. Default is 5MB, which is wasteful for a pure computation |
| `-sENVIRONMENT='node,web,worker'` | Includes runtime support for Node.js, browsers, and web workers |
| `-sASSERTIONS=0` | Removes debug assertions. Smaller output, no runtime checks |
| `-sDISABLE_EXCEPTION_CATCHING=1` | Disables C++ exception support. SPA is plain C, so this strips dead code |
| `-sWASM_BIGINT=0` | Disables BigInt integration for 64-bit integers. SPA uses only doubles and 32-bit ints |
| Flag | Purpose |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `-O3 -flto` | Maximum optimization with link-time optimization. The compiler inlines across translation units and eliminates dead code |
| `--no-entry` | No `main()` function exists. The module exposes only the exported wrapper functions |
| `-sSINGLE_FILE=1` | Inlines the WASM binary as a base64 string inside the JavaScript file. Eliminates the `.wasm` file entirely |
| `-sMODULARIZE=1` | Wraps the output in a factory function (`createSpaModule()`) instead of executing immediately. Prevents global `Module` pollution |
| `-sEXPORT_NAME=createSpaModule` | Names the factory function |
| `-sEXPORTED_FUNCTIONS` | Exposes `_spa_calculate_wrapper`, `_spa_free_result`, `_malloc`, and `_free` to JavaScript |
| `-sEXPORTED_RUNTIME_METHODS` | Makes `cwrap` and `getValue` available on the module instance |
| `-sNO_FILESYSTEM=1` | Excludes the virtual filesystem API. SPA does not read files. Saves ~15KB |
| `-sINITIAL_MEMORY=1048576` | 1MB fixed memory. SPA allocates one 80-byte struct per call, so this is more than sufficient |
| `-sALLOW_MEMORY_GROWTH=0` | Disables dynamic memory growth. Fixed memory avoids the overhead of growable ArrayBuffers and detached buffer checks |
| `-sSTACK_SIZE=65536` | 64KB stack. Default is 5MB, which is wasteful for a pure computation |
| `-sENVIRONMENT='node,web,worker'` | Includes runtime support for Node.js, browsers, and web workers |
| `-sASSERTIONS=0` | Removes debug assertions. Smaller output, no runtime checks |
| `-sDISABLE_EXCEPTION_CATCHING=1` | Disables C++ exception support. SPA is plain C, so this strips dead code |
| `-sWASM_BIGINT=0` | Disables BigInt integration for 64-bit integers. SPA uses only doubles and 32-bit ints |
The `SINGLE_FILE` flag is the critical one. Most WASM packages ship a separate `.wasm` file and resolve its path at runtime using `__dirname`, `import.meta.url`, or `URL` constructors. This breaks in bundlers (Webpack rewrites paths), edge runtimes (no filesystem), and testing environments (different module resolution). By inlining the binary, the module is self-contained. It works anywhere JavaScript runs.

View file

@ -28,16 +28,16 @@ For single calls, the difference is negligible. See [Performance](Performance) f
import { spa } from 'solar-spa';
const result = await spa(
new Date(2025, 5, 21, 12, 0, 0), // June 21, 2025 at noon
40.7128, // latitude (NYC)
-74.0060, // longitude
{ timezone: -4, elevation: 10 } // EDT (UTC-4), 10m elevation
new Date(2025, 5, 21, 12, 0, 0), // June 21, 2025 at noon
40.7128, // latitude (NYC)
-74.006, // longitude
{ timezone: -4, elevation: 10 }, // EDT (UTC-4), 10m elevation
);
console.log(result.zenith); // ~27 (degrees from vertical)
console.log(result.azimuth); // ~179 (degrees from north)
console.log(result.sunrise); // ~5.4 (fractional hours)
console.log(result.sunset); // ~20.5 (fractional hours)
console.log(result.zenith); // ~27 (degrees from vertical)
console.log(result.azimuth); // ~179 (degrees from north)
console.log(result.sunrise); // ~5.4 (fractional hours)
console.log(result.sunset); // ~20.5 (fractional hours)
```
CommonJS works too:

View file

@ -80,25 +80,25 @@ The SPA's accuracy far exceeds the precision of any practical measurement system
The SPA validates all inputs and returns a non-zero error code if any are out of range:
| Parameter | Valid range |
| --- | --- |
| Year | -2000 to 6000 |
| Month | 1 to 12 |
| Day | 1 to 31 |
| Hour | 0 to 24 |
| Minute | 0 to 59 |
| Second | 0 to less than 60 |
| Timezone | -18 to 18 |
| Latitude | -90 to 90 |
| Longitude | -180 to 180 |
| Elevation | -6500000 or higher (meters) |
| Pressure | 0 to 5000 |
| Temperature | -273 to 6000 |
| Delta UT1 | -1 to 1 |
| Delta T | -8000 to 8000 |
| Slope | -360 to 360 |
| Azimuth rotation | -360 to 360 |
| Atmospheric refraction | -5 to 5 |
| Parameter | Valid range |
| ---------------------- | --------------------------- |
| Year | -2000 to 6000 |
| Month | 1 to 12 |
| Day | 1 to 31 |
| Hour | 0 to 24 |
| Minute | 0 to 59 |
| Second | 0 to less than 60 |
| Timezone | -18 to 18 |
| Latitude | -90 to 90 |
| Longitude | -180 to 180 |
| Elevation | -6500000 or higher (meters) |
| Pressure | 0 to 5000 |
| Temperature | -273 to 6000 |
| Delta UT1 | -1 to 1 |
| Delta T | -8000 to 8000 |
| Slope | -360 to 360 |
| Azimuth rotation | -360 to 360 |
| Atmospheric refraction | -5 to 5 |
The solar-spa package propagates these error codes as thrown JavaScript errors.

View file

@ -26,7 +26,7 @@ Use `init()` at application startup to pay the initialization cost early:
```js
import { init } from 'solar-spa';
await init(); // ~5-15ms, happens once
await init(); // ~5-15ms, happens once
```
## Memory footprint
@ -45,12 +45,12 @@ Memory growth is disabled (`ALLOW_MEMORY_GROWTH=0`). This means the ArrayBuffer
Not all callers need every output. The `function` option controls how much work the SPA does:
| Code | Computation | Relative cost |
| --- | --- | --- |
| `SPA_ZA` (0) | Zenith and azimuth | ~1x |
| `SPA_ZA_INC` (1) | + incidence angle | ~1x (incidence is cheap) |
| Code | Computation | Relative cost |
| ---------------- | ------------------ | -------------------------------- |
| `SPA_ZA` (0) | Zenith and azimuth | ~1x |
| `SPA_ZA_INC` (1) | + incidence angle | ~1x (incidence is cheap) |
| `SPA_ZA_RTS` (2) | + rise/transit/set | ~3x (three position evaluations) |
| `SPA_ALL` (3) | All outputs | ~3x |
| `SPA_ALL` (3) | All outputs | ~3x |
The sunrise/sunset calculation is the expensive part. It evaluates the full position algorithm three times (for transit, sunrise, and sunset). If you only need the current sun position, use `SPA_ZA` for a roughly 3x speed improvement.
@ -69,14 +69,14 @@ These flags together produce a ~60KB output file, down from the ~150KB that a de
## When to use solar-spa vs nrel-spa
| Scenario | Recommended |
| --- | --- |
| Single position lookup (e.g., sunrise for today) | Either. Both are fast enough |
| Batch computation (hundreds or thousands of positions) | solar-spa (WASM) |
| Animation or real-time tracking | solar-spa (WASM) |
| Synchronous API required | [nrel-spa](https://github.com/acamarata/nrel-spa) (pure JS, sync) |
| Environments without WASM support | [nrel-spa](https://github.com/acamarata/nrel-spa) |
| Minimal dependency footprint | [nrel-spa](https://github.com/acamarata/nrel-spa) (zero deps, ~30KB) |
| Scenario | Recommended |
| ------------------------------------------------------ | -------------------------------------------------------------------- |
| Single position lookup (e.g., sunrise for today) | Either. Both are fast enough |
| Batch computation (hundreds or thousands of positions) | solar-spa (WASM) |
| Animation or real-time tracking | solar-spa (WASM) |
| Synchronous API required | [nrel-spa](https://github.com/acamarata/nrel-spa) (pure JS, sync) |
| Environments without WASM support | [nrel-spa](https://github.com/acamarata/nrel-spa) |
| Minimal dependency footprint | [nrel-spa](https://github.com/acamarata/nrel-spa) (zero deps, ~30KB) |
Both packages implement the same NREL algorithm and produce identical results within floating-point rounding tolerance.

View file

@ -6,15 +6,15 @@ Pre-release validation of the solar-spa v2.0.0 WASM implementation. All results
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 |
| 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.**
@ -22,48 +22,48 @@ Pre-release validation of the solar-spa v2.0.0 WASM implementation. All results
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 |
| # | 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.
@ -71,23 +71,23 @@ Note: "Summer" and "Winter" refer to Northern Hemisphere seasons. For Southern H
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 |
| # | 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).
@ -95,62 +95,62 @@ The equinox results confirm the algorithm's accuracy: a zenith of 1.84 degrees a
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 |
| # | 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 |
| # | 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 |
| # | 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 |
| # | 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.
@ -158,18 +158,18 @@ Scenario 89 confirms that different atmospheric pressures produce measurably dif
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 |
| # | 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.
@ -182,13 +182,13 @@ Measured on Apple Silicon (Node.js), single-threaded.
From the 100 validation scenarios:
| Metric | Value |
| --- | --- |
| Min | 7us |
| Max | 242us |
| Mean | 46us |
| Median | 33us |
| P95 | 110us |
| P99 | 168us |
| ------ | ----- |
| 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.
@ -196,10 +196,10 @@ The first call is slowest (242us) because it includes WASM module initialization
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 |
| 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.

View file

@ -40,7 +40,7 @@ Emscripten supports a `locateFile` callback that lets the consumer specify where
```js
const Module = await createModule({
locateFile: (path) => '/static/wasm/' + path
locateFile: (path) => '/static/wasm/' + path,
});
```
@ -54,10 +54,8 @@ Webpack can be configured to copy `.wasm` files to the output directory and rewr
// webpack.config.js
module.exports = {
module: {
rules: [
{ test: /\.wasm$/, type: 'asset/resource' }
]
}
rules: [{ test: /\.wasm$/, type: 'asset/resource' }],
},
};
```
@ -90,13 +88,15 @@ let _pending: Promise<void> | null = null;
export function init(): Promise<void> {
if (_module) return Promise.resolve();
if (_pending) return _pending;
_pending = createSpaModule().then((mod) => {
_module = mod;
_pending = null;
}).catch((err) => {
_pending = null; // Allow retry on next call
throw err;
});
_pending = createSpaModule()
.then((mod) => {
_module = mod;
_pending = null;
})
.catch((err) => {
_pending = null; // Allow retry on next call
throw err;
});
return _pending;
}
```
@ -117,13 +117,13 @@ For small to medium WASM binaries (under a few hundred KB), inlining as base64 i
## Summary
| Approach | Universal? | Consumer config? | Size overhead |
| --- | --- | --- | --- |
| Separate `.wasm` + default resolution | No | No | None |
| `locateFile` callback | Yes* | Yes (per-bundler) | None |
| Bundler-specific config | Per-bundler | Yes | None |
| `import.meta.url` | Partial | No | None |
| **SINGLE_FILE (base64 inline)** | **Yes** | **No** | **~33%** |
| Approach | Universal? | Consumer config? | Size overhead |
| ------------------------------------- | ----------- | ----------------- | ------------- |
| Separate `.wasm` + default resolution | No | No | None |
| `locateFile` callback | Yes\* | Yes (per-bundler) | None |
| Bundler-specific config | Per-bundler | Yes | None |
| `import.meta.url` | Partial | No | None |
| **SINGLE_FILE (base64 inline)** | **Yes** | **No** | **~33%** |
solar-spa uses the last approach. It works in every tested environment without any consumer configuration.