Technical Notes — How withAshoka Calculates a Chart
This is the engineering companion to Our Approach. Where the Approach page tells a knowledgeable student what we do, this page shows how — formulae, flag values, file pointers, edge cases. It is written for developers, auditors, and Jyotishis who want to verify the maths against their own implementation.
All path references are relative to the repository root. The active
implementation is src/implementations/ashoka/backend/.
Stack
| Layer | Choice |
|---|---|
| Ephemeris | Swiss Ephemeris via pyswisseph 2.10.3.2 |
| Ephemeris files | semo_18.se1, sepl_18.se1, seas_18.se1 (cover 1800–2400 CE) |
| Ayanamsa | Lahiri / Chitrapaksha (SIDM_LAHIRI), applied manually |
| House system | Whole-Sign (Sampoorna Bhava), with Bhava Chalit (equal-house) and Placidus also computed |
| Node convention | Mean node (swe.MEAN_NODE); Ketu = Rahu + 180° |
| Time base | Julian Day (UT) computed from local civil time + IANA timezone |
Planet calculations request geocentric tropical positions plus daily
motion from the Swiss Ephemeris (the .se1 files cover 1800–2400 CE), then
subtract the ayanamsa ourselves to obtain sidereal longitudes.
Lagna (Ascendant)
The Lagna is the most error-prone calculation in any Jyotish stack. We compute the ascendant in pure Python rather than delegating to pyswisseph's house functions — this keeps every step transparent and auditable end-to-end and side-steps a global-state pitfall in the library's sidereal house routines. The pipeline:
1. Greenwich Mean Sidereal Time (IAU 1982). Given Julian Day jd:
T = (jd - 2451545.0) / 36525.0
gmst_seconds = 24110.54841
+ 8640184.812866 · T
+ 0.093104 · T²
- 6.2e-6 · T³
gmst_degrees = (gmst_seconds / 240 + 180) mod 360
2. Apparent Right Medium Coeli. With east longitude lng in degrees:
ARMC = (gmst_degrees + lng) mod 360
3. Tropical ascendant. Using the obliquity of the ecliptic ε from
swe.calc_ut(jd, swe.ECL_NUT, 0):
ASC_tropical = atan2(
cos(ARMC),
-( sin(ARMC)·cos(ε) + tan(φ)·sin(ε) )
) mod 360
where φ is geographic latitude in radians.
4. Sidereal ascendant. ASC_sidereal = (ASC_tropical − ayanamsa) mod 360,
where ayanamsa = swe.get_ayanamsa_ut(jd) after swe.set_sid_mode(SIDM_LAHIRI).
5. Lagna sign + degree. The 30° band that contains ASC_sidereal is the
Lagna sign; the offset within that band is the Lagna degree.
Reference implementation: src/implementations/ashoka/backend/app/services/jyotish/chart.py:86-101.
The function returns a ChartDebug payload — julian_day, raw_armc,
tropical_asc, ayanamsa, sidereal_asc — so any output is reproducible
and auditable end-to-end.
Planet positions
For each graha:
xx, _ = swe.calc_ut(jd, planet_id, swe.FLG_SPEED)
trop = xx[0] mod 360
sid = (trop − ayanamsa) mod 360
retro = xx[3] < 0
Graha → planet ID mapping uses swe.MEAN_NODE for Rahu. Ketu is derived as
(rahu_lon + 180) mod 360 and is always treated as retrograde.
Combust (asta). BPHS orbs in degrees, separately for direct and retrograde motion. Sun, Rahu, and Ketu cannot be combust.
| Graha | Direct | Retro |
|---|---|---|
| Chandra | 12 | 12 |
| Mangal | 17 | 17 |
| Budha | 14 | 12 |
| Guru | 11 | 11 |
| Shukra | 10 | 8 |
| Shani | 15 | 15 |
Nakshatra and pada. Each nakshatra spans 360/27 ≈ 13.333°; each pada
spans 360/108 ≈ 3.333°. Pada index within the nakshatra is
int((lon mod nak_span) / pada_span) + 1.
Houses
We compute three house systems in parallel and use them for different purposes:
- Whole-Sign (Sampoorna Bhava) — primary. The Lagna sign is the first house, the next sign the second, and so on. This is the Parashara default and the system Ashoka's reading is keyed off.
- Bhava Chalit (equal-house, 30° from exact Lagna) — used to flag planets that are in one whole-sign house but, by exact longitude, fall into the next or previous house. Useful when the Lagna degree is near a sign boundary.
- Placidus — included for cross-reference with Western tools.
Divisional charts (Vargas)
Each varga is computed by mapping a planet's longitude λ (sidereal) into
a divisional sign. We implement the BPHS Parashara method throughout.
| Chart | Division | Method |
|---|---|---|
| D1 Rāśi | 1× | λ directly |
| D7 Saptamśa | 7× | Equal 7-fold split per sign, parity of source sign decides starting sign |
| D9 Navāmśa | 9× | Equal 9-fold split, starts from sign of same element (movable/fixed/dual) |
| D10 Daśāmśa | 10× | Equal 10-fold split, odd signs from self, even from 9th |
| D12 Dvādaśāmśa | 12× | Equal 12-fold split, starts from the sign itself |
| D16 Ṣoḍaśāmśa | 16× | Equal 16-fold split, starts from Aries / Leo / Sagittarius depending on movable / fixed / dual |
| D20 Vimśāmśa | 20× | Equal 20-fold split, similar starting rule |
| D30 Triṃśāmśa | unequal | BPHS unequal division — 5°/5°/8°/7°/5° per sign with fixed lordship per slot |
| D60 Ṣaṣṭyāmśa | 60× | Equal 0.5° divisions, named per BPHS |
The D30 deserves emphasis: many implementations use a simplified equal 6-division scheme. We implement the unequal BPHS scheme — Mars, Saturn, Jupiter, Mercury, Venus get fixed degree-bands (5/5/8/7/5°) within each 30° sign, and odd vs. even signs reverse the order.
The D60 advances every 0.5° of source longitude, i.e. roughly every two minutes of clock time. We surface a warning to the user when the recorded birth time is rated less than "exact" before exposing D60 outputs.
Shadbala
Six classical strengths per planet, summed per BPHS:
- Sthana Bala — positional (uccha, saptavargaja, ojha-yugma, kendra, drekkana)
- Dig Bala — directional (planet's preferred kendra)
- Kala Bala — temporal (nathonatha, paksha, tribhaga, abda/maasa/vaara/ hora, ayana, yuddha)
- Cheshta Bala — motional (proper motion vs. mean motion)
- Naisargika Bala — natural strength (fixed table per BPHS)
- Drig Bala — aspectual
Shadbala scores are computed for all seven classical planets. Rahu and Ketu are excluded from Shadbala per Parashara convention. Output units are virupas (1 rūpa = 60 virupas); display in the chart UI normalises against the BPHS minimum-strength thresholds per planet.
Ashtakavarga
We compute the full Ashtakavarga system:
- Bhinnāṣṭakavarga — per planet, the bindu (point) contribution from each of the eight contributors (the seven classical planets + the Lagna) to each of the twelve signs. Sums to a 12-vector of bindus per planet.
- Sarvāṣṭakavarga — sum of the seven planetary bhinna vectors, giving the cumulative strength of each sign.
The Kakshya contributor table — which contributor adds a bindu in which sign relative to the source planet — is taken from BPHS and applied directly.
Per-Kakshya (sub-division) breakdowns are computed but not yet exposed in the chart UI; rollout is staged with the divisional charts.
Dasha
Vimshottari (120-year cycle, primary). Mahadasha lord is determined
from the natal Moon's nakshatra; the balance at birth is (nakshatra_span − moon_offset_in_nakshatra) / nakshatra_span × dasha_years. We compute
Mahadasha → Antardasha → Pratyantardasha for the user's lifetime and
identify the active level for the reading prompt.
Yogini (36-year cycle). Nakshatra-based, with the eight Yoginis cycling through the nakshatra series. Calculated; display rolling out.
Chara (Jaimini, sign-based). Lordship of the sign cycles per Jaimini rules. Calculated; display rolling out.
Yogas
The following yogas are detected algorithmically from chart data:
- Raj Yoga (kendra-trikoṇa lord conjunction or mutual aspect)
- Dhana Yoga (2nd / 11th lord configurations)
- Pancha Mahapurusha — Ruchaka, Bhadra, Hamsa, Malavya, Sasa
- Gaja-Kesari (Jupiter in kendra from Moon)
- Viparita Raja Yoga (3rd / 6th / 8th / 12th lord interchanges)
- Neecha Bhanga Raja Yoga (debilitation cancellation, see below)
- Kemadruma (Moon isolation)
When a significant yoga is present its label and definition are passed to the reading prompt so Ashoka references it explicitly. The library is expected to grow over time; absence of a yoga in this list is an implementation gap, not a denial of the yoga.
Neecha Bhanga (debilitation cancellation)
A debilitated planet's debilitation is treated as cancelled if any of the four BPHS conditions hold:
- The lord of the debilitation sign is in a kendra from the Lagna or Moon.
- The planet that would be exalted in the debilitation sign is in a kendra from the Lagna or Moon.
- The dispositor of the debilitated planet is exalted.
- The debilitated planet is conjunct or aspected by its dispositor or by the planet exalted in that sign.
When any condition is satisfied we annotate the planet as Neecha Bhanga and expose the cancellation reason; the reading then treats the planet's effect per the Neecha Bhanga Raja Yoga rather than as a straight debilitation.
Hub Highlights — natal detection & scoring
The personal hub (/your-chart) curates a ranked handful of features rather
than dumping the whole chart. A library of deterministic detectors each emits a
candidate carrying a significance score; the top few are kept and handed to
the reading prompt — the narration never decides what is notable. Reference:
app/services/jyotish/.../api/v1/hub.py (_CONDITIONAL_BUILDERS,
NATAL_HIGHLIGHT_WEIGHTS, _select_conditional_highlights).
Detectors (base weights in NATAL_HIGHLIGHT_WEIGHTS): dignity
(exalted / moolatrikona / own / debilitated), notable yoga, functional malefic,
stellium (3 / 4 / 5+), conjunction, yogakaraka (+ strong-placement bonus),
vargottama (lagna / graha), the Rahu–Ketu axis (conjunct / angular), retrograde,
combust. The two always-on leads — rising sign and Moon — sit above the scored
set and are not ranked.
Placement intensity (karmic load)
A cross-cutting signal — app/services/jyotish/intensity.py,
placement_intensity(graha, sign, degree, house, nakshatra) — scores how
karmically loaded a placement is and feeds both the ranking and the narrative:
| Signal | Test | Bonus |
|---|---|---|
| Gandanta | within 0.8° of a water→fire sign boundary (end of Karka / Vrishchika / Meena, or start of Simha / Dhanu / Mesha) | +12 |
| Knotty nakshatra | Ketu-ruled {Ashwini, Magha, Mula} ∪ gandanta-junction {Ashlesha, Jyeshtha, Revati} (Rahu-ruled nakshatras, incl. Ardra, excluded by design) | +6 |
| Near-exact dignity | within 2° of the exact exaltation / debilitation degree (nodes have no exaltation degree, so this never fires for Rahu/Ketu) | +8 |
| Moksha dusthana | 8th or 12th house (the 4th is a kendra of inner peace — excluded) | +6 |
Capped at +20 per graha. The bonus lifts the owning highlight's rank; the
human-readable reasons plus a flavour (knot / strain / depth /
reinforcement) are appended to the deterministic chart_language so the
narration names the load with the right verb. The Moon highlight (always shown)
takes the narrative enrichment only.
Node gate. _detect_nodes also admits a node at a gandanta from any
house. Moksha houses (8/12) are deliberately not a standalone gate: because
the nodes are always opposite, every node pair already touches
{kendra ∪ trikona ∪ 8/12}, so admitting 8/12 alone would fire for every
chart. A moksha-house node still earns the +6 bonus when it surfaces.
The karmic-weight meta-line
A chart is flagged as carrying "unusual karmic weight" — one sentence opening
the newcomer Quick read — when either (a) two or more placements score
≥ 14, or (b) one placement is extreme (a gandanta, or near-exact dignity
≤ 1°) on a soul- or karma-bearing point: the Moon, the Lagna, the Atmakaraka,
or a node. Reference: _chart_intensity_meta in hub.py. Calibrated over the
real charts + a 2,000-chart synthetic sample to fire ~11% of the time (a ≥ 10
floor fired ~18% — too generous); the per-graha weights leave the top-4
membership displacement at ~10%. Full spec and calibration:
documents/domains/jyotish/placement_intensity_spec.md.
These signals also surface on the The Method tab of /your-chart — a
static, novice-friendly account of how a reading is derived.
Panchang
Computed in real time from the user's location, not pulled from an almanac.
The five elements are computed at sunrise (udaya) to give the day's
primary values — the conventional panchang reckoning, and the same instant
our calendar engine (event_scanner.py) uses, so the /panchang and
/calendar pages agree on a day's limbs. Intra-day transitions and the
muhūrta windows are computed separately. All longitudes are sidereal
(Lahiri). (Until #150 the headline was sampled at solar noon, which diverged
from Drik whenever a tithi/nakshatra boundary fell between sunrise and noon.)
| Element | Computation |
|---|---|
| Tithi | ((Moon_lon − Sun_lon) mod 360) / 12 → index 0–29 (Shukla 0–14, Krishna 15–29) |
| Vara (weekday) | Day of week from local civil date — Sun=0 .. Sat=6 in Sanskrit ordering |
| Nakshatra | Moon_lon / (360/27) → index 0–26, plus pada within nakshatra |
| Yoga (Panchang) | ((Sun_lon + Moon_lon) mod 360) / (360/27) → index 0–26 |
| Karana | ((Moon_lon − Sun_lon) mod 360) / 6 → index 0–59; 7 moveable karanas cycle 56 times then 4 fixed karanas close the lunation |
Sun & Moon — rise / set / solar noon
swe.rise_trans(jd_search, body, rsmi, geopos) for body ∈ {SUN, MOON} and
rsmi ∈ {CALC_RISE, CALC_SET, CALC_MTRANSIT}. Search starts at local
midnight so the same-day events are caught even when sunrise precedes 06:00.
Moon events that fall on the following local date (when the Moon already
rose/set before today's midnight) are clamped to null.
Inauspicious time windows — Rahu Kaal, Yamaganda, Gulika
Each is a single segment of the eight-fold division of the daylight span
(sunset − sunrise). The weekday-indexed segment number is fixed by BPHS:
| Weekday (Mon=0..Sun=6) | Rahu Kaal | Yamaganda | Gulika |
|---|---|---|---|
| Mon | 2 | 4 | 6 |
| Tue | 7 | 3 | 5 |
| Wed | 5 | 2 | 4 |
| Thu | 6 | 1 | 3 |
| Fri | 4 | 7 | 2 |
| Sat | 3 | 6 | 1 |
| Sun | 8 | 5 | 7 |
Window = [sunrise + (seg − 1) · daylight/8, sunrise + seg · daylight/8].
All three rows are verified to the minute against Drik Panchang. (The
Yamaganda row was off by one segment on every weekday until #150; the
corrected values above replace the earlier table.)
Abhijit Muhurta
The 8th of fifteen equal daytime muhūrtas:
[sunrise + 7·D/15, sunrise + 8·D/15] where D = sunset − sunrise (so the
window is ~48–54 min and straddles solar noon, widening with daylight
length). This matches Drik Panchang's definition; an earlier fixed
solar-noon ± 12 min (24 min) window was centred correctly but materially
narrow, corrected in #150.
Choghadiya — day & night
Eight day periods (sunrise → sunset) and eight night periods
(sunset → next sunrise), each labelled by name (Amrit / Shubh / Labh /
Char / Udveg / Rog / Kaal — seven unique names cycling, slot 8 repeats
slot 1), planetary lord, and quality.
| Lord | Quality | |
|---|---|---|
| Amrit | Moon | auspicious |
| Shubh | Jupiter | auspicious |
| Labh | Mercury | auspicious |
| Char | Venus | neutral |
| Udveg | Sun | inauspicious |
| Rog | Mars | inauspicious |
| Kaal | Saturn | inauspicious |
The first slot of each half-day rotates through the cycle by weekday:
| Weekday (Mon=0..Sun=6) | Day first | Night first |
|---|---|---|
| Mon | Amrit | Char |
| Tue | Rog | Kaal |
| Wed | Labh | Udveg |
| Thu | Shubh | Amrit |
| Fri | Char | Rog |
| Sat | Kaal | Labh |
| Sun | Udveg | Shubh |
Element transitions across the day
For each of Tithi / Nakshatra / Yoga / Karana we find every boundary the
sidereal index crosses between local midnight and the next local midnight.
The algorithm samples the index every 30 minutes (the slowest element —
Karana — changes ~every 11 hours, so this is generously dense), and any
sample-to-sample index change is bisected to ~1-second precision. The
output is a list of (name, index, start, end) segments covering the whole
local day; the first segment's start is local midnight and the last
segment's end is the next local midnight.
Named yoga detection
A YogaFlag carries a name, a sense (auspicious / inauspicious), and a
window. We detect:
- Sarvartha Siddhi (auspicious) — fires when the Vara × Nakshatra combination matches the BPHS table, e.g. Sun + Hasta, Mon + Rohini, Wed + Krittika. Window = the matching nakshatra's segment.
- Amrita Siddhi (auspicious) — the strongest single Vara × Nakshatra combination per day (Sun + Hasta, Mon + Mrigashira, Tue + Ashwini, Wed + Anuradha, Thu + Pushya, Fri + Revati, Sat + Rohini).
- Vyatipata (inauspicious) — fires whenever the Panchang Yoga is Vyatipata (index 16). Window = that yoga's segment.
- Vaidhriti (inauspicious) — fires whenever the Panchang Yoga is Vaidhriti (index 26).
- Bhadra (inauspicious) — fires whenever the Karana is Vishti. Window = that karana's segment. Bhadra is one of the most consequential inauspicious flags for muhūrta — weddings, journeys, and major undertakings are traditionally postponed.
- Panchaka (inauspicious) — fires when the Moon transits Dhanishta through Revati (nakshatra indices 22–26). Whole-nakshatra approximation; the strict tradition starts mid-Dhanishta.
- Rikta Tithi (inauspicious) — fires on Chaturthi, Navami, and Chaturdashi of either fortnight (tithi indices 3, 8, 13, 18, 23, 28).
When multiple flags fire, the deterministic guidance composer picks the highest-priority auspicious flag (Amrita Siddhi > Sarvartha Siddhi) and the highest-priority inauspicious flag (Vyatipata > Bhadra > Vaidhriti > Panchaka > Rikta Tithi) and joins their Ashoka-voice template lines for the home-slide guidance string.
Personalisation — Taarabala, Chandrabala, and the timing bar
The /panchang page opens with two layers computed on-the-fly per request
(no cache row, no migration) in services/jyotish/personal_panchang.py.
The personal block weighs today's sky against the signed-in user's
natal Moon. The natal Moon comes from the user's primary chart; today's
transiting Moon sign is sampled at sunrise. The two classical measures —
Taarabala (nine-fold cycle counted from the birth Nakshatra) and
Chandrabala (house of the transit Moon counted from the natal Moon
sign) — are computed by the canonical functions in compatibility.py
(single source of truth); personal_panchang.py owns only the
Panchang-page presentation: an evocative gloss per tara, a life-area
phrase per Chandrabala house, a three-way display sense
(favourable / unfavourable / neutral), a deterministic no-Claude
paragraph in Ashoka's voice, and chips. With no birth time on file the
block silently degrades to {available: false} and the page falls back
to the shared summary prose.
The timing bar is a four-band timeline
(favourable / neutral / caution / avoid) tiling the local civil day. It
has two modes. With no birth time on file it is impersonal:
score_timeline (in personal_panchang.py) collects the day's
Choghadiya segments and the Rahu Kaal / Yamaganda / Gulika windows plus
Abhijit, breaks the day at every segment boundary, and paints each slice
by the band at its midpoint under a fixed precedence: Abhijit
(favourable, dispels an overlapping inauspicious window) > Rahu Kaal /
Yamaganda / Gulika (avoid) > Choghadiya quality (auspicious→favourable,
inauspicious→caution, neutral→neutral) > rest (neutral, outside
sunrise→sunset). Adjacent slices with the same band and label merge.
When a birth time is on file, the same bar is re-scored per user by
services/jyotish/personal_muhurta.py (score_personal_timeline), and
/panchang flags it timing_bar.personal = true. The day is divided
into 24 unequal planetary horas — sunrise→sunset and
sunset→next-sunrise each split into twelve, classical and consistent with
the Choghadiya day/night reckoning — the first hora of the day ruled by
the weekday lord and advancing through the Chaldean sequence
(_HORA_CYCLE in strength.py, the single source of truth). Each hour
is scored under a tiered rule chosen for explainability:
- Tier 1 — hard overrides. Rahu Kaal / Yamaganda / Gulika and Vishti (Bhadra) karana force avoid; Abhijit forces favourable and dispels overlaps. These rule outright, ahead of any personal weight.
- Tier 2 — Chandrabala base lean. The transit Moon's house from the
natal Moon sets the hour's underlying lean (favourable / neutral /
caution). The Moon moves ~13°/day, so at most one sign change per day;
_moon_sign_spansresolves the crossing by linear interpolation (≤ 2 spans) rather than per-hour ephemeris calls. - Tier 3 — Hora, Choghadiya, Taarabala tiebreak. The hora lord is
weighted against the user's ascendant via
functional_nature(lagna)(dignities.py): a functional benefic or yogakaraka nudges +1, a functional malefic −1, else 0. The shared Choghadiya quality and Taarabala add their own ±1. Their sum shifts the base lean up or down a band; the deciding minor factor is named in the per-segmentreason.
Scoring covers sunrise→midnight (day + night horas); the pre-dawn stretch
(midnight→sunrise) stays neutral "Rest", matching the impersonal bar. The
block degrades silently to {available: false} (impersonal fallback)
when there is no profile, no birth time, or unusable coordinates — never
a guess. Adjacent slices with the same band, label and reason merge.
The daily verdict (#154) rolls today's bar up to one headline atop the
personal block. pm.roll_up_day(timeline) keys the verdict on how much of
the day scores personally favourable (favourable / steady / mixed /
caution) — the pre-dawn "Rest" contributes no favourable minutes, so it
can't dominate, and the daily hard-avoid windows aren't penalised. Because
it rolls up the same segments the bar draws, the headline can't
contradict the band beneath it. pp.compose_day_verdict adds a
deterministic, no-Claude reason naming the dominant drivers (the Moon's
house lean, the taara, and the best window's time of day). It attaches to
the personal block and silent-degrades with it (no birth time → no
verdict).
The multi-day outlook (#155 — the "Days ahead" tab) generalises this
to a range. For each of the next N days (7 by default, up to 30) it
computes that day's astronomy directly via compute_full_panchang (no
cache row, no Claude prose), scores it with the same
score_personal_timeline, and rolls the day up to one verdict with
roll_up_day (personal_muhurta.py): favourable / steady / mixed /
caution, keyed on how much of the day scores personally favourable, with
the longest favourable segment surfaced as the day's best window. The
natal chart is resolved once; the per-day ephemeris helpers acquire
SWE_LOCK internally and are not re-wrapped (the #151 non-reentrant-lock
rule). Served by GET /panchang/outlook — verified-gated, computed on
the fly, never cached — and each day links through to its full personal
Panchang. roll_up_day is the single per-day rule shared by the daily
verdict (#154) and this range outlook.
Accuracy — audit against Drik Panchang (2026)
The panchang engine was cross-checked against Drik Panchang
(same Lahiri ayanamsa) over a 10-cell matrix chosen to hit 2026 edge cases —
Adhik Jyeshtha, Amavasya, Purnima, a Kshaya tithi boundary, and a Sankranti
month change — across Ahmedabad, London, New York, Dubai and Auckland. Our
side was computed by running compute_full_panchang on the live backend.
Verified correct (to ~1 minute): sunrise/sunset; all five limbs at sunrise and all four limb transition times (Kimstughna-at-index-0 karana fix from #112 holds); Rahu Kaal; Yamaganda; Gulika Kaal; Abhijit (8th daytime muhūrta); Choghadiya (day and night); and Adhik Jyeshtha detection with correct Amanta month boundaries.
The three defects the 2026 audit surfaced — solar-noon vs sunrise limb sampling (HIGH), the Yamaganda off-by-one (MEDIUM), and the Abhijit window width (LOW) — were all corrected in #150.
Intentional, not bugs: we surface Vikram Samvat under Kartak-adi (Gujarati) reckoning (2082 in mid-2026) where Drik shows Chaitra-adi 2083 with a Jovian year name — a documented audience choice (see Calculation conventions). Moonrise/moonset agree with Drik to ~4–6 min (a horizon/limb/parallax convention difference; sun events are <1 min).
Sky Guide (transits + visibility)
The Sky Guide uses swe.azalt() to convert each visible planet's geocentric
longitude into local altitude/azimuth from the user's current GPS location
and clock time — not the natal location. We then layer the natal connection
onto the live sky:
- Lagna lord and current Mahadasha lord are tagged when visible.
- Each visible planet is mapped to the natal house it currently transits.
- Each visible planet's current nakshatra is shown.
- A planet within a few degrees of culmination (highest altitude in the sky) is flagged — classical sources treat culmination as a moment of peak strength, similar in spirit to digbala.
Today's Reading (gochar engine)
/your-chart/today reads today's sky against the user's natal chart.
Auth-gated: logged in and verified, mirroring /panchang. Each day's
reading is persisted so past days can be revisited from the history drawer
or by date at /your-chart/today/<date>.
Transit positions. We re-use the natal pipeline, replacing the birth
moment with datetime.now(UTC) and skipping the ascendant calculation
(transits are graha-relative, not place-relative). Same Lahiri ayanamsa,
same FLG_SPEED flag pattern, same Mean Node + Ketu = Rahu + 180° convention.
Reference: src/implementations/ashoka/backend/app/services/jyotish/gochar.py.
Natal-house overlay. Each transit graha is mapped into a whole-sign
natal house: (transit_sign_idx − natal_lagna_sign_idx) mod 12 + 1. This
matches the natal house system used throughout Your Chart (e.g. the Kundli
diagram on /your-chart/chart-shape).
Per-graha signature flags. For each transit we record:
dignity—exalted,debilitated,own, ornone(re-uses the same exaltation tables as the nataldignitiesmodule).retrograde— fromxx[3] < 0of the speed-onlycalc_ut.aspects_lagna— true when the transit graha sits in the natal lagna sign (1st) or its opposition (7th). v1 only flags these two; full Vedic aspects (3, 7, 10 for Mangal; 5, 7, 9 for Guru; 3, 7, 10 for Shani; etc.) are intentionally not surfaced yet.sade_sati_phase— set only on Shani, via the existingsade_sati()helper incompatibility.py.in_natal_sign— return-to-natal-sign signature, requires the natal graha → sign map to be passed in.natal_lord— true when the transit graha is the lagna lord.
"Three loudest" — v1 heuristic. We always surface Shani, Guru, Rahu — the three slowest movers, with the longest narrative arcs. This is deliberately fixed for v1; a configurable scoring rule (factoring in dignity, return-to-natal, lagna-lord transit, Sade Sati phase, aspects to the natal Moon) is tracked separately.
Karmic-timing window. _karmic_timing_lines (reading.py) adds an
optional note when one of the highlighted (slow) transits is either itself
crossing a gandanta now, or moving through the sign of a natal karmic
anchor — a natal graha at a gandanta, or a node — i.e. that natal knot is
being activated. Restricted to the highlighted transits so it stays coherent
with the three insights; usually empty; fed into the reading prompt as a
"KARMIC WINDOW" line for Ashoka to weave in (timing and disposition only, never
prediction). This is the transit-side companion to the natal placement-intensity
signal (see Hub Highlights — natal detection & scoring).
Today's Gita verse. Re-uses select_verse(lagna_sign, moon_sign) but
passes the transiting Moon sign, so the verse rotates daily while staying
themed to the user's lagna.
Caching. Each daily reading is persisted, keyed per user, profile and
date. The "Go deeper" Ashoka passages on the same page are cached per focus
area (career, relationships, spiritual, health, wealth, family).
The "today" date is computed in the profile's timezone, so the reading
rotates at the user's local midnight.
AI generation. The hero one-liner, the three insights, the Ashoka
commentary on the verse, and each "Go deeper" passage are generated by
claude-sonnet-4-6 from the structured gochar payload + a compact natal
summary. Returned as JSON, parsed, persisted.
Calendar / Almanac
The Vedic almanac at /calendar is a month-by-month view of computed
events plus their claude-sonnet-4-6-generated literary content.
What each event carries. One entry per emitted (event, day) pair;
multiple events on the same day stand as separate entries. Each records
its date, an event_type (festival / ekadashi / purnima / amavasya /
pradosh / sankranti / eclipse / transit), a name and any popular regional aliases (Pongal /
Lohri for Makar Sankranti, etc.), the tithi and nakshatra, the
deterministic facts that drove its prose (including tradition and region
tags), and the generated prose itself.
Spans. A festival-level entity for multi-day arcs: one per festival,
with member events resolving to specific years by their date. Eight spans
are currently seeded: sharad_navaratri (9 days), chaitra_navaratri (9),
pitru_paksha (15), ganesh_utsav (11), diwali_week (5), pongal (4),
onam (10), paryushan (8). Each carries a canonical_length (so a sparsely
tagged festival's day-popup reads "Ganesh Utsav · 11-day festival" rather
than the misleading "Day 1 of 2") and an AI-generated festival-level
overview shown in the day-popup's "About the festival" tab.
Scanner — app/services/calendar/event_scanner.py. Walks every
day in [start, end], samples noon UTC, computes the standard
astronomical context (tithi, nakshatra, sun sign, Hindu lunar month
under Amanta naming, Adhik Maas detection, Vikram Samvat year, ritu,
Gujarati aliases). Then iterates festival rules from festivals.yaml
(~138 entries) and dispatches by rule_type:
-
tithi_match(default) — fires on(hindu_month, paksha, tithi). Covers most named festivals (Diwali, Janmashtami, all Ekadashis, Pitru Paksha shraddhas, Navadurga days, etc.). Optionalobservance_momentselects the day-selection anchor:ekadashi_smarta,nishita,nishita_janmashtami,moonrise,arunodaya,pradosh,pradosh_amavasya,madhyahna_purnima,madhyahna_vyapini,aparahna,aparahna_conjunctive,follows,navami_merge(seedocuments/domains/jyotish/calendar_observance_conventions.mdfor derivations). Default issunrise(udaya-tithi). -
weekday_in_month— fires on every (or a specific) weekday inside a Hindu lunar month. Used by Shravan Somvar and Kartik Somvar (every Monday of those months becomes a Shiva vrat).occurrence: allis the v1 shipping value; Nth-occurrence variants documented as the extension point. -
solar_anchor— fires on a sun-sign-crossing date with an optionaloffset_days. The scanner precomputes the year's sankranti dates once into a lookup table; each rule check is O(1). Covers Vishu / Vaisakhi / Bohag Bihu / Pohela Boishakh (all anchor to Mesha Sankranti) and the four Pongal days (Bhogi −1, Thai Pongal 0, Mattu +1, Kaanum +2 from Makara Sankranti). -
nakshatra_in_solar_month— fires when the moon is in a target nakshatra AND the sun is in a target sidereal sign (i.e. inside a named Tamil / Malayalam solar month). Covers Onam's ten days (Atham → Thiruvonam inside Chingam, sun in Simha) and Karthigai Deepam (moon in Krittika, sun in Vrishchika). A per-solar-period "fired" tracker suppresses moon re-visits to the same nakshatra in the same solar window — Kerala convention is to celebrate the first occurrence.
Celestial events (#182) — app/services/calendar/celestial.py. After
the festival/lunar pass, the scanner appends two families of significant
celestial events, computed independently of festivals.yaml:
-
Eclipses (
event_type: eclipse) —pyswisseph'ssol_eclipse_when_glob/lun_eclipse_whenforward-search enumerates every solar and (non-penumbral) lunar eclipse in range. Per eclipse the facts carry the subtype (total / annular / hybrid / partial), the Delhi-reference magnitude and visibility (sol_eclipse_how/lun_eclipse_how, plus a moon-above-horizon check for lunar), the node on the eclipse axis (Rahu/Ketu), the eclipsed luminary's rashi and the Moon's nakshatra, and the Sutak window (start = first contact − 12h for solar, − 9h for lunar). Penumbral-only lunar eclipses are skipped (no Sutak, not observed — matches panchang practice). -
Transits / gochar (
event_type: transit) — a day-over-day sign-change walk (same noon-UTC sampling as Sankranti) over Mangal, Budha, Guru, Shukra, Shani, Rahu and Ketu (Surya's sign change is the Sankranti; Chandra is too fast). The slow grahas (Guru, Shani, Rahu, Ketu) emit on every sign change; the faster grahas (Mangal, Budha, Shukra) emit only when the ingress lands in their exaltation, debilitation, or own sign. The graha's dignity in the new sign (exalted/debilitated/own/moolatrikona/neutral, viadignities.py) rides in the facts — a dignity change is always an ingress, so it is a fact on thetransitrow, not a separate event_type.
Comets and other Saṃhita (omen) phenomena are out of scope — they are not in the planetary ephemeris and sit outside the nine-graha tradition.
Sankranti naming. The twelve sun-sign-crossing emits use canonical
Sanskrit short-form names (Makar / Mesh / Karka / etc., from a
_SANKRANTI_NAMES map) and the three culturally heaviest carry
alternate_names: Makar Sankranti → Pongal, Lohri, Uttarayana; Mesh
Sankranti → Vishu, Vaisakhi; Karka Sankranti → Dakshinayana.
Tradition + region tagging. Every rule carries tradition
(hindu / sikh / jain / buddhist, default [hindu], multi-valued
allowed e.g. Vaisakhi = [hindu, sikh]) and region (all-india /
tamil / malayalam / punjabi / bengali / marathi / gujarati /
assamese / odia / kannada / telugu, default [all-india]). The
calendar API surfaces both and lets the user filter the month by them.
Per-tradition principle for shared dates. When two traditions observe distinct festivals on the same calendar date, each emits as its own event row rather than collapsing into a composite. Diwali night (Ashwin Krishna 15) produces six rows: Naraka Chaturdashi, Diwali · Lakshmi Puja, Ashwin Amavasya (the recurring lunar marker), Bandi Chhor Divas (Sikh), Mahavir Nirvana (Jain), Kali Puja (Bengali). Same principle for Ram Navami + Swaminarayan Jayanti on Chaitra Shukla 9, Krishna Janmashtami + Hari Jayanti on Shravana Krishna 8, etc.
Generator. Deterministic facts are handed to claude-sonnet-4-6 with
few-shot examples covering the tonal range — both for per-event prose and
for the eight spans' festival-level overviews; output schema is validated,
bad output fails loudly.
Personalisation. A pure function (event_date, event_type, profile) → footer | None. For Purnima/Amavasya it samples transit Chandra; for
Sankranti, transit Surya; for events without a clean rule it returns
nothing. The house is (transit_sidereal_sign_index − natal_lagna_sign_index) mod 12 + 1, and the wording is templated from a fixed house-meaning table —
no Claude call.
API. The calendar API serves public month metadata, a verified-only full payload (with a per-user personalisation footer per event), the next upcoming event, the multi-day span list (one result per span and year, surviving a filter only if at least one member event passes), and fuzzy search across event names and aliases (span hits first, so "Navaratri" surfaces the span card before its nine day rows). Admin tools regenerate prose per event, per month, or per span, and reseed a date range.
Freshness. A nightly job detects the forward edge of seeded data and scans the next missing month (capped at one month per run for predictable API spend), generating prose and skipping events that already exist. A health alert fires if a month's emission count drops sharply versus the same month a year earlier. Reseeds can backfill new structured fields onto existing rows without re-running Claude.
Verification. test_calendar_ground_truth.py cross-checks ~30
core Hindu festivals against DrikPanchang dates for 2025-2027.
test_festivals_emit.py (universal) asserts every yaml slug produces
at least one event in the same 3-year window. test_sankranti_renames.py
checks the Sankranti map applies correctly. A periodic Drik-diff
job + per-event YoY cron check + ground-truth backfill for the post-2026
content additions are tracked in #147.
Public teaching pages — SEO (/festivals, /learn)
Two open, prerendered page sets draw on this calendar data: a festival
library (/festivals/:slug) and a Jyotish/Panchang glossary
(/learn/:slug, covering core concepts, the nine grahas, and the
twenty-seven nakshatras). Festival pages carry multi-year dates and
schema.org Event markup; glossary pages carry DefinedTerm +
Article. Festival dates are presently hand-maintained from the
calculated calendar (a build-time refresh from the calendar API is a
follow-up).
These public routes are statically prerendered at build time: the
client SPA is rendered to real HTML per route with React's
renderToString, with per-route <title>/description/canonical/Open
Graph and JSON-LD baked into <head>, plus a generated sitemap.xml
and robots.txt. This is the indexability foundation (the SPA otherwise
served a near-empty shell to crawlers); the gated, personalised
chart/reading is excluded.
Ishta Devata
Derivation per the classical method:
- Atmakaraka. Among the seven classical planets, the one with the
highest degree within its sign (
floorof the within-sign offset). - Karakamsha. The sign occupied by the Atmakaraka in the Navamsa (D9).
- 12th from Karakamsha. Counted in the rāśi chart.
- Lord of that 12th sign → its presiding deity per the BPHS-aligned planet → deity mapping.
Where multiple deities are associated with a single planet we present the primary deity (e.g. Vishnu for Sun) and note variants.
The Gita Reader
Data. One table, gita_verses — chapter, verse, Devanagari sanskrit,
IAST transliteration, english (Swami Sivananda, via the GPL-3.0
vedicscriptures dataset; 701 rows under standard scholarly numbering),
keywords (5–8 per verse from a 56-term controlled vocabulary, tagged once
by claude-haiku-4-5), and ashoka_commentary (a 40–50-word reflection per
verse, pre-generated once by claude-haiku-4-5 in the Ashoka voice).
Nothing on the reader is generated at request time.
Endpoints. GET /api/v1/gita/chapters (18 chapters, English titles +
Sanskrit yoga names, verse counts), /chapter/{n}, /verse/{c}/{v},
/random, and /search?q= — the search resolves a verse reference
("2.47") first, then an exact theme keyword from the controlled vocabulary,
then a substring match over the English translation and transliteration.
Reader behaviour. /gita is the introduction / landing page (explains
the Gita, its place in the Mahabharata, our reading approach + the chapter
intros; Start-here / Search / Surprise-me; an 18-chapter table of contents;
prerendered for SEO). Verses live at /gita/:chapter/:verse — one verse per
card with previous/next navigation, left/right arrow keys, and the URL tracking
the verse (/gita/2/47) so any verse is shareable; a shareable branded card PNG
at /api/v1/gita/verse/{c}/{v}/card.png. Each verse shows its speaker
(KRISHNA teaches / ARJUNA asks / SANJAYA narrates), the chapter-opener band on
verse 1, ★ on the best-known verses, and a "verses that speak to this one"
strip. Guests read the full verse text; Ashoka's reflection is trimmed to its
first sentence until signed in. Bookmarks and the last-read position are stored
in the browser (localStorage) in v1 — server-side progress
(user_gita_progress) is the planned follow-up, and is what the School's
chapter badges will read from.
What we know we do not yet do
This list is the engineering view of the limitations summarised on Our Approach.
- User-selectable ayanamsa (Raman, KP, True Chitrapaksha) is not exposed.
- True node vs. mean node is not user-selectable.
- Prasna, Tajika, and Jaimini-only systems are not implemented.
- Yoga library is non-exhaustive.
- Per-Kakshya Ashtakavarga, Yogini and Chara Dasha display, and divisional charts beyond D9 / D60 are computed but not yet surfaced in the UI.
- Calendar date verification against external panchang sources (DrikPanchang and regional almanacs) is partial — the ~30-festival ground-truth set doesn't yet cover all of the post-2026 content additions (Tier 1 sect events, the Onam ten-day arc, Pongal sub-days). Tracked in #147.
- Tier 2 sect content (Lingayat, Sri Vaishnava, Pushtimarg, Sant traditions, Bengali Durga Puja arc) and Gregorian-date festivals (Mahant Swami Maharaj Jayanti) are parked in #148, awaiting prioritisation.
- The week-view grid binding strip doesn't graphically connect across week-row boundaries — a multi-day span that crosses a Sunday/Monday line reads as two visually disconnected segments. Accepted v1 limitation.
Cross-references
- Our Approach to Jyotish — the knowledgeable-student companion to this page.
withashoka.com