Swiss Ephemeris Bug: SIDEREAL + J2000 for Lunar Nodes and Apsides #
Summary #
pyswisseph silently ignores SEFLG_J2000 for four lunar bodies when
SEFLG_SIDEREAL is also set:
| Body | SIDEREAL + J2000 in pyswisseph |
|---|---|
SE_MEAN_NODE (10) |
J2000 applied correctly |
SE_MEAN_APOG (12) |
J2000 applied correctly |
SE_TRUE_NODE (11) |
J2000 silently ignored |
SE_OSCU_APOG (13) |
J2000 silently ignored |
SE_INTP_APOG (21) |
J2000 silently ignored |
SE_INTP_PERG (22) |
J2000 silently ignored |
LibEphemeris intentionally corrects this: SEFLG_J2000 is honored for
all bodies uniformly.
Impact for users: If you use SEFLG_SIDEREAL without SEFLG_J2000
(the vast majority of use cases), there is zero difference. The divergence
only affects the specific combination SEFLG_SIDEREAL | SEFLG_J2000 on
these four bodies.
How we detected it #
During systematic validation of all sidereal flag combinations against pyswisseph, we observed an internal inconsistency:
SE_MEAN_NODEwithSIDEREAL | J2000returns a different longitude than withSIDEREALalone (J2000 precession is applied).SE_TRUE_NODEwithSIDEREAL | J2000returns the same longitude as withSIDEREALalone (J2000 precession is not applied).
Bodies from the same physical family respond differently to the same flag combination. This inconsistency was confirmed across multiple dates, all 43 ayanamsha modes, and both longitude and latitude components.
No error or warning is emitted when the user requests SIDEREAL | J2000
for the affected bodies. The J2000 flag is accepted but silently
discarded.
Why this is incorrect #
1. Ayanamsha and J2000 precession are distinct operations #
-
Ayanamsha shifts the longitude zero point from the vernal equinox to a sidereal reference. It is a 1D rotation along the ecliptic longitude coordinate.
-
J2000 precession changes the reference plane from the ecliptic of date to the ecliptic of J2000.0. It is a 3D rotation that accounts for the ~47 arcseconds per century drift of the ecliptic plane.
These two operations are geometrically independent and composable. Applying ayanamsha does not substitute for J2000 precession, and vice versa.
2. Internal inconsistency #
If the design intent were that SIDEREAL and J2000 are incompatible for
lunar nodes/apsides, the behavior should be consistent across all bodies
of the same type. Instead:
- Mean bodies (
SE_MEAN_NODE,SE_MEAN_APOG) apply J2000 correctly. - True/osculating/interpolated bodies do not.
This pattern indicates a code-path issue, not a deliberate API decision.
3. The error grows with distance from J2000 #
The delta between the correct and incorrect results grows proportionally with time distance from J2000.0, consistent with missing ecliptic plane precession:
| Epoch | TrueNode delta | Physical meaning |
|---|---|---|
| J2000.0 | ~0.004° | Frame bias only |
| 2024 CE | ~0.34° | 24 years of ecliptic precession |
| J1900 | ~1.40° | 100 years of ecliptic precession |
| 3000 CE | ~14.0° | 1000 years of ecliptic precession |
If the difference were merely a convention choice, it would not produce a systematic, time-dependent error of this form.
4. Physical sanity check fails #
The true lunar node oscillates around the mean node with an amplitude of approximately ±1.5°. At any epoch, the two should be within ~2° of each other.
With the pyswisseph behavior at 0 CE:
|TrueNode - MeanNode|withSIDEREAL | J2000= ~29°- This is physically impossible.
With the LibEphemeris fix:
|TrueNode - MeanNode|withSIDEREAL | J2000= ~1.04°- This is physically correct.
Numerical evidence #
All measurements use Lahiri ayanamsha (SE_SIDM_LAHIRI).
A. SID+J2K vs SID-only (LibEphemeris, after fix) #
| Epoch | Body | SID+J2K lon | SID lon | Delta |
|---|---|---|---|---|
| 2024 | TrueNode | 15.280° | 15.618° | -0.339° |
| 2024 | OscuApog | 190.790° | 191.128° | -0.339° |
| 2024 | MeanNode | 15.774° | 16.113° | -0.339° |
| 2024 | MeanApog | 169.639° | 169.978° | -0.339° |
All four bodies show a consistent ~0.339° J2000 precession shift — as expected for a uniform coordinate transformation.
B. TrueNode vs MeanNode physical sanity #
| Epoch | With fix | Without fix (SE behavior) |
|---|---|---|
| 2024 CE | 0.49° | 0.49° |
| 0 CE | 1.04° | 28.85° |
| 3000 CE | 1.37° | 15.37° |
The LibEphemeris fix #
Algorithm #
For all Pipeline B bodies (MeanNode, MeanApog, TrueNode, OscuApog,
IntpApog, IntpPerg), when both SEFLG_SIDEREAL and SEFLG_J2000 are
set:
- Compute tropical ecliptic-of-date position.
- Subtract mean ayanamsha (not true, because the J2000 ecliptic frame has no nutation component).
- Precess from ecliptic of date to J2000 ecliptic.
- If equatorial output is also requested, rotate to J2000 equatorial using J2000 obliquity.
This is the same pipeline already used correctly for SE_MEAN_NODE and
SE_MEAN_APOG. The fix simply extends it to the remaining four bodies.
Code locations #
The fix touches two files:
-
libephemeris/fast_calc.py(LEB binary ephemeris path):- Removed
_SID_J2K_SKIP_BODIES— J2000 precession is no longer suppressed for any body. - Extended
_deferred_sid_j2kpattern to all ecliptic-direct bodies (was previously limited to mean bodies only). - Removed
_J2K_SKIPfrom ayanamsha selection — mean ayanamsha is used for all bodies when J2000 is requested.
- Removed
-
libephemeris/planets.py(Skyfield computation path):- Removed
_eff_flags = iflag & ~SEFLG_J2000logic from TrueNode, OscuApog, and IntpApog/IntpPerg handlers. These bodies now useiflagdirectly, matching the MeanNode/MeanApog pattern.
- Removed
Test coverage #
-
tests/test_sidereal/test_se_bug_j2k_nodes.py: Dedicated tests verifying J2000 is applied, physical sanity checks, LEB vs Skyfield consistency, and documented SE divergence magnitude. -
compare_scripts/tests/test_compare_sidereal_regression.py: Updated from “verify J2K suppression” to “verify intentional divergence” for the four affected bodies. -
tests/test_leb/compare/extended/test_extended_sidereal.py: Updated to verify LEB vs Skyfield agreement for SID+J2K on true bodies (was previously verifying J2K suppression).
Note on methodology #
This analysis was performed entirely through black-box behavioral observation of pyswisseph. We compared the outputs of different flag combinations across multiple dates, bodies, and ayanamsha modes. We did not inspect the Swiss Ephemeris source code. The conclusions about the nature of the bug are derived from the observed behavior, the mathematical properties of the coordinate transformations involved, and the internal inconsistency between mean and true body handling.