Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
102 lines
4.2 KiB
Python
102 lines
4.2 KiB
Python
"""Angle-timing research helpers (BUG-1141). Offline; public v5 cases only."""
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from random import Random
|
|
|
|
import numpy as np
|
|
import pytest
|
|
|
|
from scripts.research import angle_timing_lib as at
|
|
|
|
|
|
def test_angle_diff_wraps_and_aspect_sep_takes_nearest_aspect():
|
|
assert at.angle_diff(359.0, 1.0) == pytest.approx(2.0)
|
|
assert at.angle_diff(10.0, 190.0) == pytest.approx(180.0)
|
|
seps = at.aspect_sep(np.array([0.0, 91.0, 178.5, 45.0]), np.array([0.0, 0.0, 0.0, 0.0]))
|
|
assert list(np.round(seps, 3)) == [0.0, 1.0, 1.5, 45.0]
|
|
|
|
|
|
@pytest.mark.parametrize("raw, precision, start, end", [
|
|
({"date": "2001-03-04", "precision": "day"}, "day", date(2001, 3, 4), date(2001, 3, 4)),
|
|
({"date": "2001-02", "precision": "month"}, "month", date(2001, 2, 1), date(2001, 2, 28)),
|
|
({"date": "2001", "precision": "year"}, "year", date(2001, 1, 1), date(2001, 12, 31)),
|
|
])
|
|
def test_parse_event_spans(raw, precision, start, end):
|
|
event = at.parse_event({**raw, "id": "x", "domain": "career"})
|
|
assert event is not None and (event.precision, event.start, event.end) == (precision, start, end)
|
|
|
|
|
|
def test_parse_event_rejects_future_and_malformed():
|
|
assert at.parse_event({"date": "2030", "precision": "year"}) is None
|
|
assert at.parse_event({"date": "2001-13", "precision": "month"}) is None
|
|
assert at.parse_event({"date": "", "precision": "day"}) is None
|
|
|
|
|
|
def test_sample_dates_by_technique_and_precision():
|
|
month = at.parse_event({"date": "2001-02", "precision": "month"})
|
|
year = at.parse_event({"date": "2001", "precision": "year"})
|
|
assert len(at.sample_dates(month, "TR")) == 28
|
|
assert len(at.sample_dates(month, "SP")) == 1
|
|
assert at.sample_dates(year, "TR") == []
|
|
assert len(at.sample_dates(year, "SA")) == 5
|
|
|
|
|
|
def test_truth_rank_semantics():
|
|
scores = np.zeros(121)
|
|
scores[at.RADIUS_MAX] = 3
|
|
assert at.truth_rank(scores, 10) == 0.0
|
|
flat = np.ones(121)
|
|
assert at.truth_rank(flat, 10) == pytest.approx(0.5)
|
|
worst = np.ones(121); worst[at.RADIUS_MAX] = 0
|
|
assert at.truth_rank(worst, 10) == 1.0
|
|
|
|
|
|
def test_shuffle_keeps_precision_count_and_is_seeded():
|
|
case = at.load_cases()[0]
|
|
a = at.shuffled_events(case, Random("seed-1"))
|
|
b = at.shuffled_events(case, Random("seed-1"))
|
|
assert a == b
|
|
assert [e["precision"] for e in a] == [e["precision"] for e in case["events"]]
|
|
assert [len(e["date"]) for e in a] == [len(str(e["date"])[:{"day": 10, "month": 7}.get(e["precision"], 4)]) for e in case["events"]]
|
|
birth_year = int(case["birth"]["date"][:4])
|
|
assert all(int(e["date"][:4]) > birth_year for e in a)
|
|
|
|
|
|
def test_natal_angles_move_about_a_quarter_degree_per_minute():
|
|
chart = at.CaseChart(at.load_cases()[0])
|
|
step_asc = at.angle_diff(chart.asc[1:], chart.asc[:-1])
|
|
step_mc = at.angle_diff(chart.mc[1:], chart.mc[:-1])
|
|
assert 0.05 < float(np.median(step_asc)) < 1.0
|
|
assert 0.2 < float(np.median(step_mc)) < 0.3
|
|
# the truth minute is the window centre
|
|
assert chart.offsets[at.RADIUS_MAX] == 0
|
|
|
|
|
|
def test_separation_matrices_have_one_row_per_usable_event():
|
|
chart = at.CaseChart(at.load_cases()[0])
|
|
events = chart.events()
|
|
tr = chart.separations("TR", events)
|
|
sp = chart.separations("SP", events)
|
|
assert tr.shape[0] == sum(1 for e in events if e.precision != "year")
|
|
assert sp.shape == (len(events), len(chart.offsets))
|
|
assert np.isfinite(sp).all() and (sp >= 0).all() and (sp <= 45).all()
|
|
|
|
|
|
def test_a1_runner_is_deterministic_and_reports_every_preregistered_test():
|
|
from scripts.research import angle_timing_research as runner
|
|
|
|
cases = at.load_cases()[:2]
|
|
first = runner.run_a1(cases, 3, lambda _m: None)
|
|
second = runner.run_a1(cases, 3, lambda _m: None)
|
|
assert at.stable_json(first) == at.stable_json(second)
|
|
assert first["tests"] == 9
|
|
assert first["bonferroni_percentile"] == pytest.approx(100 * (1 - 0.05 / 9), abs=1e-3)
|
|
for tech in at.TECHNIQUES:
|
|
for orb in at.ORBS:
|
|
for radius in runner.RADII:
|
|
cell = first["table"][tech][str(orb)][str(radius)]
|
|
assert 0.0 <= cell["real_mean_rank"] <= 1.0
|
|
assert 0.0 <= cell["percentile_beaten"] <= 100.0
|
|
assert len(first["shuffled_means"][tech][str(orb)][str(radius)]) == 3
|