From 7142366dc4602922a5fbcf7c7bb81bd6559821a1 Mon Sep 17 00:00:00 2001 From: 732642856 <732642856@qq.com> Date: Tue, 14 Jul 2026 18:05:53 +0800 Subject: [PATCH] add auditable Swiss gulika calculator --- docs/research/pre_work_error_ledger.md | 1 + references/technique_registry.json | 11 ++--- scripts/gulika.py | 67 ++++++++++++++++++++++++++ tests/test_gulika.py | 17 +++++++ 4 files changed, 90 insertions(+), 6 deletions(-) create mode 100644 scripts/gulika.py create mode 100644 tests/test_gulika.py diff --git a/docs/research/pre_work_error_ledger.md b/docs/research/pre_work_error_ledger.md index 3963d1f2..a7e7297a 100644 --- a/docs/research/pre_work_error_ledger.md +++ b/docs/research/pre_work_error_ledger.md @@ -89,6 +89,7 @@ For large architecture or release work, also read: | ERR-056 | A WorkBuddy checkout of the same remote diverged substantially from the active source branch and can be mistaken for a mergeable mirror. | active | Read `whole_machine_fragment_sweep_2026_07_14.md`; do not copy or merge it without explicit commit-level review on a separate branch. | | ERR-057 | The release quality profile checked untracked files but did not execute the privacy AST scan or the real Chromium report-isolation probe. | mitigated 2026-07-14 | `release_hygiene_check()` now requires `public_release_privacy_scan.py --json` and `report_renderer_isolation_poc.py --strict`; parity manifest validation also runs as a contract check. | | ERR-058 | Formula-based Sahams used the day/night operand rules but omitted the documented zodiacal-order `+30°` exception. | mitigated 2026-07-14 | `_calc_formula_saham()` applies the `references/saham_rules.json` forward-arc condition and one-sign correction; keep external numeric oracle parity as a separate `partial` requirement. | +| ERR-059 | Gulika was either an approximate fallback or falsely implied as a chart module output. | mitigated 2026-07-14 | `scripts/gulika.py` computes Prasna Marga Ghatika segment Ascendant with Swiss sunrise/sunset and Lahiri sidereal houses; registry is `partial` and exposes only the actual calculator until external numeric parity and chart integration are complete. | ## Fragment Sweep Command Set diff --git a/references/technique_registry.json b/references/technique_registry.json index b461279e..20138857 100644 --- a/references/technique_registry.json +++ b/references/technique_registry.json @@ -420,7 +420,7 @@ "upagraha", "event" ], - "status": "blocked", + "status": "partial", "knowledge_refs": [ "references/prashna-complete-guide.md" ], @@ -430,17 +430,16 @@ "full-reading" ], "output_paths": [ - "modules.chart.upagraha", - "scripts/prashna.py:calc_gulika_simple" + "scripts/gulika.py:calculate_gulika" ], "audit_label": "Upagraha/Gulika/Maandi", - "missing_impact": "Sensitive malefic shadow points are unavailable, weakening pressure, delay and vulnerability interpretation.", + "missing_impact": "Swiss day/night and Prasna Marga Ghatika Gulika are available as supporting evidence; Maandi, other Upagrahas and external numeric parity remain unavailable.", "entry_type": "supporting_indicator", "evidence_role": "secondary", "user_visibility": "expert_audit", "verification_level": { - "calculation": "verified", - "rule": "verified", + "calculation": "partial", + "rule": "partial", "prediction": "support_only" }, "conclusion_policy": "Supporting evidence only; it can raise/lower confidence but cannot by itself decide an event or timing claim." diff --git a/scripts/gulika.py b/scripts/gulika.py new file mode 100644 index 00000000..f92f439b --- /dev/null +++ b/scripts/gulika.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Swiss-Ephemeris Gulika calculator using the Prasna Marga Ghatika table.""" +from __future__ import annotations + +from datetime import datetime +from typing import Any + +import swisseph as swe + +try: + from saham_daynight import determine_daytime +except ImportError: + from scripts.saham_daynight import determine_daytime + + +# Monday=0, matching datetime.weekday(). Values are the end of Saturn's share +# measured in Ghatika from the relevant sunrise/sunset (30 Ghatika per period). +GHATIKA_END = { + 0: {"day": 22, "night": 6}, + 1: {"day": 18, "night": 2}, + 2: {"day": 14, "night": 26}, + 3: {"day": 10, "night": 22}, + 4: {"day": 6, "night": 18}, + 5: {"day": 2, "night": 14}, + 6: {"day": 26, "night": 10}, +} + + +def _sidereal_ascendant(jd_ut: float, lat: float, lon: float) -> float: + swe.set_sid_mode(swe.SIDM_LAHIRI) + cusps, ascmc = swe.houses_ex(jd_ut, lat, lon, b"P", swe.FLG_SIDEREAL) + return float(ascmc[0]) % 360 + + +def calculate_gulika( + moment: datetime, + *, + lat: float, + lon: float, + tz: float, +) -> dict[str, Any]: + """Return Gulika from local moment/location using Swiss sunrise and sunset.""" + daynight = determine_daytime(moment, lat=lat, lon=lon, tz=tz) + is_day = bool(daynight["is_daytime"]) + period = "day" if is_day else "night" + ghatika_end = GHATIKA_END[moment.weekday()][period] + start_jd = daynight["sunrise_jd_ut"] if is_day else daynight["sunset_jd_ut"] + end_jd = daynight["sunset_jd_ut"] if is_day else daynight["sunrise_jd_ut"] + 1.0 + if end_jd <= start_jd: + end_jd += 1.0 + segment_jd = start_jd + (end_jd - start_jd) * (ghatika_end / 30.0) + longitude = _sidereal_ascendant(segment_jd, float(lat), float(lon)) + return { + "scope": "gulika_prasna_marga", + "status": "partial", + "longitude": round(longitude, 6), + "sign_idx": int(longitude / 30) % 12, + "degree_in_sign": round(longitude % 30, 6), + "period": period, + "weekday": moment.weekday(), + "ghatika_end": ghatika_end, + "segment_jd_ut": segment_jd, + "daynight_evidence": daynight, + "ayanamsa": "lahiri", + "rule_source": "references/prashna-complete-guide.md#3.5", + "boundary": "Formula is implemented from the local classical guide; external JHora/PyJHora numeric parity remains required before enabling Sphuta or verdict layers.", + } diff --git a/tests/test_gulika.py b/tests/test_gulika.py new file mode 100644 index 00000000..0fa09021 --- /dev/null +++ b/tests/test_gulika.py @@ -0,0 +1,17 @@ +from datetime import datetime + +from scripts.gulika import GHATIKA_END, calculate_gulika + + +def test_gulika_uses_prasna_marga_weekday_table() -> None: + assert GHATIKA_END[6] == {"day": 26, "night": 10} + assert GHATIKA_END[0] == {"day": 22, "night": 6} + + +def test_gulika_returns_sidereal_segment_ascendant_with_audit_trace() -> None: + result = calculate_gulika(datetime(1990, 6, 15, 12, 0), lat=39.9042, lon=116.4074, tz=8) + + assert result["status"] == "partial" + assert 0 <= result["longitude"] < 360 + assert result["ghatika_end"] in range(0, 31) + assert result["rule_source"].endswith("#3.5")