from life import experience as wisdom

~/posts/Vedic calendar from first principles

<<< 2025/Oct/01 · tools, astronomy, hobbies >>>
Computing tithi, nakshatra, yoga and karana from planetary positions using VSOP87 theory.

The problem with lookup tables

I wanted to understand how the five elements of the Vedic calendar are derived from actual planetary positions, not look them up in a table someone else computed. Every panchanga app and website gives you today's tithi and nakshatra. None of them show the math.

I came across this gap while building drikganit. The existing Python panchanga libraries either wrapped PyEphem around pre-computed tables or covered a fixed date range. None of them let me trace a tithi back to the raw sun-moon elongation that produced it. I needed to know whether the output was right, which meant I needed to be able to reproduce it from the planetary positions directly.

The panchanga (five limbs) describes each day through five attributes: tithi (lunar day), vara (weekday), nakshatra (lunar mansion), yoga (sun-moon angular combination) and karana (half-tithi). All five derive from the positions of the Sun and Moon against the sidereal zodiac. If you can compute solar and lunar longitude for a given moment, you can compute the entire panchanga from scratch.

Solar position with VSOP87

VSOP87 (Variations Seculaires des Orbites Planetaires) is a planetary theory developed by Pierre Bretagnon at the Bureau des Longitudes. It expresses planetary positions as trigonometric series with thousands of terms, each term contributing a periodic correction to the mean orbital elements.

For the Sun's apparent longitude, VSOP87 gives Earth's heliocentric position, which you invert to get the geocentric solar longitude. The full series has several thousand terms. For panchanga accuracy (within a few arc-minutes), truncating to the first hundred or so terms is sufficient. I implemented the series in Python, reading the coefficients from the published tables.

Circular diagram showing Sun-Moon angular relationship and how it maps to tithis
Circular diagram showing Sun-Moon angular relationship and how it maps to tithis

Lunar position

The Moon's position is harder. Its orbit has significant perturbations from the Sun, Jupiter and the non-spherical shape of the Earth. I used the lunar mean anomaly approach with the major correction terms (evection, variation, annual equation) rather than a full lunar theory like ELP2000.

This introduces the main accuracy tradeoff. A full lunar ephemeris gives arc-second precision. The truncated approach gives arc-minute precision, which is sufficient for tithi boundaries (each tithi spans 12 degrees of angular separation) but can occasionally place a tithi boundary off by a few minutes of time.

From longitudes to panchanga

Let's have a look at how drikganit computes all five elements. Running it for a specific date shows the complete panchanga with transition times:

$ panchanga --when 2026-03-02 --where ujjain --table

Pañcāṅga:
  Tithi:       14/Caturdaśī (Śukla)  15/Pūrṇimā @ 17:56
  Nakṣatra:    9/Āśleṣā/4  10/Maghā/1 @ 07:52
  Vāra:        2/Somavāra
  Yoga:        6/Atigaṇḍa  7/Sukarman @ 12:19
  Karaṇa:      28/Vaṇija  29/Viṣṭi @ 17:56  30/Bava @ Mar-03/05:29

The arrow notation shows transitions within the vedic day (sunrise to sunrise). On this day, the tithi changes from Caturdaśī to Pūrṇimā at 17:56, the nakshatra changes from Āśleṣā to Maghā at 07:52 and the karana transitions twice.

The tithi computation is the simplest to trace through the code:

def calculate_tithi(sun_lon, moon_lon):
  # elongation (moon - sun)
  delta = (moon_lon - sun_lon) % 360

  # tithi index (1-30), each tithi spans 12°
  index = int(delta / 12) + 1

  # progress within tithi
  progress = (delta % 12) / 12 * 100

  paksha = "Śukla" if index <= 15 else "Kṛṣṇa"
  return {'index': index, 'name_iast': tithi_names[index - 1],
          'paksha': paksha, 'progress': round(progress, 1)}

Tithis 1-15 are Shukla Paksha (waxing, from new moon to full moon). Tithis 16-30 are Krishna Paksha (waning, from full moon to new moon). The transition points are Amavasya (new moon, 0 degrees separation) and Purnima (full moon, 180 degrees).

Nakshatra divides the sidereal zodiac into 27 equal segments of 13.333 degrees each. The Moon's sidereal longitude determines which nakshatra it occupies. This requires a sidereal correction (ayanamsha) since the ephemeris gives tropical longitudes. drikganit uses swe.set_sid_mode(swe.SIDM_LAHIRI): the Lahiri ayanamsha, which is the Indian government standard.

Yoga is computed from the sum of the Sun's and Moon's sidereal longitudes, divided into 27 equal segments. Karana is half a tithi, giving 60 karanas per lunar month, though only 11 karana names cycle through them.

Testing against reference panchangas

I compared drikganit's output against published panchangas from Drik Panchang and the Rashtriya Panchang published by the India Meteorological Department. For a sample of 365 days across 2024, tithi matched on 362 days. The three mismatches were at tithi boundaries where the transition time differed by 10-20 minutes, placing the tithi differently depending on the exact moment of calculation.

Nakshatra matched on 360 of 365 days, with similar boundary-timing explanations for the mismatches. This level of accuracy is acceptable for a calendar tool. It would not be acceptable for muhurta (auspicious timing) calculations where precision to the minute matters.

Output formats

drikganit produces three output formats. The default JSON output includes everything: graha positions, panchanga elements, transition times, muhurta windows:

$ panchanga --when 2026-03-02 --where ujjain | python3 -m json.tool | head -20
{
    "config": {
        "tool": "panchanga",
        "version": "1.0",
        "location": {
            "name": "ujjain",
            "lat": 23.1765,
            "lon": 75.7885,
            "alt": 494,
            "timezone": "Asia/Kolkata",
            "description": "Ujjain, Madhya Pradesh, IN"
        },
        ...
    },
    "results": [...]
}

The --csv mode produces one row per astronomical event (sunrise, graha rise/set, tithi transition, nakshatra transition), useful for feeding into other tools:

datetime,event,vara,paksha,tithi,nakshatra,yoga,karana,lagna,muhurta,masa_am,masa_pm
2026-03-02T06:47:53+05:30,sunrise,Somavāra,Śukla,Caturdaśī,Āśleṣā,Atigaṇḍa,Vaṇija,Kumbha,Rudra,...

The panjika tool reads from the panchanga cache and renders monthly calendars as SVG or PNG, with paksha color coding, moon phase symbols and festival markers. The --table mode gives a quick terminal overview:

$ panjika --when 2026-03 --where ujjain --table
Ujjain, Madhya Pradesh, IN  March 2026

Daily:
  Day  Vara          Paksha/Tithi     Nakshatra
    1  Ravivāra      Ś.13/Trayodaśī   8/Puṣya
    2  Somavāra      Ś.14/Caturdaśī   9/Āśleṣā
    3  Maṅgalavāra   Ś.15/Pūrṇimā     10/Maghā
    ...

Vrata/Parva:
  Mar-03  Pūrṇimā · Holī · Dola Pūrṇimā · Phālguna Pūrṇimā
  Mar-19  Amāvasyā · Phālguna Amāvasyā
  Mar-27  Rāma Navamī

The tool computes everything from the date input alone. No external API calls, no network dependency. Swiss Ephemeris data files are cached locally. Run it offline, get a panchanga.

Working through the derivation made something clear: the five elements are not five separate systems. They are four sampling functions applied to two quantities — solar longitude and lunar longitude — at different scales. Once you see that, the whole calendar collapses into something much simpler than the lookup tables suggest.