Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions .github/workflows/novas-qc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: Explicit independent benchmark QC
on:
workflow_dispatch:
inputs:
dataset_url:
description: Authorized HTTPS chart dataset URL (blank runs offset-only QC)
required: false
type: string
dataset_sha256:
description: Required SHA-256 when supplying a dataset URL
required: false
type: string
permissions:
contents: read
jobs:
independent-qc:
runs-on: ubuntu-24.04
timeout-minutes: 30
defaults:
run:
working-directory: library
steps:
- uses: actions/checkout@v4
with:
path: library
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.14'
- run: rustup toolchain install 1.96.0 --profile minimal --target wasm32-unknown-unknown
- name: Fetch the pinned benchmark source
run: |
revision=$(node -p 'JSON.parse(require("fs").readFileSync("tools/verification/novas-lock.json")).revision')
git init ../novas-reference
git -C ../novas-reference remote add origin https://github.com/AstrologyEngine/novas-benchmark.git
git -C ../novas-reference fetch --depth=1 origin "$revision"
git -C ../novas-reference checkout --detach FETCH_HEAD
- run: npm run setup
working-directory: novas-reference
- name: Prepare optional dataset and Wasm tool
env:
DATASET_URL: ${{ inputs.dataset_url }}
DATASET_SHA256: ${{ inputs.dataset_sha256 }}
run: |
mkdir -p target
if [ -n "$DATASET_URL" ]; then
node -e 'if (!/^[a-f0-9]{64}$/.test(process.env.DATASET_SHA256)) throw new Error("Dataset SHA-256 required")'
curl --fail --location --proto '=https' --proto-redir '=https' "$DATASET_URL" --output target/qc-dataset.bin
node -e 'const fs=require("fs"),crypto=require("crypto");if(crypto.createHash("sha256").update(fs.readFileSync("target/qc-dataset.bin")).digest("hex")!==process.env.DATASET_SHA256)throw new Error("Dataset checksum mismatch")'
cargo install wasm-bindgen-cli --version 0.2.122 --locked --root target/qc-tools
fi
- name: Run QC (numerical failures fail this job)
env:
DATASET_URL: ${{ inputs.dataset_url }}
run: |
JPL_KERNEL=../novas-reference/data/de440s.bsp node ../novas-reference/src/server.mjs > target/benchmark.log 2>&1 &
service_pid=$!
trap 'kill "$service_pid" 2>/dev/null || true' EXIT
for attempt in $(seq 1 30); do
if curl --fail --silent http://127.0.0.1:8788/health > /dev/null; then break; fi
sleep 1
done
if [ -n "$DATASET_URL" ]; then
node tools/verification/novas-qc.mjs http://127.0.0.1:8788 target/novas-qc-report.json target/qc-dataset.bin --wasm
else
node tools/verification/novas-qc.mjs http://127.0.0.1:8788 target/novas-qc-report.json
fi
- uses: actions/upload-artifact@v4
if: always()
with:
name: independent-qc-evidence
path: |
library/target/novas-qc-report.json
library/target/benchmark.log
if-no-files-found: warn
79 changes: 79 additions & 0 deletions examples/novas_qc_capture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Jack Asher
// SPDX-License-Identifier: MIT

#[cfg(feature = "generation")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
use astrology_engine::tooling::{nutation::iau2000b_full, time::AstroTime};
use astrology_engine::{
calculate_chart, lahiri_ayanamsa, ChartInput, Ephemeris, Epoch, HouseSystem, Location,
ZodiacConfiguration,
};
use serde::Deserialize;
use serde_json::json;
use std::io::{self, Read};
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Case {
id: String,
utc: String,
zodiac: String,
chart: bool,
latitude: f64,
longitude: f64,
#[serde(rename = "houseSystem")]
house_system: HouseSystem,
}
let data = std::env::args().nth(1).map(std::fs::read).transpose()?;
let ephemeris = data
.map(Ephemeris::parse)
.transpose()
.map_err(|e| format!("Dataset: {e:?}"))?;
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
let cases: Vec<Case> = serde_json::from_str(&input)?;
let mut output = Vec::with_capacity(cases.len());
for case in cases {
let epoch: Epoch = case.utc.parse()?;
let time = AstroTime::from_tdb(epoch);
let nutation = iau2000b_full(time);
let reported = lahiri_ayanamsa(epoch)?;
let chart = if case.chart {
let eph = ephemeris
.as_ref()
.ok_or("Chart requested without dataset")?;
let configuration: ZodiacConfiguration = serde_json::from_value(
json!({"reference":case.zodiac,"divisions":"equal","ophiuchus":null}),
)?;
Some(
match calculate_chart(
eph,
ChartInput {
epoch,
location: Location {
latitude: case.latitude,
longitude: case.longitude,
},
house_system: case.house_system,
zodiac: configuration,
},
) {
Ok(value) => json!({"value":value}),
Err(error) => json!({"error":error.to_string()}),
},
)
} else {
None
};
output.push(json!({"id":case.id,"utc":case.utc,"zodiac":case.zodiac,
"effectiveJdTT":time.tt+2451545.0,"effectiveJdUT1":time.ut+2451545.0,
"physicalJdTT":epoch.to_jde_tt_days(),"etSeconds":epoch.to_et_seconds(),
"reportedLahiri":reported,"nutationArcseconds":{"longitude":nutation.dpsi,"obliquity":nutation.deps},"chart":chart}));
}
println!("{}", serde_json::to_string(&output)?);
Ok(())
}
#[cfg(not(feature = "generation"))]
fn main() {
eprintln!("Use --features generation for the QC capture adapter");
std::process::exit(2);
}
15 changes: 15 additions & 0 deletions tools/verification/novas-lock.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"repository": "https://github.com/AstrologyEngine/novas-benchmark",
"provenance": {
"engine": "SuperNOVAS",
"version": "1.8.0",
"calceph": "5.0.1",
"erfa": "2.0.1",
"serviceHash": "358a650b2db4b814ed1304f2f52ccb25476dd82b173572a0501d08a8036a5476",
"dependencyHash": "63fba9330cee63c15b5d7adf1870d2c4a1e948cd509bf93507f24cfc9c2faff0",
"dataHash": "c1c7feeab882263fc493a9d5a5b2ddd71b54826cdf65d8d17a76126b260a49f2",
"ceresDataHash": "7af67832bbd60a3a0b6f3643a7eb7de25964cd6f4b4f1005ea637da1564993fc",
"chironDataHash": "71145196b709f04f8461c386dbac472ec581e7a237a0744ff26a7fe48f2c1666"
},
"revision": "5beb4cb3734cb13fad2eceaa1065de32798dfed5"
}
42 changes: 42 additions & 0 deletions tools/verification/novas-qc-findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Independent QC findings

This is a report of the pinned library branch and supplied immutable dataset,
not a change to calculation behavior or expected regression fixtures.

Dataset SHA-256:
`06d4c26e589207bf479dbd217fafb3e4d6c559626425b156912e39a4d859487b`.
Reference: independent SuperNOVAS service revision in `novas-lock.json`.
Reproduce with the native/Wasm command in [the QC guide](novas-qc.md).

The expanded sweep includes 831 cases, of which 423 request charts. It
completes 53,457 numerical checks without numerical failures. This includes
4,932 node checks, whose maximum absolute residual is 0.391780 arcseconds
(or arcseconds/day for rates), within the unchanged one-arcsecond limits.

The overall QC gate **fails with exit 1**: twelve polar inputs are rejected
by both native and Wasm with the library's overlapping-house error. Each of
these four spacetime combinations is tested in tropical, Lahiri and
Fagan-Bradley, at longitude zero with Placidus:

| UTC | Latitude |
| --- | ---: |
| 2026-03-20 18:00 | -85 |
| 2026-03-20 18:00 | -70 |
| 2026-03-20 06:00 | 70 |
| 2026-03-20 06:00 | 85 |

Each target completes 411 charts and rejects 12, producing 24 recorded
calculation errors. The independent reference constructs an ordered Porphyry
fallback for these inputs. The report preserves that geometry alongside the
errors. No row is fabricated for an unavailable library chart, and native
failure does not prevent a Wasm attempt.

The library already documents rejection of overlapping polar house sets.
These results expose that coverage limitation; they do not by themselves
justify changing its convention. Any library house change requires a separate
implementation and review. The benchmark gate remains strict in the meantime.

The expanded body list also includes Ceres and Chiron. All 4,932 completed
minor-body checks pass at the original angular/rate thresholds. Polar chart
rejections still prevent complete chart coverage; no values are fabricated
for rejected charts.
80 changes: 80 additions & 0 deletions tools/verification/novas-qc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Independent benchmark quality control

This QC is based on `fix/lahiri-full-nutation-import` at
`9f84a6e9ec815fb52418f26d98a6ccdd78d688dd`. It adds tooling only: library
calculations, fitted datasets and regression fixtures are unchanged.

Start the independent MIT benchmark from the exact commit recorded in
`novas-lock.json`, using its setup command and pinned JPL inputs. The runner
checks the service-source, dependency and dataset identities, and requires the
same native executable identity throughout the run. The benchmark and the
library must remain separate repositories.

```sh
node tools/verification/novas-qc.mjs http://127.0.0.1:8788 target/offset-qc.json
node tools/verification/novas-qc.mjs http://127.0.0.1:8788 target/chart-qc.json /path/to/cheb.bin --wasm
```

The runner requires Rust 1.96.0. With `--wasm`, it also requires Bun, the wasm32
Rust target and wasm-bindgen-cli 0.2.122. It uses `target/qc-tools/bin/wasm-bindgen`
if installed there, or WASM_BINDGEN/an executable on PATH. It rebuilds both the
native capture and Wasm adapter, records executable/glue hashes and a source
snapshot hash, and rejects source/dataset changes during the run.

Exit 0 means all requested checks passed; 1 means numerical or chart-calculation failures;
2 means setup, transport, identity or contract failure. Reports retain failure
rows. Do not change formulas, thresholds or frozen fixtures merely to make QC
pass. The manually dispatched `Explicit independent benchmark QC` workflow can
run offset-only, or native plus Wasm charts with an explicitly SHA-pinned dataset.
It is a separate opt-in gate, not a required baseline PR check.

## Scope and time conventions

Offset-only covers 408 cases: a two-year grid over 1801–2199, 80 short-interval
2026 cases, and 128 deterministic held-out instants (seed 0x4e4f5641).
Original IAU 2000B longitude/obliquity and reported Lahiri offset use a fixed
one-microarcsecond tolerance. The source library's 77 terms and the benchmark's
ERFA routine share the same standard; this checks implementation and integration,
not independent evidence that the standard itself is correct.

With a dataset, 375 additional chart cases cover 125 dates in 1851–2099 across
tropical, Lahiri and Fagan-Bradley. Fifteen major/derived bodies supply longitude,
speed and declination; limits are one arcsecond and one arcsecond/day. The runner
uses captured effective library TT for matched offset checks, and the physical
TT corresponding to each UTC input for end-to-end body checks. The latter retain
time-model effects rather than silently correcting candidate values.

The benchmark projects its full-nutation tropical positions with a matching
full-nutation offset. The library retains its five-term projection while reporting
the original 77-term offset. Neither is substituted for the other during QC.

The chart sweep rotates Whole Sign, Equal and Placidus across latitudes
-65, -45, 0, 45 and 65 degrees, with varying east-positive longitude. It checks
all twelve cusps and four angle longitudes at captured effective TT and UT1,
plus requested/actual house-system identity. These are matched-time diagnostics;
they do not establish the library's UTC-to-UT1 model accuracy. Angle speeds
and declinations are not benchmarked.

The North and South Node checks explicitly target the benchmark's osculating
geometric orbital-plane model, with symbolic South declination (same as North).
This client does not claim that the library supports switching to mean nodes
or geometric South declination. The benchmark's effective node model is
validated before any numerical comparison.

An additional 48 polar chart cases use March 20, 2026 at 00:00, 06:00, 12:00
and 18:00 UTC, latitudes -85, -70, 70 and 85 degrees, longitude zero, all three
zodiacs and Placidus with explicit reference Porphyry fallback. Total coverage
with a dataset is 831 cases, including 423 chart inputs. Native and Wasm are
attempted independently even if one rejects a chart. Reports preserve every
input, target outcome and independent reference geometry, including rejected
charts. Missing outcomes cannot count as passing evidence.

Reports explicitly set `fullChartCoverage: false`: mean-node and geometric
South-node library QC and True Sky remain outside this client.
Unsupported coverage and calculation errors fail the gate. See the
[current findings](novas-qc-findings.md) for the polar limitation retained by
this sweep.

Ceres and Chiron use the pinned Horizons kernels identified by separate hashes
in the lock and service provenance. Their longitude, declination and rates
are included in both native and Wasm end-to-end checks.
Loading
Loading