Lots / punkty arabskie — 7 Lots hermetycznych (LOG-08) #2

Merged
gitea merged 2 commits from feat/lots-log08 into master 2026-07-20 18:50:55 +00:00
7 changed files with 166 additions and 2 deletions
Showing only changes of commit 4c0f5f2b26 - Show all commits
+1 -1
View File
@@ -6,7 +6,7 @@ i nie w bazie.
## API
- `POST /api/query``QueryRequest``QueryResponse`
- `POST /chart/positions``{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
- `POST /chart/positions``{when_utc, lat, lon, house_system?}` → pełny horoskop: pozycje (LOG-01) + osie i domy (LOG-05) + aspekty główne z applying/separating (LOG-06) + opcjonalnie stacje planet (`stations:true`, LOG-03). Zwraca też sektę i 7 Lots hermetycznych z domami (LOG-08). Obiekty: 10 planet + mean NN/SN/Lilith (LOG-02). `house_system`: `whole_sign` (dom.) / `equal` / `porphyry`.
- `POST /chart/report``{when_utc, lat, lon, limit?}` → wynik obliczeń wyszukany w bazie: fasety sygnifikatorów **w znaku / w domu / w aspekcie**, z rozwinięciem skrótów, odsiewaniem duplikatów (ten sam sygnifikator i opis), rankingiem siły (LOG-21) oraz opcją group (grupowanie identycznych opisów)
- `POST /chart/profections``{when_utc, lat, lon, start_age?, count?}` → profekcje roczne: wiek, profektowany Asc, Władca Roku (+MC/Su/Mo) (LOG-10)
- `POST /chart/return``{when_utc, lat, lon, kind, around?}` → Solar/Lunar Return: moment powrotu + pełny horoskop na ten moment (LOG-12)
+2
View File
@@ -24,6 +24,8 @@ ABBREVIATIONS: dict[str, str] = {
"NN": "North Node", "SN": "South Node", "Lilith": "Lilith", "Chiron": "Chiron",
# osie
"Asc": "Ascendant", "Dsc": "Descendant", "MC": "Midheaven", "IC": "Imum Coeli",
# Lots (punkty arabskie)
"PF": "Part of Fortune", "Fortune": "Part of Fortune", "Spirit": "Lot of Spirit",
# aspekty
"conj": "conjunction", "sex": "sextile", "sq": "square", "tri": "trine",
"opp": "opposition", "semisex": "semisextile", "semisq": "semisquare",
+18 -1
View File
@@ -21,7 +21,8 @@ def _fmt(name: str, lon: float) -> dict:
}
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN) -> dict:
def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str = H.WHOLE_SIGN,
lots_method: str = "degree") -> dict:
from app.engine.aspects import find_aspects
positions = engine.positions(moment)
@@ -50,4 +51,20 @@ def build_chart(engine: EphemerisEngine, moment: ChartMoment, house_system: str
]
for pdict, obj in zip(result["positions"], positions):
pdict["house"] = H.assign_house(obj.longitude, cusp_list)
# Lots (LOG-08) — wymagają Asc i sekty (dzień/noc)
from app.engine.firdaria import is_day_birth
from app.engine.lots import compute_lots
pts = {p.name: p.longitude for p in positions}
pts["Asc"] = asc
day = is_day_birth(pts["Sun"], asc, mc) if "Sun" in pts else True
result["sect"] = "day" if day else "night"
result["lots"] = [
{**lot,
"sign": SIGNS[sign_index(lot["longitude"])],
"in_sign": in_sign(lot["longitude"]),
"house": H.assign_house(lot["longitude"], cusp_list)}
for lot in compute_lots(pts, day, lots_method)
]
return result
+60
View File
@@ -0,0 +1,60 @@
"""Lots / punkty arabskie (LOG-08) — 7 Lots hermetycznych.
Formuła: Lot = C + A B (od punktu C odmierzamy odległość między A i B).
Większość Lots **odwraca się w horoskopach nocnych** (zamiana A↔B) — np.
Fortuna: dzień Asc + Mo Su, noc Asc + Su Mo.
Dwa warianty liczenia (notes3):
- `degree` (domyślny) — dokładny stopień,
- `sign` — liczone całymi znakami (Lot wypada na 0° wyliczonego znaku).
Kolejność ma znaczenie: Fortuna i Duch liczone są pierwsze, bo pozostałe Lots
odwołują się do nich.
"""
from __future__ import annotations
from app.engine.formats import norm360, sign_index
# (nazwa, C, A, B, odwracalny w nocy)
LOT_DEFS: list[tuple[str, str, str, str, bool]] = [
("Fortune", "Asc", "Moon", "Sun", True),
("Spirit", "Asc", "Sun", "Moon", True),
("Eros", "Asc", "Venus", "Spirit", True),
("Necessity", "Asc", "Fortune", "Mercury", True),
("Courage", "Asc", "Fortune", "Mars", True),
("Victory", "Asc", "Jupiter", "Spirit", True),
("Nemesis", "Asc", "Fortune", "Saturn", True),
]
METHODS = ("degree", "sign")
def compute_lots(
points: dict[str, float], is_day: bool, method: str = "degree"
) -> list[dict]:
"""points: nazwa → długość natalna (wymagane Asc + planety formuł).
Zwraca listę {name, longitude, formula} w kolejności definicji.
"""
if method not in METHODS:
raise ValueError(f"nieznana metoda liczenia Lots: {method}")
vals = dict(points)
out: list[dict] = []
for name, c, a, b, reversible in LOT_DEFS:
first, second = (a, b) if (is_day or not reversible) else (b, a)
if any(k not in vals for k in (c, first, second)):
continue # brak składnika — pomijamy
if method == "sign":
idx = (sign_index(vals[c]) + sign_index(vals[first])
- sign_index(vals[second])) % 12
lon = idx * 30.0
else:
lon = norm360(vals[c] + vals[first] - vals[second])
vals[name] = lon # dostępny dla kolejnych Lots
out.append({
"name": name,
"longitude": lon,
"formula": f"{c} + {first} {second}",
})
return out
+2
View File
@@ -22,6 +22,8 @@ PLANET_ABBR = {
"Jupiter": "Ju", "Saturn": "Sa", "Uranus": "Ur", "Neptune": "Ne", "Pluto": "Pl",
# punkty wirtualne — tokeny wg SIGNIFICATORS KEY ([NN, [SN, [Lilith)
"North Node": "NN", "South Node": "SN", "Lilith": "Lilith",
# Lots — w bazie Fortuna występuje jako [PF (Part of Fortune)
"Fortune": "PF", "Spirit": "Spirit",
}
SIGN_TO_ABBR = dict(zip(SIGNS, SIGN_ABBR))
+71
View File
@@ -0,0 +1,71 @@
"""Lots / punkty arabskie (LOG-08).
Wyrocznie z notes3 (astro-seek, horoskop referencyjny 30.04.1984, urodzenie DZIENNE):
- Fortuna wprost w tabeli obiektów: Cancer 12°35'24" = 102.5900°
- Ducha (Spirit) potwierdza jego antyscja: Taurus 28°14' -> Spirit = 180 58.2333
"""
import pytest
from app.engine.formats import sign_index
from app.engine.lots import LOT_DEFS, compute_lots
# natalne długości horoskopu referencyjnego
NATAL = {"Asc": 112.1786, "Sun": 40.1392, "Moon": 30.5514, "Mercury": 27.3839,
"Venus": 27.6769, "Mars": 234.5411, "Jupiter": 282.9617, "Saturn": 223.3108}
FORTUNE_REF = 102.5900 # Cancer 12°35'24"
SPIRIT_REF = 180.0 - 58.2333 # z antyscji Taurus 28°14'
def _by_name(lots):
return {lot["name"]: lot["longitude"] for lot in lots}
def _arcmin(a, b):
return abs(((a - b + 180.0) % 360.0) - 180.0) * 60.0
def test_fortune_matches_astroseek():
lots = _by_name(compute_lots(NATAL, is_day=True))
assert _arcmin(lots["Fortune"], FORTUNE_REF) < 2.0
assert sign_index(lots["Fortune"]) == 3 # Cancer
def test_spirit_matches_antiscia_reference():
lots = _by_name(compute_lots(NATAL, is_day=True))
assert _arcmin(lots["Spirit"], SPIRIT_REF) < 2.0
def test_fortune_and_spirit_reverse_at_night():
day = _by_name(compute_lots(NATAL, is_day=True))
night = _by_name(compute_lots(NATAL, is_day=False))
# nocą formuła się odwraca: Fortuna nocna = Duch dzienny (i odwrotnie)
assert _arcmin(night["Fortune"], day["Spirit"]) < 0.01
assert _arcmin(night["Spirit"], day["Fortune"]) < 0.01
def test_all_seven_hermetic_lots_present():
lots = compute_lots(NATAL, is_day=True)
assert [lot["name"] for lot in lots] == [d[0] for d in LOT_DEFS]
assert len(lots) == 7
def test_derived_lots_use_fortune_and_spirit():
lots = _by_name(compute_lots(NATAL, is_day=True))
# Necessity = Asc + Fortune Mercury
expected = (NATAL["Asc"] + lots["Fortune"] - NATAL["Mercury"]) % 360.0
assert _arcmin(lots["Necessity"], expected) < 0.01
# Eros = Asc + Venus Spirit
expected_eros = (NATAL["Asc"] + NATAL["Venus"] - lots["Spirit"]) % 360.0
assert _arcmin(lots["Eros"], expected_eros) < 0.01
def test_by_sign_method_lands_on_sign_start():
lots = compute_lots(NATAL, is_day=True, method="sign")
for lot in lots:
assert lot["longitude"] % 30.0 == 0.0 # 0° wyliczonego znaku
def test_unknown_method_rejected():
with pytest.raises(ValueError):
compute_lots(NATAL, is_day=True, method="bzdura")
@@ -88,6 +88,18 @@
</tbody>
</table>
{% if result.lots %}
<div class="meta">Lots hermetyczne ({{ result.lots | length }}) · sekta: <strong>{{ result.sect }}</strong></div>
<table class="angles">
<thead><tr><th>Lot</th><th>Znak</th><th>W znaku</th><th>Dom</th><th>Formuła</th></tr></thead>
<tbody>
{% for l in result.lots %}
<tr><td>{{ l.name }}</td><td>{{ l.sign }}</td><td class="mono">{{ l.in_sign }}</td><td>{{ l.house }}</td><td class="muted small">{{ l.formula }}</td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% set with_stations = result.positions | selectattr('stations', 'defined') | list %}
{% if with_stations %}
<div class="meta">Stacje planet (poprzednia / następna; <span class="badge">blisko</span> = mniej niż 7 dni)</div>