"""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)