Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
84 lines
3.4 KiB
Python
84 lines
3.4 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()
|