Files
Jyotisha/tests/test_rectification_nearest_boundary.py
T

249 lines
11 KiB
Python

"""Nearest chart-boundary reads window_scan transitions only.
Public AA chart: Steve Jobs, 1955-02-24 19:15, San Francisco
(references/public_oracle_cases.json id steve_jobs_1955_aa).
"""
from __future__ import annotations
import copy
from datetime import datetime, timedelta
import pytest
from scripts import domain_calculation_service, varga
from scripts.active_rectification_event_engine import _d11_chart
from scripts.rectification.event_probes import LAYER_DOMAIN as EVENT_LAYER_DOMAIN
from scripts.rectification.nearest_boundary import LAYER_DOMAIN, nearest_chart_boundary
from scripts.rectification.refinement_packet import window_scan
from scripts.rectification_varga_segments_api import BOUNDARY_CHARTS, scan_varga_segments
JOBS = {
"year": 1955, "month": 2, "day": 24,
"lat": 37.7749, "lon": -122.4194, "tz": -8.0,
}
DIVISIONS = [2, 4, 5, 7, 9, 10, 12, 24, 30]
VARGA_CHARTS = ["D2", "D4", "D5", "D7", "D9", "D10", "D11", "D12", "D24", "D30"]
def _features(start: datetime, count: int) -> list[dict]:
rows = []
for offset in range(count):
moment = start + timedelta(minutes=offset)
chart = domain_calculation_service.compute_chart({
**JOBS,
"year": moment.year, "month": moment.month, "day": moment.day,
"hour": moment.hour, "minute": moment.minute,
"ayanamsa": "raman", "node_mode": "mean",
})
ascendant = float(chart["ascendant"]["lon"])
planets = {
name: float(item["lon"])
for name, item in chart.get("planets", {}).items()
if isinstance(item, dict) and isinstance(item.get("lon"), (int, float))
}
computed = varga.calc_all_vargas(planets, ascendant, divisions=DIVISIONS)
vargas = {f"D{value['_meta']['div']}": int(value["Ascendant"]["sign_idx"]) for value in computed.values()}
vargas["D11"] = int(_d11_chart(planets, ascendant)["Ascendant"]["sign_idx"])
rows.append({"feature": {
"time": moment.strftime("%H:%M"),
"ascendant_sign_index": int(ascendant // 30) % 12,
"varga_ascendants": vargas,
}})
return rows
@pytest.fixture(scope="module")
def jobs_scan():
built = {"static_contexts": _features(datetime(1955, 2, 24, 19, 0), 41)}
scan = window_scan(built)
return built, scan
def test_domain_map_matches_event_probes():
assert LAYER_DOMAIN == EVENT_LAYER_DOMAIN
assert "d1" not in LAYER_DOMAIN
def test_ignores_display_layers_and_keeps_the_earlier_tie():
scan = {"transitions": [
{"layer": "kp1", "at": "19:16", "from_sign": "白羊座", "to_sign": "金牛座"},
{"layer": "hora", "at": "19:10", "from_sign": "白羊座", "to_sign": "金牛座"},
{"layer": "d9", "at": "19:10", "from_sign": "狮子座", "to_sign": "狮子座"},
{"layer": "d10", "at": "19:10", "from_sign": "白羊座", "to_sign": "金牛座"},
{"layer": "d12", "at": "19:20", "from_sign": "双子座", "to_sign": "巨蟹座"},
{"layer": "d5", "at": "19:20", "from_sign": "处女座", "to_sign": "天秤座"},
{"layer": "d24", "at": "19:20", "from_sign": "天蝎座", "to_sign": "射手座"},
], "scanned": True}
before = copy.deepcopy(scan)
tied = nearest_chart_boundary(scan, "19:15")
assert scan == before
assert tied["found"] is True
assert tied["at"] == "19:10"
assert tied["distance_minutes"] == 5
assert [row["layer"] for row in tied["layers"]] == ["d10"]
assert tied["topics"] == ["工作"]
assert tied["domains"] == ["career"]
both = nearest_chart_boundary(scan, "19:20")
assert [row["layer"] for row in both["layers"]] == ["d5", "d24", "d12"]
assert both["topics"] == ["学业", "父母"]
assert both["distance_minutes"] == 0
wrapped = nearest_chart_boundary(
{"transitions": [{"layer": "d9", "at": "23:58", "from_sign": "白羊座", "to_sign": "金牛座"}]},
"00:02",
)
assert wrapped["distance_minutes"] == 4
assert nearest_chart_boundary({"transitions": []}, "19:15")["reason"] == "no_transition"
assert nearest_chart_boundary(None, "19:15")["reason"] == "unavailable"
def test_jobs_inside_flush_and_stable(jobs_scan):
_built, scan = jobs_scan
assert "nearest_boundary" not in scan
changes = [
row for row in scan["transitions"]
if row.get("from_sign") and row.get("to_sign") and row["from_sign"] != row["to_sign"]
and row["layer"] in {"d1", "d9", "d10", "d4", "d5", "d24", "d7", "d12", "d2", "d11", "d30"}
]
assert changes, "public AA window produced no chart change"
flush_at = changes[0]["at"]
flush = nearest_chart_boundary(scan, flush_at)
assert flush["found"] is True
assert flush["distance_minutes"] == 0
assert flush["at"] == flush_at
assert flush["layers"]
assert all(row["from_sign"] != row["to_sign"] for row in flush["layers"])
change_clocks = {row["at"] for row in changes}
inside_clock = None
inside_distance = -1
for minute in range(19 * 60, 19 * 60 + 41):
clock = f"{minute // 60:02d}:{minute % 60:02d}"
if clock in change_clocks:
continue
found = nearest_chart_boundary(scan, clock)
distance = found["distance_minutes"]
if found["found"] and distance > inside_distance:
inside_clock, inside_distance = clock, distance
assert inside_clock is not None
assert inside_distance >= 1
inside = nearest_chart_boundary(scan, inside_clock)
assert inside["at"] != inside_clock
features = jobs_scan[0]["static_contexts"]
stable_start = stable_count = best_start = best_count = 0
previous = None
for index, row in enumerate(features):
signature = (
row["feature"]["ascendant_sign_index"],
tuple(sorted(row["feature"]["varga_ascendants"].items())),
)
if signature == previous:
stable_count += 1
else:
stable_start, stable_count, previous = index, 1, signature
if stable_count > best_count:
best_start, best_count = stable_start, stable_count
assert best_count >= 2
stable_rows = features[best_start:best_start + best_count]
stable_scan = window_scan({"static_contexts": stable_rows})
recorded = stable_rows[best_count // 2]["feature"]["time"]
stable = nearest_chart_boundary(stable_scan, recorded)
assert stable["found"] is False
assert stable["reason"] == "no_transition"
print(
f"JOBS_BOUNDARY flush={flush_at} layers={[row['layer'] for row in flush['layers']]} "
f"inside={inside_clock} distance={inside_distance} at={inside['at']} "
f"topics={inside['topics']} stable={stable_rows[0]['feature']['time']}-{stable_rows[-1]['feature']['time']} "
f"recorded={recorded}"
)
def test_api_boundary_is_optional_and_matches_window_scan(tmp_path):
start = datetime(1955, 2, 24, 19, 12)
body = {
"start_at": start.strftime("%Y-%m-%dT%H:%M"),
"end_at": (start + timedelta(minutes=6)).strftime("%Y-%m-%dT%H:%M"),
"latitude": JOBS["lat"], "longitude": JOBS["lon"], "timezone_offset": JOBS["tz"],
"target_charts": ["D1", "D9", "D10"],
}
plain = scan_varga_segments(body, cache_dir=tmp_path)
assert "nearest_boundary" not in plain
assert set(plain["minutes"][0]["signs"]) == {"D1", "D9", "D10"}
edged = scan_varga_segments({
**body,
"boundary_charts": list(BOUNDARY_CHARTS),
"recorded_time": "19:15",
}, cache_dir=tmp_path)
assert plain["window_key"] != edged["window_key"]
assert set(edged["minutes"][0]["signs"]) == {"D1", "D9", "D10"}
assert "nearest_boundary" in edged
scan = window_scan({"static_contexts": _features(start, 7)})
assert nearest_chart_boundary(scan, "19:15") == edged["nearest_boundary"]
with pytest.raises(ValueError, match="invalid_target_charts"):
scan_varga_segments({**body, "target_charts": ["D12"]}, cache_dir=tmp_path)
with pytest.raises(ValueError, match="invalid_boundary_charts"):
scan_varga_segments({**body, "boundary_charts": ["D99"], "recorded_time": "19:15"}, cache_dir=tmp_path)
# sha256 of the omitted-ayanamsa public-chart body, captured before this whitelist.
OMITTED_AYANAMSA_WINDOW_KEY = "58c936a2ccdbb5e339c79f9c43b990d556b627230cd5d5d200f3fcaaea85fad4"
def _public_segment_body() -> dict:
start = datetime(1955, 2, 24, 19, 12)
return {
"start_at": start.strftime("%Y-%m-%dT%H:%M"),
"end_at": (start + timedelta(minutes=6)).strftime("%Y-%m-%dT%H:%M"),
"latitude": JOBS["lat"], "longitude": JOBS["lon"], "timezone_offset": JOBS["tz"],
"target_charts": ["D1", "D9", "D10"],
}
def test_omitted_ayanamsa_matches_raman_and_lahiri_moves(tmp_path):
from scripts.rectification_varga_segments_api import VERSION, _segment_ayanamsa
body = _public_segment_body()
omitted = scan_varga_segments(body, cache_dir=tmp_path)
assert omitted["version"] == VERSION == "rectification-varga-segments-v1"
assert omitted["ayanamsa"] == "raman"
assert omitted["node_mode"] == "mean"
assert omitted["window_key"] == OMITTED_AYANAMSA_WINDOW_KEY
assert "nearest_boundary" not in omitted
for same in (
{"ayanamsa": "raman"},
{"ayanamsa": " Raman "},
{"ayanamsa": " "},
{"ayanamsa": None},
{"node_mode": "true"},
):
assert scan_varga_segments({**body, **same}, cache_dir=tmp_path) == omitted
lahiri = scan_varga_segments({**body, "ayanamsa": "Lahiri"}, cache_dir=tmp_path)
assert lahiri["ayanamsa"] == "lahiri"
assert lahiri["node_mode"] == "mean"
assert lahiri["window_key"] != omitted["window_key"]
assert [row["signs"] for row in lahiri["minutes"]] != [row["signs"] for row in omitted["minutes"]]
assert scan_varga_segments({**body, "ayanamsa": "lahiri"}, cache_dir=tmp_path) == lahiri
kp = scan_varga_segments({**body, "ayanamsa": "kp"}, cache_dir=tmp_path)
assert kp["ayanamsa"] == "kp"
assert kp["window_key"] not in {omitted["window_key"], lahiri["window_key"]}
for alias in ("krishnamurti", "krishnamurti_paddhati", "krishnamurti/kp", "Krishnamurti-Paddhati"):
assert scan_varga_segments({**body, "ayanamsa": alias}, cache_dir=tmp_path) == kp
citra = scan_varga_segments({**body, "ayanamsa": "true_chitra"}, cache_dir=tmp_path)
assert citra["ayanamsa"] == "true_citra"
assert scan_varga_segments({**body, "ayanamsa": "true-citra"}, cache_dir=tmp_path) == citra
pushya = scan_varga_segments({**body, "ayanamsa": "true_pushya"}, cache_dir=tmp_path)
assert pushya["ayanamsa"] == "true_pushya"
fagan = scan_varga_segments({**body, "ayanamsa": "fagan-bradley"}, cache_dir=tmp_path)
assert fagan["ayanamsa"] == "fagan_bradley"
assert len({omitted["window_key"], lahiri["window_key"], kp["window_key"], citra["window_key"], pushya["window_key"], fagan["window_key"]}) == 6
cached = {path.name for path in tmp_path.glob("*.json")}
for bad in ("djwhal_khul", "sassanian", "nope", 1, True):
with pytest.raises(ValueError, match="^invalid_ayanamsa$"):
scan_varga_segments({**body, "ayanamsa": bad}, cache_dir=tmp_path)
assert {path.name for path in tmp_path.glob("*.json")} == cached
assert _segment_ayanamsa("true_chitra") == "true_citra"
assert _segment_ayanamsa(None) == "raman"