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.
180 lines
6.7 KiB
Python
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)
|