Натальная карта

Валидация и тестовые случаи

Послойная проверка, эталонные карты, допуски и каталог типовых багов.

15 мин чтения 481 строк · docs/08-spravochniki/validatsiya-i-testovye-sluchai.md
Содержание

Как убедиться, что калькулятор натальных карт считает правильно. Все значения в этом файле — проверяемые эталоны. Числа, помеченные ✅, проверены исполнением кода.


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:002451545.0григ.✅
1987-01-27 00:002446822.5григ.✅
1987-04-10 00:002446895.5григ.✅
1957-10-04 19:26:242436116.31григ.✅
1900-01-01 00:002415020.5григ.
1600-01-01 00:002305447.5григ.
837-04-10 07:122026871.8юлиан.
−1000-07-12 12:001356001.0юлиан.
−4712-01-01 12:000.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. Эталон ✅

Проверено исполнением:

МоментJDGMST ожидаемыйПолучено
1987-04-10 00:00 UT2446895.513ч 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.0023.43929111° = 23°26′21.448″ ✅
2025.00.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 находится в «восточной» половине относительно MCASC должен лежать в интервале (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. Универсальные инварианты

Независимо от системы домов:

ПроверкаУсловие
Куспидов ровно 12len(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 / MC0.001 °Чистая тригонометрия, должно совпадать точно
Куспиды Placidus0.01 °Итерации
ΔT0.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Что даёт
Astrodienstastro.comЭталонные карты, движок Swiss Ephemeris
JPL Horizonsssd.jpl.nasa.gov/horizonsЭталонные эфемериды планет
IERSiers.orgОфициальные ΔT, параметры вращения Земли
Meeus, Astronomical Algorithmsкнига, 2-е изд. 1998Все базовые формулы с worked examples
Swiss Ephemeris docsastro.com/swissephAPI, точности, тесты
USNOaa.usno.navy.milАстрономические данные