473d059a6a
build / build (push) Successful in 1m8s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m2s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m43s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 22s
Komplet wyliczen, ktore astrolog czyta „obok" pozycji: - bilans zywiolow i jakosci w czterech wariantach (7 klasycznych / 10 z nowozytnymi, z Ascendentem i bez) + wykrywanie BRAKUJACYCH zywiolow — klasyczne „no air", podstawa pod scoring sily (LOG-21), - faza Ksiezyca: elongacja, nazwa fazy, procent oswietlenia, przybywa/ubywa, - stopnie krytyczne wg jakosci znaku (kardynalne 0/13/26, stale 8/21, zmienne 4/17) + 29 stopien anaretyczny i 0 stopni wejscia w znak, - dzien i godziny planetarne w porzadku chaldejskim, - syzygia prenatalna (ostatni now albo pelnia przed urodzeniem), - podzialy: dwunastniki (D12) i nawamsa (D9). Dwie rzeczy wymagaly prawdziwego liczenia, nie tabelki: * godziny planetarne sa NIEROWNE — dzien od wschodu do zachodu dzieli sie na 12, noc osobno. Bez faktycznego wschodu/zachodu wynik bylby zmyslony, wiec szukamy ich numerycznie (przejscie wysokosci Slonca przez -0°50', bisekcja jak przy stacjach z LOG-03). Doba planetarna startuje o WSCHODZIE, nie o polnocy. * syzygia prenatalna — szukanie wstecz przejscia elongacji przez 0/180 stopni. Walidacja wobec faktow NIEZALEZNYCH od naszego kodu: - 30.04.1984 to poniedzialek -> wladca dnia Ksiezyc; 5. godzina poniedzialku w porzadku chaldejskim to Slonce (Mo, Sa, Ju, Ma, Su) — zgadza sie, - wschod/zachod dla Krakowa: 03:18 / 17:57 UTC = 5:18 / 19:57 lokalnie — zgodne z rzeczywistoscia dla konca kwietnia, - syzygia: pelnia 15.04.1984 19:10:45 UTC; rzeczywista byla 19:11 — roznica ponizej minuty, - bilans przeliczony recznie: Ogien 4, Ziemia 4, Woda 3, Powietrze 0. UI: checkbox „tabele dodatkowe" na ekranie Horoskop (opt-in, bo szuka numerycznie) i sekcja wynikow. Endpoint: /chart/positions?tables=true. Testy: 28 nowych (w tym noc polarna -> brak godzin planetarnych, oraz sprawdzenie, ze w znalezionej syzygii elongacja FAKTYCZNIE wynosi 0/180). Calosc: 202 passed / 1 skipped + 17 (prezentacja). Zweryfikowane e2e w UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
"""Tabele pomocnicze horoskopu (LOG-23).
|
|
|
|
Wartości referencyjne dla horoskopu 30.04.1984 09:20 UTC, Kraków (50.0647N, 19.9450E)
|
|
sprawdzone wobec faktów NIEZALEŻNYCH od naszego kodu:
|
|
* 30.04.1984 to poniedziałek → władca dnia Księżyc; 5. godzina poniedziałku
|
|
w porządku chaldejskim to Słońce (Mo, Sa, Ju, Ma, Su),
|
|
* wschód/zachód dla Krakowa końcem kwietnia ≈ 5:18 / 19:57 czasu lokalnego
|
|
(CEST = UTC+2), czyli 03:18 / 17:57 UTC,
|
|
* pełnia poprzedzająca urodzenie: 15.04.1984 ok. 19:11 UTC.
|
|
"""
|
|
from datetime import datetime, timezone
|
|
|
|
import pytest
|
|
|
|
from app.engine.chart import build_chart
|
|
from app.engine.models import ChartMoment
|
|
from app.engine.tables import (
|
|
build_tables,
|
|
critical_degrees,
|
|
dwadasamsa,
|
|
element_of,
|
|
moon_phase,
|
|
navamsa,
|
|
planetary_hours,
|
|
prenatal_syzygy,
|
|
quality_of,
|
|
tally,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def krakow():
|
|
return ChartMoment(when_utc=datetime(1984, 4, 30, 9, 20, tzinfo=timezone.utc),
|
|
lat=50.0647, lon=19.9450)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def tables(own_engine, krakow):
|
|
return build_tables(own_engine, krakow, build_chart(own_engine, krakow))
|
|
|
|
|
|
# ------------------------------------------------------ żywioły i jakości
|
|
|
|
def test_element_and_quality_mapping():
|
|
assert element_of("Aries") == "Fire" and element_of("Cancer") == "Water"
|
|
assert quality_of("Aries") == "Cardinal" and quality_of("Taurus") == "Fixed"
|
|
assert quality_of("Gemini") == "Mutable"
|
|
|
|
|
|
def test_tally_matches_hand_count(tables):
|
|
"""Ręcznie przeliczone dla horoskopu referencyjnego (10 planet + Asc)."""
|
|
base = tables["tally"]["with_modern_10_plus_asc"]
|
|
assert base["elements"] == {"Fire": 4, "Earth": 4, "Air": 0, "Water": 3}
|
|
assert base["qualities"] == {"Cardinal": 4, "Fixed": 6, "Mutable": 1}
|
|
assert base["total"] == 11
|
|
|
|
|
|
def test_missing_element_detected(tables):
|
|
"""Klasyczne „no air" — podstawa pod scoring siły (LOG-21)."""
|
|
assert tables["tally"]["missing_elements"] == ["Air"]
|
|
assert tables["tally"]["missing_qualities"] == []
|
|
|
|
|
|
def test_tally_variants_differ_by_object_count(tables):
|
|
t = tables["tally"]
|
|
assert t["classical_7"]["total"] == 7
|
|
assert t["with_modern_10"]["total"] == 10
|
|
assert t["classical_7_plus_asc"]["total"] == 8
|
|
assert t["with_modern_10_plus_asc"]["total"] == 11
|
|
|
|
|
|
def test_tally_sums_equal_counted_objects(tables):
|
|
for variant in ("classical_7", "with_modern_10", "with_modern_10_plus_asc"):
|
|
v = tables["tally"][variant]
|
|
assert sum(v["elements"].values()) == v["total"]
|
|
assert sum(v["qualities"].values()) == v["total"]
|
|
|
|
|
|
# ---------------------------------------------------------- faza Księżyca
|
|
|
|
def test_moon_phase_reference_is_balsamic_new(tables):
|
|
"""Nów wypadł 1.05.1984, więc 30.04 Księżyc jest tuż przed nowiem."""
|
|
mp = tables["moon_phase"]
|
|
assert mp["phase"] == "New Moon"
|
|
assert 340.0 < mp["angle"] < 360.0
|
|
assert mp["illumination"] < 0.05
|
|
assert mp["waxing"] is False # elongacja > 180 = ubywa
|
|
|
|
|
|
@pytest.mark.parametrize("angle,expected", [
|
|
(0.0, "New Moon"), (90.0, "First Quarter"), (180.0, "Full Moon"),
|
|
(270.0, "Last Quarter"), (46.0, "Waxing Crescent"), (300.0, "Waning Crescent"),
|
|
])
|
|
def test_moon_phase_buckets(angle, expected):
|
|
assert moon_phase(0.0, angle)["phase"] == expected
|
|
|
|
|
|
def test_moon_phase_illumination_extremes():
|
|
assert moon_phase(0.0, 0.0)["illumination"] == 0.0
|
|
assert moon_phase(0.0, 180.0)["illumination"] == 1.0
|
|
assert moon_phase(0.0, 90.0)["illumination"] == pytest.approx(0.5)
|
|
|
|
|
|
# ------------------------------------------------------- stopnie krytyczne
|
|
|
|
def test_critical_degrees_reference(tables):
|
|
found = {c["name"]: c["flags"] for c in tables["critical_degrees"]}
|
|
assert "Jupiter" in found # Cap 12°57' -> 13° kardynalny
|
|
assert any("13" in f for f in found["Jupiter"])
|
|
assert "Pluto" in found # Sco 0°28' -> wejście w znak
|
|
|
|
|
|
def test_anaretic_degree_flagged():
|
|
flags = critical_degrees([{"name": "X", "sign": "Leo", "decimal": 149.5,
|
|
"in_sign": "Leo 29°30'"}])
|
|
assert flags and any("anaretyczny" in f for f in flags[0]["flags"])
|
|
|
|
|
|
def test_no_flags_for_ordinary_degree():
|
|
assert critical_degrees([{"name": "X", "sign": "Leo", "decimal": 135.0,
|
|
"in_sign": "Leo 15°"}]) == []
|
|
|
|
|
|
# --------------------------------------------------------------- podziały
|
|
|
|
def test_dwadasamsa_starts_from_own_sign():
|
|
"""12. część liczy się OD znaku, w którym stoi punkt."""
|
|
assert dwadasamsa(0.0) == pytest.approx(0.0) # Ari 0 -> Ari
|
|
assert dwadasamsa(2.5) == pytest.approx(30.0) # Ari 2°30' -> Tau 0
|
|
assert dwadasamsa(30.0) == pytest.approx(30.0) # Tau 0 -> Tau
|
|
|
|
|
|
def test_navamsa_classic_starts():
|
|
"""Znaki kardynalne zaczynają od siebie, stałe od 9. znaku."""
|
|
assert navamsa(0.0) == pytest.approx(0.0) # Ari -> Ari
|
|
assert navamsa(30.0) == pytest.approx(270.0) # Tau -> Cap (9. od Byka)
|
|
assert navamsa(60.0) == pytest.approx(180.0) # Gem -> Lib
|
|
|
|
|
|
def test_divisional_covers_all_positions(tables, own_engine, krakow):
|
|
chart = build_chart(own_engine, krakow)
|
|
assert len(tables["divisional"]) == len(chart["positions"])
|
|
|
|
|
|
# ------------------------------------------------- dzień i godziny planetarne
|
|
|
|
def test_planetary_day_ruler_is_moon_on_monday(tables):
|
|
"""30.04.1984 to poniedziałek → władcą dnia jest Księżyc."""
|
|
assert tables["planetary_hours"]["day_ruler"] == "Moon"
|
|
|
|
|
|
def test_planetary_hour_matches_chaldean_sequence(tables):
|
|
"""Poniedziałek: 1=Mo, 2=Sa, 3=Ju, 4=Ma, 5=Su — urodzenie w 5. godzinie dnia."""
|
|
ph = tables["planetary_hours"]
|
|
assert ph["hour_number"] == 5
|
|
assert ph["hour_ruler"] == "Sun"
|
|
assert ph["daytime"] is True
|
|
|
|
|
|
def test_sunrise_sunset_match_krakow_late_april(tables):
|
|
"""Wschód ≈ 03:18 UTC, zachód ≈ 17:57 UTC (5:18 i 19:57 czasu lokalnego)."""
|
|
ph = tables["planetary_hours"]
|
|
assert ph["period_start"].startswith("1984-04-30T03:1")
|
|
assert ph["period_end"].startswith("1984-04-30T17:5")
|
|
|
|
|
|
def test_planetary_hours_are_unequal_and_complete(tables):
|
|
"""Godziny są nierówne: wiosną dzienna trwa dłużej niż 60 minut."""
|
|
ph = tables["planetary_hours"]
|
|
assert ph["hour_length_minutes"] > 60.0
|
|
assert len(ph["hours"]) == 12
|
|
assert sum(1 for h in ph["hours"] if h["current"]) == 1
|
|
|
|
|
|
def test_polar_night_returns_none(own_engine):
|
|
"""Za kołem podbiegunowym w grudniu Słońce nie wschodzi — brak godzin."""
|
|
polar = ChartMoment(when_utc=datetime(2024, 12, 21, 12, 0, tzinfo=timezone.utc),
|
|
lat=78.0, lon=15.0)
|
|
assert planetary_hours(own_engine, polar) is None
|
|
|
|
|
|
# ------------------------------------------------------- syzygia prenatalna
|
|
|
|
def test_prenatal_syzygy_is_april_1984_full_moon(tables):
|
|
"""Rzeczywista pełnia: 15.04.1984 ok. 19:11 UTC."""
|
|
s = tables["prenatal_syzygy"]
|
|
assert s["type"] == "full_moon"
|
|
assert s["when_utc"].startswith("1984-04-15T19:1")
|
|
|
|
|
|
def test_prenatal_syzygy_precedes_birth_within_a_cycle(tables):
|
|
days = tables["prenatal_syzygy"]["days_before_birth"]
|
|
assert 0 < days < 29.6, "syzygia musi być w ostatnim cyklu przed urodzeniem"
|
|
|
|
|
|
def test_prenatal_syzygy_elongation_is_at_target(own_engine, krakow):
|
|
"""W znalezionym momencie elongacja MUSI wynosić 0° albo 180°."""
|
|
from app.engine.formats import norm360
|
|
|
|
s = prenatal_syzygy(own_engine, krakow)
|
|
when = datetime.fromisoformat(s["when_utc"])
|
|
pts = {p.name: p.longitude for p in
|
|
own_engine.positions(ChartMoment(when_utc=when, lat=krakow.lat, lon=krakow.lon),
|
|
["Sun", "Moon"])}
|
|
elong = norm360(pts["Moon"] - pts["Sun"])
|
|
target = 0.0 if s["type"] == "new_moon" else 180.0
|
|
assert abs(((elong - target + 180.0) % 360.0) - 180.0) < 0.02
|
|
|
|
|
|
# ------------------------------------------------------------------ całość
|
|
|
|
def test_build_tables_light_skips_numeric_search(own_engine, krakow):
|
|
"""heavy=False pomija to, co wymaga szukania numerycznego."""
|
|
light = build_tables(own_engine, krakow, build_chart(own_engine, krakow), heavy=False)
|
|
assert "tally" in light and "moon_phase" in light
|
|
assert "planetary_hours" not in light and "prenatal_syzygy" not in light
|