add auditable Swiss gulika calculator

This commit is contained in:
732642856
2026-07-14 18:05:53 +08:00
parent f00d0fa368
commit 7142366dc4
4 changed files with 90 additions and 6 deletions
+1
View File
@@ -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
+5 -6
View File
@@ -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."
+67
View File
@@ -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.",
}
+17
View File
@@ -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")