Skip to content

Accuracy and verification

A chart is only useful if it is right, and you should not have to take that on trust. This page describes how the engine is tested, the limits it must meet, the latest results and the data you can use to check the numbers yourself.

  • Tested against Swiss Ephemeris. The engine is compared with Swiss Ephemeris 2.10.03 and its JPL-based ephemeris files on 5,433 births drawn from a fixed random seed (0x1a6a2026), so the same sample is used every time:

    • 5,000 random births from 1900 to 2100: half in 29 cities weighted towards Nepal and India, half at random places between 60° S and 65° N.
    • 433 edge cases placed seconds to minutes away from sign, nakshatra, pada, lagna sign, tithi and yoga boundaries, and at clock changes in Nepal, the UK, the US and Australia.

    Five of the clock-change cases are local times that repeat or never happen, sent without disambiguation. The engine correctly answers ambiguous_local_time for them, so 5,428 births are compared.

  • Same instant, same conventions. Both sides are evaluated at the same UT instant: geocentric apparent positions, true equinox of date, Lahiri ayanamsha including nutation, mean node, and the lagna as the tropical ascendant minus the ayanamsha. Vimshottari dates on the reference side come from its sidereal Moon with 365.25-day years.

  • Sunrise and sunset are compared for every day of 1990, 2026 and 2075 in Kathmandu with the center_geometric convention, and for 2026 with upper_limb_refracted in Kathmandu and center_geometric in London and Reykjavik.

  • Hard limits block the release. If any sample crosses a hard limit below, the release does not ship.

  • Reviewed conventions. Guna tables, dasha rules and choghadiya sequences are reviewed by a contracted jyotish reviewer before they ship.

  • Deterministic output. The same input with the same engine and data versions returns byte-identical JSON. Dasha output never depends on the server clock.

  • Everything named. meta in every response states the engine version, ayanamsha and its value, node type and data versions. See Conventions.

The target is what each release aims for. The hard limit is the line no sample may cross.

Quantity Target Hard limit
Moon longitude ≤ 15″ > 60″
Sun and planet longitudes ≤ 5″ > 60″
Lagna longitude, same UT ≤ 10″ > 30″
Ayanamsha ≤ 1″ > 1″
Mean node ≤ 5″ > 10″
Discrete fields: sign, D9, nakshatra, pada, tithi, yoga Exact A mismatch is allowed only when the underlying value is within that quantity’s hard limit of a boundary
Dasha boundary dates ± 1 day > 1 day
Sunrise and sunset, against the configured convention ± 1 min > 2 min

The “discrete fields” rule matters in practice. A Moon 20″ from a nakshatra boundary is inside the Moon’s 60″ hard limit, so two correct engines may legitimately disagree on its nakshatra. Everywhere else, discrete fields must match exactly.

Run of 2026-10-21, engine 0.1.0, against Swiss Ephemeris 2.10.03 with JPL-based ephemeris files. Differences are |engine − reference| over the 5,428 compared births, using the longitudes the API returns (4 decimal places).

Quantity Worst case 99th percentile Target Result
Moon longitude 4.20″ 2.43″ ≤ 15″ Target met
Sun longitude 2.19″ 1.63″ ≤ 5″ Target met
Mercury longitude 11.33″ 7.28″ ≤ 5″ Target missed, inside hard limit
Venus longitude 20.24″ 12.73″ ≤ 5″ Target missed, inside hard limit
Mars longitude 12.68″ 7.51″ ≤ 5″ Target missed, inside hard limit
Jupiter longitude 10.14″ 8.86″ ≤ 5″ Target missed, inside hard limit
Saturn longitude 13.07″ 11.06″ ≤ 5″ Target missed, inside hard limit
Lagna longitude 4.67″ 2.42″ ≤ 10″ Target met
Ayanamsha (Lahiri) 0.43″ 0.35″ ≤ 1″ Target met
Mean node 0.43″ 0.36″ ≤ 5″ Target met
Dasha boundary dates (maha and antar) 0.56 days 0.31 days ± 1 day Target met
Sunrise and sunset, center_geometric, Kathmandu 0.80 s 0.70 s or less ± 1 min Target met
Sunrise and sunset, upper_limb_refracted, Kathmandu 2026 4.80 s 4.64 s ± 1 min Target met

Discrete fields: 227,976 checks of sign, D9, nakshatra, pada, tithi and yoga gave 57 mismatches, every one with the reference value within 2.3″ of a boundary, so all are allowed by the boundary rule. Most are the edge cases placed on purpose next to a boundary.

No sample crossed a hard limit. The Moon, Sun, lagna, ayanamsha, node, dasha and sunrise figures meet their targets. Mercury, Venus, Mars, Jupiter and Saturn do not yet meet the 5″ target, at up to 20.24″ for Venus, but stay well inside the 60″ hard limit. Seven births are left out of the dasha row because the Moon sits less than 1″ from a nakshatra boundary and the two sides start from a different lord; they are counted under discrete fields.

Ephemeris error is rarely the biggest uncertainty in a chart: the recorded birth time is. Set options.sensitivityMinutes on a kundali request and the response lists any change of the D1 lagna sign, D9 lagna sign, Moon nakshatra or Moon pada within that many minutes, with the instant of each change. The engine’s tests check that each changesAt falls within 1.5 seconds of the change in the engine’s own positions. Compared with a reference, the position differences above move these instants slightly: a 4.7″ lagna difference is about 0.3 seconds of time, and a 4.2″ Moon difference about 8 seconds. See Kundali.

Version 1 of the golden corpus is public: 500 of the compared births, with the request, our output, the Swiss Ephemeris reference values and the difference for each. 400 cases are spread evenly through the random births and 100 through the edge cases.

  • Corpus: golden-corpus-v1.json (JSON, 1.7 MB, published 2026-10-21, engine 0.1.0)
  • Summary: golden-corpus-v1.summary.json, with the worst case, 99th percentile, target and hard limit of every quantity above, the sample size and seed, and the SHA-256 of the corpus file

The current engine is a pre-release (engineVersion 0.1.0), and its results may still change within the tolerances above. When an engine release changes the results, a new corpus version will be published and announced in the changelog.

The file is one JSON object. The top level describes the run:

Field Meaning
version, published Corpus version and publication date.
engine engineVersion, the source commit, deltaTModelVersion, and the versions of the ephemeris library, time zone library and tzdb.
reference The reference (Swiss Ephemeris 2.10.03), the SHA-256 of each ephemeris file it used, and the conventions in plain words.
sample The seed, the number of cases, how they were picked and the size of the full run.
cases The 500 cases.

Each case has these fields:

Field Meaning
id R0000 to R4999 for random births, E-… for boundary edge cases, TZ-… for clock-change cases.
kind random-place, city:<name>, or edge:<type> such as edge:moon-nakshatra or edge:timezone.
request The body to send to POST /v1/kundali: birth (calendar, date, time, place, sometimes utcOffset) and options (ayanamsha, nodeType, and disambiguation or nepalHistoricOffset when the case needs them).
resolved instantUtc, tzid and utcOffset: the UTC instant every value below was computed for.
engine Our output: ayanamsha in degrees; lagna and grahas.<id> (sun, moon, mercury, venus, mars, jupiter, saturn, rahu, ketu), each with sidereal longitude in degrees and 1-based sign, nakshatra, pada and d9 (D9 sign); 1-based tithi and yoga; and vimshottari, the maha dasha periods with lord, start and end.
reference The same fields from Swiss Ephemeris. Longitudes have 6 decimal places; the signs, nakshatras, padas, tithi and yoga are derived from them the same way.
deltaArcsec Engine minus reference in arcseconds, signed, for each graha (Rahu stands for both nodes), the lagna and the ayanamsha.
  1. Download the corpus and check its SHA-256 against the summary.

  2. For each case, compute positions in your own reference at resolved.instantUtc, at request.birth.place, with the conventions above: geocentric apparent, true equinox of date, Lahiri including nutation, mean node, lagna as tropical ascendant minus ayanamsha. Use resolved.instantUtc directly so time zone handling does not mix with the astronomy.

  3. Compare each longitude with reference and with engine on a circle, so 359.9999° and 0.0001° are close:

    const diffArcsec = (a, b) => ((((a - b) % 360) + 540) % 360 - 180) * 3600;
  4. Compare the discrete fields exactly, and apply the boundary rule above to any mismatch.

  5. To check the live API, send each request to POST /v1/kundali. With the same meta.engineVersion as the corpus, the longitudes should match engine. If subject.instantUtc differs from resolved.instantUtc, the difference comes from time zone data, not from the astronomy.

Without the corpus, you can compare any of your own results the same way:

  1. Record the full request and the response’s meta.
  2. Configure your reference to the same conventions: the same ayanamsha (meta.ayanamsha.id; compare meta.ayanamsha.degrees first), node type, whole-sign houses and the UTC instant from subject.instantUtc.
  3. Compare longitudes in arcseconds, and discrete fields with the boundary rule above.

Most disagreements turn out to be a different time zone offset (check subject.utcOffset and offsetSource), a different ayanamsha, or the true node against the mean node.

If a result is outside the tolerances, write to hello@navapatra.com with the request, the response’s meta and x-request-id, and your reference values. We aim to fix confirmed accuracy bugs within 7 days; see Reporting wrong results in the Terms.