Содержание
Как убедиться, что калькулятор натальных карт считает правильно. Все значения в этом файле — проверяемые эталоны. Числа, помеченные ✅, проверены исполнением кода.
1. Принцип послойной валидации
Ошибку нужно ловить на самом раннем слое. Проверять строго в этом порядке — каждый следующий слой опирается на предыдущий:
Слой 1. Юлианский день JD ← если сломан, сломано всё
Слой 2. ΔT и шкалы времени
Слой 3. Звёздное время GMST/LST/RAMC
Слой 4. Наклон эклиптики ε, нутация
Слой 5. Положения планет λ, β, скорость
Слой 6. Углы ASC / MC / Vertex
Слой 7. Куспиды домов
Слой 8. Аспекты, дома планет, производные точки
Типичный симптом по слоям:
| Симптом | Вероятный слой |
|---|---|
| Всё смещено на целые сутки | 1 (JD) |
| Всё смещено на 1–2 часа | Часовой пояс / DST |
| Планеты верны, ASC/MC неверны | 3 или 6 |
| ASC верен, MC неверен (или наоборот) | 6, квадранты |
| ASC/MC верны, куспиды неверны | 7 |
| Всё смещено на ~24° | Перепутан тропический/сидерический зодиак |
| Знак долготы места перепутан | Западная долгота как положительная |
| Ошибка растёт для древних дат | 2 (ΔT) |
2. Слой 1: Юлианский день
2.1. Алгоритм (Meeus, Astronomical Algorithms, гл. 7)
import math
def julian_day(Y, M, D, gregorian=True):
"""
Y — год (астрономический счёт: 1 до н.э. = 0, 2 до н.э. = -1)
M — месяц 1..12
D — день с дробной частью (D = day + hour/24 + min/1440 + sec/86400)
gregorian — True для дат после 1582-10-15, False для юлианского календаря
"""
if M <= 2:
Y -= 1
M += 12
if gregorian:
A = Y // 100
B = 2 - A + A // 4
else:
B = 0
return (math.floor(365.25 * (Y + 4716))
+ math.floor(30.6001 * (M + 1))
+ D + B - 1524.5)
⚠️ Граница календарей: 1582-10-04 (юлианский) сразу сменяется на 1582-10-15 (григорианский). Дат 5–14 октября 1582 не существует. Но переход на григорианский календарь в разных странах происходил в разные годы (Россия — 1918!). Для астрологии дат до 1918 в России нужна осторожность: дата рождения обычно записана по юлианскому календарю («старый стиль»).
2.2. Эталонные значения ✅
Проверено исполнением кода:
| Дата (UT) | Ожидаемый JD | Календарь | Статус |
|---|---|---|---|
| 2000-01-01 12:00 | 2451545.0 | григ. | ✅ |
| 1987-01-27 00:00 | 2446822.5 | григ. | ✅ |
| 1987-04-10 00:00 | 2446895.5 | григ. | ✅ |
| 1957-10-04 19:26:24 | 2436116.31 | григ. | ✅ |
| 1900-01-01 00:00 | 2415020.5 | григ. | |
| 1600-01-01 00:00 | 2305447.5 | григ. | |
| 837-04-10 07:12 | 2026871.8 | юлиан. | |
| −1000-07-12 12:00 | 1356001.0 | юлиан. | |
| −4712-01-01 12:00 | 0.0 | юлиан. | ✅ |
2.3. Обратное преобразование
Обязательно тестировать round-trip: revjul(julday(d)) == d для случайных дат в диапазоне −4000…+3000.
def jd_to_date(jd):
jd += 0.5
Z = int(jd); F = jd - Z
if Z < 2299161:
A = Z
else:
alpha = int((Z - 1867216.25) / 36524.25)
A = Z + 1 + alpha - alpha // 4
B = A + 1524
C = int((B - 122.1) / 365.25)
D = int(365.25 * C)
E = int((B - D) / 30.6001)
day = B - D - int(30.6001 * E) + F
month = E - 1 if E < 14 else E - 13
year = C - 4716 if month > 2 else C - 4715
return year, month, day
3. Слой 2: ΔT (Delta-T)
TT = UT + ΔT. Эфемериды считаются в TT, а время рождения задано в UT.
3.1. Порядок величины по эпохам
| Год | ΔT, секунды | Влияние на λ Луны (0.55″/с) |
|---|---|---|
| −500 | ≈ +17190 | ≈ 2.6° |
| 0 | ≈ +10580 | ≈ 1.6° |
| 1000 | ≈ +1570 | ≈ 14′ |
| 1600 | ≈ +120 | ≈ 66″ |
| 1700 | ≈ +9 | ≈ 5″ |
| 1800 | ≈ +13.7 | ≈ 7.5″ |
| 1900 | ≈ −2.8 | ≈ 1.5″ |
| 1950 | ≈ +29.1 | ≈ 16″ |
| 2000 | ≈ +63.8 | ≈ 35″ |
| 2020 | ≈ +69.4 | ≈ 38″ |
| 2025 | ≈ +69.2 | ≈ 38″ |
Вывод для практики: для дат XX–XXI века ΔT влияет на положение Луны на ~0.5′ и на ASC на ~0.3′ — заметно, но не критично. Для дат до 1700 г. игнорирование ΔT даёт грубую ошибку.
3.2. Проверка
Для современных дат ΔT известно из наблюдений (таблицы IERS). Swiss Ephemeris swe_deltat() возвращает его в сутках, не секундах — частая ошибка.
import swisseph as swe
dt_days = swe.deltat(2451545.0)
print(dt_days * 86400) # ≈ 63.83 секунды
4. Слой 3: Звёздное время
4.1. Формула GMST (IAU 1982, Meeus 12.4)
Для любого момента UT (не только 0h):
GMST° = 280.46061837
+ 360.98564736629 × (JD − 2451545.0)
+ 0.000387933 × T²
− T³ / 38 710 000
где T = (JD − 2451545.0) / 36525.
Результат нормализуется к [0, 360). В часах: / 15.
4.2. Эталон ✅
Проверено исполнением:
| Момент | JD | GMST ожидаемый | Получено |
|---|---|---|---|
| 1987-04-10 00:00 UT | 2446895.5 | 13ч 10м 46.3668с | 13ч 10м 46.3668с ✅ |
Это канонический пример из Meeus, пример 12.a. Если ваш код даёт это значение — слой 3 верен.
4.3. LST и RAMC
LST (часы) = GMST (часы) + λ_geo / 15 (λ_geo: восток +, запад −)
RAMC (°) = LST × 15 = GMST° + λ_geo
Нормализовать к [0, 360).
⚠️ Для строгости нужно GAST = GMST + уравнение равноденствий (нутация), но эффект < 1.1 с времени = < 0.005° — ниже порога значимости для астрологии.
4.4. Быстрая проверка на здравый смысл
В день весеннего равноденствия около полуночи LST ≈ 12ч; в полдень LST ≈ 0ч. Звёздные сутки короче солнечных на 3м56с, поэтому LST в фиксированное гражданское время нарастает примерно на 2 часа в месяц.
5. Слой 4: Наклон эклиптики
5.1. Средний наклон (IAU 1980)
ε₀ = 23°26′21.448″ − 46.8150″·T − 0.00059″·T² + 0.001813″·T³
В десятичных градусах:
ε₀ = 23.439291111 − 0.0130041667·T − 1.6389e-7·T² + 5.036e-7·T³
5.2. Эталон ✅
| Эпоха | T | ε₀ |
|---|---|---|
| J2000.0 | 0 | 23.43929111° = 23°26′21.448″ ✅ |
| 2025.0 | 0.25 | ≈ 23.43604° = 23°26′09.7″ |
| 1900.0 | −1.0 | ≈ 23.45229° = 23°27′08.3″ |
Убывает примерно на 0.47″/год.
5.3. Истинный наклон
ε = ε₀ + Δε (Δε — нутация в наклоне, до ±9.2″)
Для астрологии разница ε₀ vs ε даёт сдвиг ASC на < 0.01° — обычно пренебрежимо, но Swiss Ephemeris учитывает.
6. Слой 6: Углы ASC и MC — главный источник ошибок
6.1. Формулы
MC = arctan2( sin(RAMC), cos(RAMC)·cos(ε) )
ASC = arctan2( cos(RAMC),
−( sin(RAMC)·cos(ε) + tan(φ)·sin(ε) ) )
Обе — через двухаргументный arctan2, результат нормализовать к [0, 360).
6.2. Контрольные соотношения (инварианты)
Эти проверки должны выполняться всегда, для любых входных данных. Отличный материал для property-based тестов:
| Инвариант | Условие |
|---|---|
DSC = ASC + 180° | всегда |
IC = MC + 180° | всегда |
| ASC находится в «восточной» половине относительно MC | ASC должен лежать в интервале (MC, MC+180°) при движении против часовой стрелки — иначе потерян квадрант |
| На экваторе (φ=0) | ASC − MC ≈ 90° точно только при RAMC = 0 или 180 |
| При φ → ±90° | ASC вырождается; Placidus/Koch не определены |
| RAMC = 0° | MC = 0° (♈0°) |
| RAMC = 90° | MC = 90° (♋0°) |
| RAMC = 180° | MC = 180° (♎0°) |
| RAMC = 270° | MC = 270° (♑0°) |
6.3. Тест на симметрию полушарий
Для широты +φ и −φ при одинаковом RAMC:
- MC одинаков (не зависит от широты)
- ASC различается — и это правильно
6.4. Знаменитая ловушка: MC ниже горизонта
В полярных областях MC может оказаться под горизонтом. Формула всё равно верна: MC — это точка эклиптики на верхнем меридиане, а не «самая высокая точка эклиптики».
7. Слой 7: Куспиды домов
7.1. Универсальные инварианты
Независимо от системы домов:
| Проверка | Условие |
|---|---|
| Куспидов ровно 12 | len(cusps) == 12 |
| Монотонность | При обходе cusp[i] → cusp[i+1] всегда движение вперёд по зодиаку |
| Замкнутость | Сумма 12 дуг = ровно 360° |
| Оппозиции | cusp[7] = cusp[1] + 180°, cusp[8] = cusp[2] + 180°, … cusp[12] = cusp[6] + 180° — для всех квадрантных систем |
| Куспид I = ASC | Для всех, кроме Whole Sign и некоторых равнодомных вариантов |
| Куспид X = MC | Для квадрантных систем (Placidus, Koch, Campanus, Regiomontanus, Porphyry, Alcabitius, Topocentric). Не для Equal и Whole Sign |
| Равные дома | Для Equal: все дуги ровно 30° |
| Whole Sign | Все куспиды кратны 30° |
7.2. Тест сходимости Placidus
Placidus решается итеративно. Проверить:
- Сходимость за < 20 итераций при |φ| < 60°
- Корректный отказ (или fallback на Porphyry) при |φ| > 66°33′
7.3. Сравнение систем на одних данных
Полезный интеграционный тест: посчитать куспиды всеми системами для одной карты и убедиться, что:
- ASC и MC совпадают во всех квадрантных системах (с точностью до 1e-9)
- Porphyry даёт ровно треть дуги ASC→MC
8. Эталонные карты для сквозного теста
8.1. Как получить ground truth
Источник истины — astro.com (Astrodienst), раздел Extended Chart Selection. Там же доступен вывод в виде таблицы позиций с точностью до секунды дуги. Swiss Ephemeris — это движок astro.com, поэтому совпадение должно быть до 0.01″.
Альтернативы для перекрёстной проверки:
- JPL Horizons (ssd.jpl.nasa.gov/horizons) — эталон для геоцентрических координат планет
- Swiss Ephemeris test suite — утилита
swetestиз дистрибутива
8.2. Рекомендуемый набор тестовых карт
Подбирать так, чтобы покрыть краевые случаи:
| № | Сценарий | Что проверяет |
|---|---|---|
| 1 | Обычная дата, средняя широта (напр. Москва 55.75N, 2000-06-15 12:00 MSK) | Базовый путь |
| 2 | Южное полушарие (Сидней −33.87, 151.21) | Знак широты |
| 3 | Западная долгота (Нью-Йорк 40.71N, −74.01) | Знак долготы |
| 4 | Полярная широта (Мурманск 68.97N) | Отказ Placidus/Koch |
| 5 | Заполярье зимой (Лонгйир 78.22N) | Вырождение систем домов |
| 6 | Экватор (φ = 0) | Деление на ноль в tan(φ) |
| 7 | Момент, когда ASC ровно 0° знака | Граничные условия |
| 8 | Дата до 1582 (юлианский календарь) | Ветка календаря |
| 9 | Древняя дата (−500 г.) | ΔT |
| 10 | Дата в период DST | Часовой пояс |
| 11 | Дата смены часового пояса в стране | Историческая tz |
| 12 | Ретроградный Меркурий | Знак скорости |
| 13 | Планета out-of-bounds (|δ| > 23°26′) | Склонение |
| 14 | Соединение через 0° Овна (напр. 359° и 1°) | Wraparound в аспектах |
8.3. Шаблон теста
def test_chart(name, dt_utc, lat, lon, expected):
"""expected — dict с эталонными значениями из astro.com"""
chart = compute_chart(dt_utc, lat, lon)
for body, exp_lon in expected['planets'].items():
got = chart['planets'][body]['lon']
# допуск 0.01° = 36 угловых секунд — с запасом
assert abs(norm180(got - exp_lon)) < 0.01, \
f"{name}/{body}: got {got:.6f}, expected {exp_lon:.6f}"
assert abs(norm180(chart['asc'] - expected['asc'])) < 0.01
assert abs(norm180(chart['mc'] - expected['mc'])) < 0.01
8.4. Допуски
| Величина | Разумный допуск | Комментарий |
|---|---|---|
| Планеты (Swiss vs Swiss) | 1e-6 ° | Побитовое совпадение |
| Планеты (Moshier vs Swiss) | 0.001 ° ≈ 3.6″ | Moshier чуть грубее |
| Планеты (своя VSOP87 vs Swiss) | 0.01 ° | Усечённые ряды |
| Луна | 0.005 ° | Самая сложная |
| ASC / MC | 0.001 ° | Чистая тригонометрия, должно совпадать точно |
| Куспиды Placidus | 0.01 ° | Итерации |
| ΔT | 0.1 с для XX–XXI вв. |
9. Каталог типовых багов
Отсортировано по частоте встречаемости в реальных проектах.
9.1. Знак географической долготы ⚠️ №1
# НЕВЕРНО для Нью-Йорка
lon = 74.0060 # это где-то в Узбекистане
# ВЕРНО
lon = -74.0060
Симптом: ASC/MC неверны, планеты верны. Ошибка в RAMC равна удвоенной долготе.
9.2. Часовой пояс и DST ⚠️ №2
# НЕВЕРНО — наивный datetime
dt = datetime(1985, 7, 4, 14, 30)
# ВЕРНО — с историческим правилом зоны
from zoneinfo import ZoneInfo
dt = datetime(1985, 7, 4, 14, 30, tzinfo=ZoneInfo("Europe/Moscow"))
dt_utc = dt.astimezone(ZoneInfo("UTC"))
Симптом: ASC сдвинут ровно на ~15° или ~30° (1 или 2 часа).
9.3. Потеря квадранта в arctan
# НЕВЕРНО
mc = math.degrees(math.atan(math.tan(ramc_rad) / math.cos(eps_rad)))
# ВЕРНО
mc = math.degrees(math.atan2(math.sin(ramc_rad),
math.cos(ramc_rad) * math.cos(eps_rad))) % 360
Симптом: MC или ASC периодически отличаются ровно на 180°.
9.4. Градусы vs радианы
Python math.sin() ждёт радианы. Забытый math.radians() — классика.
Симптом: полная бессмыслица в результатах.
9.5. Wraparound в аспектах
# НЕВЕРНО — не найдёт соединение 359° и 1°
if abs(lon1 - lon2) < orb: ...
# ВЕРНО
diff = abs(((lon1 - lon2 + 180) % 360) - 180)
if diff < orb: ...
9.6. Определение дома планеты через wraparound
Дом XII часто пересекает 0° Овна. Наивное cusp[i] <= lon < cusp[i+1] сломается.
def house_of(lon, cusps):
"""cusps — список из 12 куспидов в градусах."""
lon = lon % 360
for i in range(12):
a = cusps[i] % 360
b = cusps[(i + 1) % 12] % 360
span = (b - a) % 360
offset = (lon - a) % 360
if offset < span:
return i + 1
return 12
9.7. ΔT в сутках vs секундах
swe_deltat() возвращает сутки. Умножить на 86400 для секунд.
9.8. Тропический vs сидерический
Забытый вызов swe.set_sid_mode() или флаг SEFLG_SIDEREAL.
Симптом: всё смещено ровно на величину аянамсы (~24°).
9.9. Астрономический год vs год до н.э.
Нет «нулевого года» в историческом счёте: 1 до н.э. → 1 н.э. В астрономическом счёте: 1 до н.э. = 0, 2 до н.э. = −1, 100 до н.э. = −99. Симптом: ошибка ровно в 1 год для дат до н.э.
9.10. Скорость планеты не запрошена
Без флага SEFLG_SPEED поле скорости в результате — мусор, и ретроградность определяется неверно.
9.11. Полярные широты
Placidus и Koch математически не существуют при |φ| > 66°33′ (полярный круг). Нужен явный fallback (обычно Porphyry или Whole Sign) и предупреждение пользователю.
9.12. Юлианский vs григорианский календарь
Для России даты до 1918 г. записаны по старому стилю. Разница в XX веке — 13 суток.
10. Быстрый чек-лист приёмки
Перед тем как считать калькулятор готовым:
- JD round-trip для 10 000 случайных дат в диапазоне −4000…+3000
- JD совпадает с эталонами из §2.2
- GMST совпадает с эталоном Meeus 12.a (13ч10м46.3668с) ✅
- ε при T=0 равно 23.43929111° ✅
- ΔT для J2000 ≈ 63.8 с
-
DSC = ASC + 180иIC = MC + 180для 1000 случайных входов - Сумма 12 дуг домов = 360° для всех систем
- Оппозиции куспидов (1↔7, 2↔8, …) для квадрантных систем
- Placidus/Koch корректно отказывают при φ = 70°
- Equal: все дуги ровно 30°; Whole Sign: все куспиды кратны 30°
- Сквозное сравнение ≥ 10 карт с astro.com, допуск 0.01°
- Покрыты все 14 сценариев из §8.2
- Аспект между 359° и 1° определяется как соединение
- Планета на куспиде попадает в правильный дом
- Ретроградность Меркурия совпадает с эфемеридами
- Сидерический режим даёт сдвиг ≈ 24° относительно тропического
11. Источники эталонов
| Источник | URL | Что даёт |
|---|---|---|
| Astrodienst | astro.com | Эталонные карты, движок Swiss Ephemeris |
| JPL Horizons | ssd.jpl.nasa.gov/horizons | Эталонные эфемериды планет |
| IERS | iers.org | Официальные ΔT, параметры вращения Земли |
| Meeus, Astronomical Algorithms | книга, 2-е изд. 1998 | Все базовые формулы с worked examples |
| Swiss Ephemeris docs | astro.com/swisseph | API, точности, тесты |
| USNO | aa.usno.navy.mil | Астрономические данные |