Files
Jyotisha/tests/test_nadi_seconds_research.py
T
jesse-uxandJesse_Chen c40f23c20e research(rectification): nadi second-level study helpers and N0 checks (BUG-1240)
Five-level Vimshottari and equal-division D150 helpers, plus a runner that
refuses to score until the preregistration file exists. First three lords
match the production chain on the v5 events. No production files changed.
2026-10-05 23:26:05 +08:00

180 lines
6.7 KiB
Python

"""Nadi / five-level dasha research helpers. Public v5 cases only; no production edits."""
from __future__ import annotations
import inspect
import math
from datetime import datetime, timedelta
import pytest
from scripts.research import nadi_seconds_lib as lib
class _Guard(dict):
"""Raises if a fitter touches the truth label."""
def __getitem__(self, key):
if key == "true_minute":
raise AssertionError("read true_minute")
return dict.__getitem__(self, key)
def get(self, key, default=None):
if key == "true_minute":
raise AssertionError("read true_minute")
return dict.get(self, key, default)
def _packet() -> dict:
moment = datetime(2010, 1, 1)
lords = lib.five_lords("1990-06-15", 120.0, moment)
return {
"lords": lords,
"again": lib.five_lords("1990-06-15", 120.0, moment),
"d150": lib.d150_equal(10.25),
"classical": lib.classical_nadi_index(40.1),
"p": lib.paired_sign_flip_p([0.1, -0.2, 0.3, 0.0], seed=20261005, permutations=200),
"ci": lib.bootstrap_mean_ci([1.0, 0.0, 1.0, 0.0], seed=20261008, resamples=300),
}
def test_two_runs_are_byte_identical() -> None:
assert lib.canonical_bytes(_packet()) == lib.canonical_bytes(_packet())
def test_five_levels_abut_nest_and_flip_on_the_boundary() -> None:
birth = "1990-06-15"
moon = 0.0
event = datetime(1990, 6, 25)
chain = lib.chain_at(birth, moon, event)
assert len(chain) == 5
for period in chain:
assert period["start"] <= event < period["end"]
for period in chain[:4]:
rows = lib.subdivide(period)
assert rows[0]["start"] == period["start"]
for left, right in zip(rows, rows[1:], strict=False):
assert left["end"] == right["start"]
rows = lib.subdivide(chain[0])
boundary = rows[0]["end"]
before = lib.five_lords(birth, moon, boundary - timedelta(seconds=1))
after = lib.five_lords(birth, moon, boundary)
assert before[1] == rows[0]["lord"]
assert after[1] == rows[1]["lord"]
assert before[1] != after[1]
def test_d150_part_flips_across_slice_and_sign_boundaries() -> None:
assert lib.d150_equal(0.199)["part_index"] == 0
assert lib.d150_equal(0.201)["part_index"] == 1
assert lib.d150_equal(29.999)["part_index"] == 149
assert lib.d150_equal(30.001)["part_index"] == 0
assert lib.classical_nadi_index(0.1)["status"] == "variant_unverified"
assert lib.classical_nadi_index(0.1)["index"] == 0
assert lib.classical_nadi_index(30.1)["order"] == "reverse"
assert lib.classical_nadi_index(30.1)["index"] == 149
assert lib.classical_nadi_index(60.0)["order"] == "from_middle"
assert lib.classical_nadi_index(60.0)["index"] == 75
def test_rule_families_use_domain_house_lords_only() -> None:
assert lib.domain_target_lords(0, "career") == frozenset({"Saturn"})
lords = ("Sun", "Moon", "Mars", "Mercury", "Jupiter")
targets = frozenset({"Mercury"})
assert lib.explained(lords, targets, "A4") is True
assert lib.explained(lords, targets, "A5") is False
assert lib.explained(lords, targets, "B1") is True
assert lib.explained(lords, targets, "B2") is False
assert lib.explained(lords, targets, "N5A", segment_lord="Saturn") is False
assert lib.explained(lords, targets, "N5B", segment_lord="Moon") is True
assert lib.fit_count([lords, lords], ["career", "career"], 0, "A5") == 0
assert lib.rank_offsets({-5: 1, 0: 3, 5: 3, 10: 2}) == [0, 5]
def test_fitter_does_not_read_true_minute() -> None:
for function in (
lib.fit_count,
lib.rank_offsets,
lib.explained,
lib.counts_by_offset,
lib.public_fields,
lib.lords_for_events,
):
assert "true_minute" not in inspect.getsource(function)
raw = lib.load_cases()[0]
guarded = _Guard(raw)
guarded["birth"] = _Guard(raw["birth"])
guarded["events"] = [_Guard(event) for event in raw["events"]]
public = lib.public_fields(guarded)
assert "true_minute" not in public
sky = lib.sky_at(public["birth"], lib.candidate_moment(public["birth"], 0))
events = lib.day_events(public)[:1]
assert events
rows = lib.lords_for_events(sky["birth_date"], sky["moon_lon"], events)
count = lib.fit_count(rows, [events[0]["domain"]], sky["asc_index"], "A5")
assert count in (0, 1)
assert lib.rank_offsets({0: 2, 5: 2, -5: 1}) == [0, 5]
def test_midnight_candidate_uses_the_previous_local_date_and_matches_production() -> None:
birth = {
"date": "1990-06-15",
"time": "00:00",
"latitude": 31.2,
"longitude": 121.5,
"timezone_offset": 8.0,
}
moment = lib.candidate_moment(birth, -30)
assert moment == datetime(1990, 6, 14, 23, 59, 30)
sky = lib.sky_at(birth, moment)
assert sky["birth_date"] == "1990-06-14"
event = datetime(2001, 3, 4)
assert lib.five_lords(sky["birth_date"], sky["moon_lon"], event)[:3] == lib.production_three(
sky["birth_date"], sky["moon_lon"], event,
)
def test_v5_truth_audit_matches_the_task_counts() -> None:
audit = lib.truth_audit(lib.load_cases())
assert audit["cases"] == 77
assert audit["rounded_5min_count"] == 52
assert audit["unrounded_count"] == 25
assert audit["day_event_total"] == 244
assert audit["day_events_ge_3"] == 43
assert audit["day_events_ge_5"] == 19
assert audit["day_events_zero"] == 4
assert sum(audit["minute_histogram"].values()) == 77
def test_first_three_levels_match_production_on_every_v5_event() -> None:
mismatches = []
for case in lib.load_cases():
row = lib.reconcile_case(case)
if row["mismatches"]:
mismatches.append(row["case_id"])
assert mismatches == []
def test_placebo_shift_is_seeded_and_stays_after_birth() -> None:
case = lib.public_fields(lib.load_cases()[0])
events = lib.day_events(case)
first = lib.placebo_shift_dates(
events, case["birth"]["date"], seed=f"20261005:{case['case_id']}", low_days=30, high_days=180,
)
second = lib.placebo_shift_dates(
events, case["birth"]["date"], seed=f"20261005:{case['case_id']}", low_days=30, high_days=180,
)
assert first == second
birth = case["birth"]["date"]
assert all(birth < row["date"] <= "2026-09-14" for row in first)
assert [row["domain"] for row in first] == [row["domain"] for row in events]
assert [row["date"] for row in first] != [row["date"] for row in events]
def test_longitude_shift_is_about_ten_kilometres() -> None:
moved = lib.shift_longitude_km(121.5, 31.2, 10.0)
scale = 111.32 * math.cos(math.radians(31.2))
assert moved == pytest.approx(121.5 + 10.0 / scale)
with pytest.raises(ValueError):
lib.shift_longitude_km(0.0, 90.0, 10.0)