"""Systemy zodiaku (LOG-04): ayanamsy, syderyczny, draconic, RA. Wartości referencyjne ayanams pochodzą ze **Swiss Ephemeris** (get_ayanamsa_ut) użytego jako wyrocznia — silnika B nie importujemy tu (izolacja AGPL), więc stałe są wpięte tak jak referencje astro-seek w innych testach. Nasz model odtwarza je z dokładnością do ~0,02" w latach 1900–2100. """ import math import pytest from app.engine import zodiac as Z from app.engine.chart import build_chart from app.engine.formats import sign_index # wyrocznia: swisseph get_ayanamsa_ut (JD UT -> ayanamsa °) ORACLE = { "sidereal_lahiri": { 2451545.0: 23.857092, 2445820.88889: 23.638183, 2415021.0: 22.460550, 2433283.0: 23.158744, 2469808.0: 24.555632, 2488070.0: 25.254287, }, "sidereal_fagan_bradley": { 2451545.0: 24.740300, 2445820.88889: 24.521391, 2415021.0: 23.343757, 2433283.0: 24.041952, 2469808.0: 25.438840, 2488070.0: 26.137495, }, "sidereal_krishnamurti": { 2451545.0: 23.760240, 2415021.0: 22.363697, 2469808.0: 24.458780, 2488070.0: 25.157435, }, } def _arcsec(a, b): return abs(a - b) * 3600.0 @pytest.mark.parametrize("name", list(ORACLE)) def test_ayanamsha_matches_swisseph_oracle(name): for jd, ref in ORACLE[name].items(): got = Z.ayanamsha(name, jd) assert _arcsec(got, ref) < 0.1, f"{name} @ JD{jd}: {got} vs {ref} ({_arcsec(got, ref):.3f}\")" def test_julian_day_j2000(): from datetime import datetime, timezone jd = Z.julian_day(datetime(2000, 1, 1, 12, 0, tzinfo=timezone.utc)) assert abs(jd - 2451545.0) < 1e-6 def test_offset_tropical_is_zero(): assert Z.offset("tropical", 2451545.0) == 0.0 def test_offset_draconic_is_node_longitude(): assert Z.offset("draconic", 2451545.0, node_lon=68.0) == 68.0 # normalizacja assert Z.offset("draconic", 2451545.0, node_lon=428.0) == pytest.approx(68.0) def test_offset_draconic_requires_node(): with pytest.raises(ValueError): Z.offset("draconic", 2451545.0) def test_unknown_zodiac_rejected(): with pytest.raises(ValueError): Z.offset("bzdura", 2451545.0) with pytest.raises(ValueError): Z.ayanamsha("sidereal_nieistnieje", 2451545.0) def test_apply_subtracts_and_normalizes(): assert Z.apply(10.0, 20.0) == pytest.approx(350.0) assert Z.apply(40.0, 24.74) == pytest.approx(15.26) def test_to_equatorial_reference_points(): # punkt równonocy: λ=0,β=0 -> RA=0, dec=0 ra, dec = Z.to_equatorial(0.0, 0.0, 23.4392911) assert ra == pytest.approx(0.0, abs=1e-9) and dec == pytest.approx(0.0, abs=1e-9) # przesilenie letnie: λ=90,β=0 -> RA=90, dec=+ε ra, dec = Z.to_equatorial(90.0, 0.0, 23.4392911) assert ra == pytest.approx(90.0, abs=1e-6) assert dec == pytest.approx(23.4392911, abs=1e-6) # ---- integracja z pełnym horoskopem ---- def test_sidereal_chart_shifts_signs_back(own_engine, reference_moment): trop = build_chart(own_engine, reference_moment, "whole_sign") sid = build_chart(own_engine, reference_moment, "whole_sign", zodiac="sidereal_lahiri") assert sid["zodiac"] == "sidereal_lahiri" assert "ayanamsha" in sid and 23.0 < sid["ayanamsha"] < 24.5 tp = {p["name"]: p for p in trop["positions"]} sp = {p["name"]: p for p in sid["positions"]} # syderyczna długość = tropikalna − ayanamsa (mod 360) dla każdego obiektu ay = sid["ayanamsha"] for name in tp: expect = (tp[name]["decimal"] - ay) % 360.0 assert abs(((sp[name]["decimal"] - expect + 180) % 360) - 180) < 1e-4 def test_sidereal_preserves_house_numbers(own_engine, reference_moment): # obrót zodiaku nie zmienia numerów domów (geometria niezmiennicza) trop = build_chart(own_engine, reference_moment, "whole_sign") sid = build_chart(own_engine, reference_moment, "whole_sign", zodiac="sidereal_lahiri") tp = {p["name"]: p["house"] for p in trop["positions"]} sp = {p["name"]: p["house"] for p in sid["positions"]} assert tp == sp def test_draconic_puts_node_at_zero_aries(own_engine, reference_moment): drac = build_chart(own_engine, reference_moment, "whole_sign", zodiac="draconic") assert drac["zodiac"] == "draconic" nn = next(p for p in drac["positions"] if p["name"] == "North Node") # węzeł wznoszący = 0° Barana w draconic assert sign_index(nn["decimal"]) == 0 assert nn["decimal"] < 0.001 or nn["decimal"] > 359.999 def test_tropical_default_unchanged(own_engine, reference_moment): # domyślnie tropikalny: brak ayanamshy, zodiac=tropical chart = build_chart(own_engine, reference_moment, "whole_sign") assert chart["zodiac"] == "tropical" assert "ayanamsha" not in chart