Try Astrologer API

Subscribe to support and grow the project.

Computation Tracing #

LibEphemeris can record which sub-backend computed each celestial body during a calculation session. This is useful for debugging, performance analysis, and understanding the auto-mode fallback chain.

Two Tracing Mechanisms #

LibEphemeris offers two complementary ways to trace computation sources:

Mechanism API Overhead Output Best for
Programmatic tracing start_tracing() / get_trace_results() ~50 ns when inactive {body_id: "source"} dict Application code, automated checks
DEBUG logging LIBEPHEMERIS_LOG_LEVEL=DEBUG Log I/O per call Structured log lines Manual debugging, test runs

Programmatic Tracing (ContextVar-based) #

The recommended approach for application code. Uses Python ContextVar for thread-safe, zero-overhead-when-inactive tracing.

Basic Usage #

import libephemeris as swe
from libephemeris.constants import SE_SUN, SE_MOON, SE_MARS, SEFLG_SPEED

# Start tracing -- returns a token for cleanup
token = swe.start_tracing()

# Compute positions as usual
swe.calc_ut(2451545.0, SE_SUN, SEFLG_SPEED)
swe.calc_ut(2451545.0, SE_MOON, SEFLG_SPEED)
swe.calc_ut(2451545.0, SE_MARS, SEFLG_SPEED)

# Retrieve results: {body_id: "source_name"}
traces = swe.get_trace_results()
print(traces)
# Example: {0: "LEB", 1: "LEB", 4: "Skyfield"}

# Clean up
token.var.reset(token)

Traced Backends #

Each successful computation records one of these source tags:

Source Description
"LEB" Precomputed Chebyshev polynomials (fastest path)
"Skyfield" NASA JPL DE440/DE441 via Skyfield
"Horizons" NASA JPL Horizons REST API
"SPK" Direct SPK kernel evaluation (minor bodies)
"ASSIST" N-body integration via REBOUND/ASSIST
"Keplerian" Analytical Keplerian orbit (last-resort fallback)

Thread Safety #

start_tracing() uses Python’s contextvars.ContextVar, so each thread gets its own independent trace accumulator:

import threading
import libephemeris as swe
from libephemeris.constants import SE_SUN, SE_MOON, SEFLG_SPEED

results = {}

def worker(name, body):
    token = swe.start_tracing()
    swe.calc_ut(2451545.0, body, SEFLG_SPEED)
    results[name] = swe.get_trace_results()
    token.var.reset(token)

t1 = threading.Thread(target=worker, args=("sun", SE_SUN))
t2 = threading.Thread(target=worker, args=("moon", SE_MOON))
t1.start(); t2.start()
t1.join(); t2.join()

# Each thread only sees its own body
print(results["sun"])   # {0: "LEB"}
print(results["moon"])  # {1: "LEB"}

Nested Tracing #

Calling start_tracing() inside an already-active session creates an independent scope. Resetting the inner token restores the outer session:

token_outer = swe.start_tracing()
swe.calc_ut(2451545.0, SE_SUN, SEFLG_SPEED)

token_inner = swe.start_tracing()
swe.calc_ut(2451545.0, SE_MOON, SEFLG_SPEED)
print(swe.get_trace_results())  # {1: "LEB"} -- inner only
token_inner.var.reset(token_inner)

print(swe.get_trace_results())  # {0: "LEB"} -- outer restored
token_outer.var.reset(token_outer)

Overwrite Behavior #

If the same body is computed multiple times, the last source wins:

token = swe.start_tracing()
swe.calc_ut(jd1, SE_SUN, SEFLG_SPEED)  # computed via LEB
swe.calc_ut(jd2, SE_SUN, SEFLG_SPEED)  # computed via Skyfield
traces = swe.get_trace_results()
print(traces[SE_SUN])  # "Skyfield" (last call wins)
token.var.reset(token)

Return Value Safety #

get_trace_results() always returns a copy of the internal dict. Mutating the returned dict does not affect the tracing session.

Performance #

When tracing is not active, the overhead is a single ContextVar.get(None) check per computation (~50 ns). When active, each dispatch point adds one dict assignment (~100 ns). In both cases the cost is negligible compared to ephemeris calculations (~5-120 us).

DEBUG Log Tracing #

For manual debugging and test runs, enable DEBUG-level logging to see per-call source information in structured log lines:

LIBEPHEMERIS_LOG_LEVEL=DEBUG pytest -s tests/

Or programmatically:

import logging
logging.getLogger("libephemeris").setLevel(logging.DEBUG)

Log output format:

[libephemeris] DEBUG: body=0 jd=2448045.9167 source=LEB
[libephemeris] DEBUG: body=15 jd=2448045.9167 source=SPK
[libephemeris] DEBUG: body=146199 jd=2448045.9167 source=ASSIST (n-body)

See Testing – Source Tracing for the full list of log-level source tags.

When to Use Which #

  • Programmatic tracing (start_tracing()): when you need structured data in application code – e.g., to verify that LEB is being used, to build diagnostic endpoints, or to assert backend selection in tests.

  • DEBUG logging: when you need human-readable output during manual debugging or CI test runs. More verbose (includes Julian Day per call), but requires log parsing to extract structured data.

Both mechanisms track the same 11 dispatch points in planets.py (9) and context.py (2).