feat(logic): systemy zodiaku — syderyczny, draconic (LOG-04)
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m40s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 29s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 22s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m55s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 35s
Testy / Kontrola składni wszystkich warstw (push) Successful in 21s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m40s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 29s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 22s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m55s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 35s
Testy / Kontrola składni wszystkich warstw (push) Successful in 21s
Jedyne wymaganie Must bez implementacji. Nowy zodiac.py + wpiecie w horoskop: - zodiac.py: ayanamsy (Lahiri, Fagan-Bradley, Krishnamurti) modelem ayan(jd)=ayan0+B*x+C*x^2 (wspolna precesja, rozna stala) — skalibrowanym do Swiss Ephemeris jako WYROCZNI: zgodnosc do ~0,02" w latach 1900-2100. Draconic = wzgledem wzla wznoszacego (wzel = 0 Barana). RA: konwersja ekliptyka->rownik (to_equatorial) na przyszly widok rownikowy. - chart.py: build_chart(..., zodiac): offset jednolicie przesuwa etykiety znakow/dlugosci obiektow, osi, cusps i Lots; DOMY licza sie po dlugosci tropikalnej (geometria niezmiennicza wzgledem obrotu -> numery domow bez zmian). Domyslnie tropical -> sciezka i wyniki bez zmian. - main.py: /chart/positions przyjmuje `zodiac`; bledny -> 422. - prezentacja: dropdown „Zodiak" + pokazanie ayanamshy w wynikach. Testy (14): ayanamsy vs wyrocznia swisseph (<0.1"), julian_day, draconic (wzel=0 Barana), niezmienniczosc domow, RA w punktach charakterystycznych, odrzucenie bledow. Cala logika: 99 passed, 1 skipped. Zweryfikowane e2e w przegladarce (horoskop syderyczny Lahiri: Slonce Aries 16°34', ayan 23.6382). RA jako osobny widok zodiaku (per-obiekt, z szerokoscia) — do osobnego PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,28 +7,51 @@ pozycje.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from app.engine import houses as H
|
from app.engine import houses as H
|
||||||
|
from app.engine import zodiac as Z
|
||||||
from app.engine.base import EphemerisEngine
|
from app.engine.base import EphemerisEngine
|
||||||
from app.engine.formats import SIGNS, in_sign, norm360, sign_index
|
from app.engine.formats import SIGNS, absolute, decimal, in_sign, norm360, sign_index
|
||||||
from app.engine.models import ChartMoment
|
from app.engine.models import ChartMoment
|
||||||
|
|
||||||
|
|
||||||
def _fmt(name: str, lon: float) -> dict:
|
def _fmt(name: str, lon: float, off: float = 0.0) -> dict:
|
||||||
|
lon = norm360(lon - off)
|
||||||
return {
|
return {
|
||||||
"name": name,
|
"name": name,
|
||||||
"sign": SIGNS[sign_index(lon)],
|
"sign": SIGNS[sign_index(lon)],
|
||||||
"in_sign": in_sign(lon),
|
"in_sign": in_sign(lon),
|
||||||
"decimal": round(norm360(lon), 6),
|
"decimal": round(lon, 6),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _shift_pos(pdict: dict, off: float) -> None:
|
||||||
|
"""Przelicza etykiety pozycji na wybrany zodiak (in-place). off=0 → bez zmian."""
|
||||||
|
if not off:
|
||||||
|
return
|
||||||
|
lon = norm360(pdict["decimal"] - off)
|
||||||
|
pdict["sign"] = SIGNS[sign_index(lon)]
|
||||||
|
pdict["in_sign"] = in_sign(lon)
|
||||||
|
pdict["absolute"] = absolute(lon)
|
||||||
|
pdict["decimal"] = decimal(lon)
|
||||||
|
|
||||||
|
|
||||||
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN,
|
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN,
|
||||||
lots_method: str = "degree") -> dict:
|
lots_method: str = "degree", zodiac: str = Z.TROPICAL) -> dict:
|
||||||
from app.engine.aspects import find_aspects
|
from app.engine.aspects import find_aspects
|
||||||
|
|
||||||
positions = engine.positions(moment)
|
positions = engine.positions(moment)
|
||||||
result: dict = {"engine": engine.name, "positions": [p.as_dict() for p in positions]}
|
result: dict = {"engine": engine.name, "positions": [p.as_dict() for p in positions]}
|
||||||
|
# aspekty liczymy PRZED zmianą zodiaku — kąty między obiektami są niezmiennicze
|
||||||
result["aspects"] = find_aspects(result["positions"]) # aspekty (LOG-06)
|
result["aspects"] = find_aspects(result["positions"]) # aspekty (LOG-06)
|
||||||
|
|
||||||
|
# offset zodiaku (LOG-04): syderyczny = ayanamsa, draconic = długość węzła
|
||||||
|
node_lon = next((p.longitude for p in positions if p.name == "North Node"), None)
|
||||||
|
off = Z.offset(zodiac, Z.julian_day(moment.when_utc), node_lon)
|
||||||
|
result["zodiac"] = zodiac
|
||||||
|
if zodiac in Z.SIDEREAL:
|
||||||
|
result["ayanamsha"] = round(off, 6)
|
||||||
|
for pdict in result["positions"]:
|
||||||
|
_shift_pos(pdict, off)
|
||||||
|
|
||||||
if not hasattr(engine, "sidereal"):
|
if not hasattr(engine, "sidereal"):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -36,21 +59,22 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
|||||||
asc = H.compute_asc(ramc, eps, moment.lat)
|
asc = H.compute_asc(ramc, eps, moment.lat)
|
||||||
mc = H.compute_mc(ramc, eps)
|
mc = H.compute_mc(ramc, eps)
|
||||||
system = house_system if house_system in H.SYSTEMS else H.WHOLE_SIGN
|
system = house_system if house_system in H.SYSTEMS else H.WHOLE_SIGN
|
||||||
cusp_list = H.cusps(asc, mc, system)
|
cusp_list = H.cusps(asc, mc, system) # tropikalne — geometria domów jest niezmiennicza
|
||||||
|
|
||||||
result["house_system"] = system
|
result["house_system"] = system
|
||||||
result["angles"] = {
|
result["angles"] = {
|
||||||
"Asc": _fmt("Asc", asc),
|
"Asc": _fmt("Asc", asc, off),
|
||||||
"MC": _fmt("MC", mc),
|
"MC": _fmt("MC", mc, off),
|
||||||
"Dsc": _fmt("Dsc", norm360(asc + 180.0)),
|
"Dsc": _fmt("Dsc", norm360(asc + 180.0), off),
|
||||||
"IC": _fmt("IC", norm360(mc + 180.0)),
|
"IC": _fmt("IC", norm360(mc + 180.0), off),
|
||||||
}
|
}
|
||||||
result["cusps"] = [
|
result["cusps"] = [
|
||||||
{"house": i + 1, "sign": SIGNS[sign_index(c)], "in_sign": in_sign(c)}
|
{"house": i + 1, "sign": SIGNS[sign_index(norm360(c - off))],
|
||||||
|
"in_sign": in_sign(norm360(c - off))}
|
||||||
for i, c in enumerate(cusp_list)
|
for i, c in enumerate(cusp_list)
|
||||||
]
|
]
|
||||||
for pdict, obj in zip(result["positions"], positions):
|
for pdict, obj in zip(result["positions"], positions):
|
||||||
pdict["house"] = H.assign_house(obj.longitude, cusp_list)
|
pdict["house"] = H.assign_house(obj.longitude, cusp_list) # dom po długości tropikalnej
|
||||||
|
|
||||||
# Lots (LOG-08) — wymagają Asc i sekty (dzień/noc)
|
# Lots (LOG-08) — wymagają Asc i sekty (dzień/noc)
|
||||||
from app.engine.firdaria import is_day_birth
|
from app.engine.firdaria import is_day_birth
|
||||||
@@ -62,9 +86,10 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
|
|||||||
result["sect"] = "day" if day else "night"
|
result["sect"] = "day" if day else "night"
|
||||||
result["lots"] = [
|
result["lots"] = [
|
||||||
{**lot,
|
{**lot,
|
||||||
"sign": SIGNS[sign_index(lot["longitude"])],
|
"longitude": decimal(norm360(lot["longitude"] - off)), # w wybranym zodiaku
|
||||||
"in_sign": in_sign(lot["longitude"]),
|
"sign": SIGNS[sign_index(norm360(lot["longitude"] - off))],
|
||||||
"house": H.assign_house(lot["longitude"], cusp_list)}
|
"in_sign": in_sign(norm360(lot["longitude"] - off)),
|
||||||
|
"house": H.assign_house(lot["longitude"], cusp_list)} # dom po długości tropikalnej
|
||||||
for lot in compute_lots(pts, day, lots_method)
|
for lot in compute_lots(pts, day, lots_method)
|
||||||
]
|
]
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
"""Systemy zodiaku (LOG-04): tropikalny, syderyczny (ayanamsy), draconic + RA.
|
||||||
|
|
||||||
|
Wszystkie pozycje silnika są liczone **tropikalnie of-date** (kontrakt LOG-28).
|
||||||
|
Zmiana zodiaku to — dla zodiaków ekliptycznych — jednolite przesunięcie długości:
|
||||||
|
|
||||||
|
długość_docelowa = (długość_tropikalna − offset) mod 360
|
||||||
|
|
||||||
|
gdzie offset to:
|
||||||
|
- **syderyczny**: ayanamsa (kąt między tropikalnym a syderycznym punktem Barana),
|
||||||
|
- **draconic**: długość wznoszącego węzła Księżyca (węzeł = 0° draconic),
|
||||||
|
- **tropikalny**: 0.
|
||||||
|
|
||||||
|
Ponieważ to stałe przesunięcie obiektu ORAZ cusps, **numery domów się nie zmieniają**
|
||||||
|
(geometria jest niezmiennicza względem obrotu) — przesuwamy tylko etykiety znaków.
|
||||||
|
|
||||||
|
Model ayanamsy: `ayan(jd) = ayan0 + B·x + C·x²`, gdzie `x = jd − J2000`. Prędkość
|
||||||
|
precesji (B, C) jest **wspólna** dla wszystkich ayanams; różni je tylko stała `ayan0`
|
||||||
|
(wybór syderycznego zera). Stałe skalibrowano do Swiss Ephemeris jako wyroczni —
|
||||||
|
zgodność do ~0,02" w latach 1900–2100 (patrz tests/test_zodiac.py).
|
||||||
|
|
||||||
|
RA (right ascension): konwersja ekliptyka→równik dla przyszłego widoku równikowego.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.engine.formats import norm360
|
||||||
|
|
||||||
|
TROPICAL = "tropical"
|
||||||
|
DRACONIC = "draconic"
|
||||||
|
|
||||||
|
# stała ayanamsy w J2000.0 (°) — skalibrowana do swisseph (get_ayanamsa_ut)
|
||||||
|
_AYAN0 = {
|
||||||
|
"lahiri": 23.857092,
|
||||||
|
"fagan_bradley": 24.740300,
|
||||||
|
"krishnamurti": 23.760240,
|
||||||
|
}
|
||||||
|
_J2000 = 2451545.0
|
||||||
|
_B = 3.824459e-5 # °/dobę — liniowy człon precesji (wspólny)
|
||||||
|
_C = 2.304e-13 # °/dobę² — drobne przyspieszenie (wspólne)
|
||||||
|
|
||||||
|
# nazwy zodiaków akceptowane przez API
|
||||||
|
SIDEREAL = tuple(f"sidereal_{k}" for k in _AYAN0) # sidereal_lahiri, ...
|
||||||
|
SYSTEMS = (TROPICAL, *SIDEREAL, DRACONIC)
|
||||||
|
|
||||||
|
|
||||||
|
def julian_day(dt: datetime) -> float:
|
||||||
|
"""Julian Day (UT) z momentu UTC — algorytm Meeusa (kalendarz gregoriański)."""
|
||||||
|
y, m = dt.year, dt.month
|
||||||
|
day = dt.day + (dt.hour + dt.minute / 60.0 + dt.second / 3600.0
|
||||||
|
+ dt.microsecond / 3.6e9) / 24.0
|
||||||
|
if m <= 2:
|
||||||
|
y -= 1
|
||||||
|
m += 12
|
||||||
|
a = y // 100
|
||||||
|
b = 2 - a + a // 4
|
||||||
|
return math.floor(365.25 * (y + 4716)) + math.floor(30.6001 * (m + 1)) + day + b - 1524.5
|
||||||
|
|
||||||
|
|
||||||
|
def ayanamsha(name: str, jd: float) -> float:
|
||||||
|
"""Ayanamsa [°] danej szkoły dla Julian Day (UT)."""
|
||||||
|
key = name[len("sidereal_"):] if name.startswith("sidereal_") else name
|
||||||
|
if key not in _AYAN0:
|
||||||
|
raise ValueError(f"Nieznana ayanamsa: {name!r} (dostępne: {', '.join(_AYAN0)})")
|
||||||
|
x = jd - _J2000
|
||||||
|
return _AYAN0[key] + _B * x + _C * x * x
|
||||||
|
|
||||||
|
|
||||||
|
def offset(zodiac: str, jd: float, node_lon: float | None = None) -> float:
|
||||||
|
"""Ile odjąć od długości tropikalnej, by dostać wybrany zodiak.
|
||||||
|
|
||||||
|
`node_lon` (tropikalna długość węzła wznoszącego) wymagana tylko dla draconic.
|
||||||
|
"""
|
||||||
|
if zodiac == TROPICAL:
|
||||||
|
return 0.0
|
||||||
|
if zodiac == DRACONIC:
|
||||||
|
if node_lon is None:
|
||||||
|
raise ValueError("draconic wymaga długości węzła (node_lon)")
|
||||||
|
return norm360(node_lon)
|
||||||
|
if zodiac in SIDEREAL:
|
||||||
|
return ayanamsha(zodiac, jd)
|
||||||
|
raise ValueError(f"Nieznany zodiak: {zodiac!r} (dostępne: {', '.join(SYSTEMS)})")
|
||||||
|
|
||||||
|
|
||||||
|
def apply(lon: float, off: float) -> float:
|
||||||
|
"""Długość w docelowym zodiaku."""
|
||||||
|
return norm360(lon - off)
|
||||||
|
|
||||||
|
|
||||||
|
def to_equatorial(lon: float, lat: float, eps: float) -> tuple[float, float]:
|
||||||
|
"""Ekliptyka (λ, β) → równik: (RA, deklinacja) w stopniach. Wszystko w °.
|
||||||
|
|
||||||
|
RA rośnie 0–360°; deklinacja w [−90, 90].
|
||||||
|
"""
|
||||||
|
lam, bet, e = math.radians(lon), math.radians(lat), math.radians(eps)
|
||||||
|
sin_dec = math.sin(bet) * math.cos(e) + math.cos(bet) * math.sin(e) * math.sin(lam)
|
||||||
|
dec = math.asin(max(-1.0, min(1.0, sin_dec)))
|
||||||
|
ra = math.atan2(
|
||||||
|
math.sin(lam) * math.cos(e) - math.tan(bet) * math.sin(e),
|
||||||
|
math.cos(lam),
|
||||||
|
)
|
||||||
|
return norm360(math.degrees(ra)), math.degrees(dec)
|
||||||
@@ -39,6 +39,7 @@ class PositionsRequest(BaseModel):
|
|||||||
objects: list[str] | None = None
|
objects: list[str] | None = None
|
||||||
house_system: str = "whole_sign" # whole_sign | equal | porphyry
|
house_system: str = "whole_sign" # whole_sign | equal | porphyry
|
||||||
stations: bool = False # licz stacje (LOG-03; wolniejsze — root-findy)
|
stations: bool = False # licz stacje (LOG-03; wolniejsze — root-findy)
|
||||||
|
zodiac: str = "tropical" # LOG-04: tropical | sidereal_{lahiri,fagan_bradley,krishnamurti} | draconic
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/query", response_model=QueryResponse)
|
@app.post("/api/query", response_model=QueryResponse)
|
||||||
@@ -58,7 +59,10 @@ def chart_positions(req: PositionsRequest) -> dict:
|
|||||||
|
|
||||||
engine = get_engine()
|
engine = get_engine()
|
||||||
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
moment = ChartMoment(when_utc=req.when_utc, lat=req.lat, lon=req.lon)
|
||||||
chart = build_chart(engine, moment, req.house_system)
|
try:
|
||||||
|
chart = build_chart(engine, moment, req.house_system, zodiac=req.zodiac)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=422, detail=str(e))
|
||||||
if req.stations:
|
if req.stations:
|
||||||
from app.engine.stations import find_stations
|
from app.engine.stations import find_stations
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
"""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
|
||||||
@@ -31,6 +31,7 @@ class LogicClient:
|
|||||||
objects: list[str] | None = None,
|
objects: list[str] | None = None,
|
||||||
house_system: str = "whole_sign",
|
house_system: str = "whole_sign",
|
||||||
stations: bool = False,
|
stations: bool = False,
|
||||||
|
zodiac: str = "tropical",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Pełny horoskop dla danego momentu — woła logic /chart/positions."""
|
"""Pełny horoskop dla danego momentu — woła logic /chart/positions."""
|
||||||
payload = {
|
payload = {
|
||||||
@@ -40,6 +41,7 @@ class LogicClient:
|
|||||||
"objects": objects,
|
"objects": objects,
|
||||||
"house_system": house_system,
|
"house_system": house_system,
|
||||||
"stations": stations,
|
"stations": stations,
|
||||||
|
"zodiac": zodiac,
|
||||||
}
|
}
|
||||||
# stacje wymagają root-findów — dłuższy timeout
|
# stacje wymagają root-findów — dłuższy timeout
|
||||||
with httpx.Client(timeout=max(settings.http_timeout, 60.0) if stations else settings.http_timeout) as client:
|
with httpx.Client(timeout=max(settings.http_timeout, 60.0) if stations else settings.http_timeout) as client:
|
||||||
|
|||||||
@@ -65,16 +65,18 @@ def chart_compute(
|
|||||||
lon: float = Form(0.0),
|
lon: float = Form(0.0),
|
||||||
house_system: str = Form("whole_sign"),
|
house_system: str = Form("whole_sign"),
|
||||||
stations: bool = Form(False),
|
stations: bool = Form(False),
|
||||||
|
zodiac: str = Form("tropical"),
|
||||||
):
|
):
|
||||||
form = {"date": date, "time": time, "tz_offset": tz_offset,
|
form = {"date": date, "time": time, "tz_offset": tz_offset,
|
||||||
"lat": lat, "lon": lon, "house_system": house_system, "stations": stations}
|
"lat": lat, "lon": lon, "house_system": house_system, "stations": stations,
|
||||||
|
"zodiac": zodiac}
|
||||||
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
|
ctx: dict = {"form": form, "result": None, "error": None, "moment": None}
|
||||||
try:
|
try:
|
||||||
iso_utc, label = _build_utc(date, time, tz_offset)
|
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||||
ctx["moment"] = label
|
ctx["moment"] = label
|
||||||
ctx["result"] = logic.positions(
|
ctx["result"] = logic.positions(
|
||||||
when_utc_iso=iso_utc, lat=lat, lon=lon,
|
when_utc_iso=iso_utc, lat=lat, lon=lon,
|
||||||
house_system=house_system, stations=stations,
|
house_system=house_system, stations=stations, zodiac=zodiac,
|
||||||
)
|
)
|
||||||
except (httpx.HTTPError,) as e:
|
except (httpx.HTTPError,) as e:
|
||||||
ctx["error"] = _logic_error(e)
|
ctx["error"] = _logic_error(e)
|
||||||
|
|||||||
@@ -32,6 +32,16 @@
|
|||||||
<option value="porphyry" {{ 'selected' if hs == 'porphyry' else '' }}>Porphyry</option>
|
<option value="porphyry" {{ 'selected' if hs == 'porphyry' else '' }}>Porphyry</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label>Zodiak
|
||||||
|
<select name="zodiac">
|
||||||
|
{% set zd = form.zodiac or 'tropical' %}
|
||||||
|
<option value="tropical" {{ 'selected' if zd == 'tropical' else '' }}>Tropikalny</option>
|
||||||
|
<option value="sidereal_lahiri" {{ 'selected' if zd == 'sidereal_lahiri' else '' }}>Syderyczny (Lahiri)</option>
|
||||||
|
<option value="sidereal_fagan_bradley" {{ 'selected' if zd == 'sidereal_fagan_bradley' else '' }}>Syderyczny (Fagan-Bradley)</option>
|
||||||
|
<option value="sidereal_krishnamurti" {{ 'selected' if zd == 'sidereal_krishnamurti' else '' }}>Syderyczny (Krishnamurti)</option>
|
||||||
|
<option value="draconic" {{ 'selected' if zd == 'draconic' else '' }}>Draconic</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="opts">
|
<div class="opts">
|
||||||
<label><input type="checkbox" name="stations" value="true" {{ 'checked' if form.stations else '' }}>
|
<label><input type="checkbox" name="stations" value="true" {{ 'checked' if form.stations else '' }}>
|
||||||
@@ -54,6 +64,7 @@
|
|||||||
Silnik: <strong>{{ result.engine }}</strong> ·
|
Silnik: <strong>{{ result.engine }}</strong> ·
|
||||||
obiektów: {{ result.positions | length }}
|
obiektów: {{ result.positions | length }}
|
||||||
{% if result.house_system %}· domy: {{ result.house_system }}{% endif %}
|
{% if result.house_system %}· domy: {{ result.house_system }}{% endif %}
|
||||||
|
{% if result.zodiac %}· zodiak: {{ result.zodiac }}{% if result.ayanamsha is defined %} (ayanamsa {{ '%.4f'|format(result.ayanamsha) }}°){% endif %}{% endif %}
|
||||||
{% if moment %}· moment: {{ moment }}{% endif %}
|
{% if moment %}· moment: {{ moment }}{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user