Chapter 14 — Precision, Performance, and LEB Mode #
What You Will Learn #
In this chapter, you will discover how precise LibEphemeris is compared to reference astronomical sources, how precision behaves over different time scales (from antiquity to the distant future), what the LEB mode (precomputed binary ephemerides) is and how to optimize calculation speed, how to configure calculation modes and precision tiers, and how to manage data downloading and caching.
14.1 How precise is this library? #
A legitimate question before entrusting your calculations to any software: how much can we trust the results?
The short answer is: for modern use (1900–2100), the precision is sub-arcsecond for all bodies. This means the error is less than 1/3600 of a degree — invisible for any practical, astrological, or amateur astronomical purpose.
Let’s look at the details by category.
Main Planets #
LibEphemeris directly uses the JPL DE440 ephemerides (or DE441 for extended dates), the same ones produced by NASA’s Jet Propulsion Laboratory for space missions. We are not “approximating” the planets’ positions: we are reading the exact same data that NASA uses to navigate its space probes.
The precision is sub-milliarcsecond for the Sun, Moon, and planets — the maximum achievable with current technology:
import libephemeris as ephem
# Position of the Sun at J2000.0 (January 1, 2000, noon)
jd = ephem.julday(2000, 1, 1, 12.0)
pos, _ = ephem.calc_ut(jd, ephem.SE_SUN, ephem.SEFLG_SPEED)
print(f"Sun at J2000.0:")
print(f" Longitude: {pos[0]:.9f}°")
print(f" Latitude: {pos[1]:.9f}°")
print(f" Distance: {pos[2]:.9f} AU")
print(f" Speed: {pos[3]:.6f}°/day")
Sun at J2000.0:
Longitude: 280.368917535°
Latitude: 0.000227101°
Distance: 0.983328100 AU
Speed: 1.019428°/day
Moon #
The Moon is the most complex celestial body to calculate. Its gravitational perturbations (due to the Sun, Jupiter, and the non-spherical shape of the Earth) require thousands of terms. LibEphemeris’s precision for the Moon is sub-arcsecond, identical to that of the JPL ephemerides.
Fixed Stars #
The positions of the fixed stars come from the Hipparcos catalog (ESA, 1997), with an original precision of about 1 milliarcsecond. The propagation of proper motion introduces an error that grows over time, but for a few centuries from the catalog (epoch J2000.0) the precision remains under 0.5".
Astrological Houses #
Cusp calculations are purely geometric (depending on ARMC, latitude, and obliquity of the ecliptic). The precision is about 0.02" for all house systems — practically identical to any other implementation.
Ayanamsha #
The ayanamsha (precession for the sidereal zodiac) has a precision of about 0.07" compared to other implementations. The differences between various software are due to model choices (which precession formula to use, which reference epoch), not calculation errors.
Where We Are More Precise #
LibEphemeris uses more recent sources than many other software for two specific calculations:
- Planetary magnitudes: implementation based on Mallama & Hilton (2018), more precise than the 1980s models used elsewhere
- Apparent diameters: IAU 2015 values, updated from historical constants
Delta T: Where Precision Drops #
Delta T (the difference TT − UT) is the Achilles’ heel of any historical ephemeris. For dates far from the present, Delta T is estimated using empirical formulas whose uncertainty grows rapidly:
import libephemeris as ephem
# Delta T at different epochs
for anno in [1600, 1800, 1900, 1950, 2000, 2024]:
jd = ephem.julday(anno, 1, 1, 12.0)
dt = ephem.deltat(jd)
print(f"Year {anno}: Delta T = {dt * 86400:7.2f} seconds")
Year 1600: Delta T = 109.11 seconds
Year 1800: Delta T = 18.37 seconds
Year 1900: Delta T = -1.97 seconds
Year 1950: Delta T = 28.93 seconds
Year 2000: Delta T = 63.83 seconds
Year 2024: Delta T = 69.18 seconds
For the year 2000, Delta T is known with sub-millisecond precision (thanks to IERS data). For the year 1600, the uncertainty is several seconds. For the year -500 (ancient astronomy), the uncertainty can be minutes — which translates to significant errors in the Moon’s position.
14.2 Time Scales of Error #
Not all calculations have the same temporal validity. Every component of the library has a “precision horizon” beyond which the error grows.
JPL Ephemerides #
JPL ephemerides are generated by integrating the equations of motion of the Solar System. Each version covers a specific range:
- DE440 (
mediumtier): 1550–2650 — full precision for over 1000 years - DE440s (
basetier): 1849–2150 — lightweight version, sufficient for modern use - DE441 (
extendedtier): -13200 to +17191 — for historical and prehistoric research
Outside the chosen ephemeris’ range, the library raises an error: it does not produce false results.
Delta T #
Before about 1600, the uncertainty of Delta T becomes the dominant error factor. For the Moon (which moves by ~0.5"/second), a 30-second error in Delta T produces an error of ~15" in position — still acceptable for astrology, but not for precise historical eclipses.
Fixed Stars #
The proper motion of stars is measured precisely by the Hipparcos catalog (epoch J2000.0). The propagation is linear and precise for a few centuries from the catalog. For very ancient or future dates, the actual proper motion might be non-linear (due to binary companions, gravitational perturbations).
Minor Bodies (Keplerian) #
For asteroids and comets calculated with Keplerian propagation (without SPK files), precision degrades rapidly — arcminutes in just a few months. For this reason, the library supports the automatic download of high-precision SPK files from JPL Horizons.
Rule of Thumb #
For modern astrology (1900–2100), precision is sub-arcsecond for all main bodies, houses, and ayanamsha. There is no reason to worry about precision for this range.
14.3 Precision Tiers #
LibEphemeris organizes ephemerides into three precision tiers, each with a different trade-off between temporal coverage and file size:
-
base— usesde440s.bsp(~31 MB), covers 1849–2150. Ideal for modern applications that do not require historical dates. -
medium(default) — usesde440.bsp(~114 MB), covers 1550–2650. The best trade-off for most uses. -
extended— usesde441.bsp(~3.1 GB), covers -13200 to +17191. For historical research, astronomical archaeology, and calculations spanning millennia.
Selecting a Tier #
import libephemeris as ephem
# View current tier
print(f"Active tier: {ephem.get_precision_tier()}")
# Change tier
ephem.set_precision_tier("base")
print(f"New tier: {ephem.get_precision_tier()}")
# Revert to default
ephem.set_precision_tier("medium")
print(f"Restored tier: {ephem.get_precision_tier()}")
Active tier: medium
New tier: base
Restored tier: medium
The tier can also be set via the LIBEPHEMERIS_PRECISION environment variable:
export LIBEPHEMERIS_PRECISION=extended
Information on Available Tiers #
import libephemeris as ephem
from libephemeris.state import list_tiers
for tier in list_tiers():
print(f"{tier.name:10s} {tier.ephemeris_file:14s} {tier.description}")
base de440s.bsp Modern usage (1850-2150), ~31 MB
medium de440.bsp General purpose (1550-2650), ~114 MB
extended de441.bsp Extended range (-13200 to +17191), ~3.1 GB
14.4 LEB Mode: Precomputed Binary Ephemerides #
The Problem #
Skyfield (the underlying calculation engine) is extremely precise, but every single call to calc_ut() involves several steps: reading the SPK file, interpolating Chebyshev polynomials, rotating reference frames, correcting for aberration, nutation, etc. For a single calculation, this is imperceptible, but for thousands (a monthly ephemeris, a transit search over an entire year), the cost accumulates.
The Solution: LEB #
LEB (LibEphemeris Binary) is a precomputed ephemeris format that stores approximations with Chebyshev polynomials for every celestial body and every time interval. The .leb file contains optimized coefficients that allow calculating positions with a single polynomial evaluation, skipping the entire Skyfield pipeline.
LEB precision is identical to Skyfield’s — the differences are under a milliarcsecond:
import libephemeris as ephem
jd = ephem.julday(2024, 4, 8, 12.0)
# Calculation with Skyfield
ephem.set_calc_mode("skyfield")
pos_sky, _ = ephem.calc_ut(jd, ephem.SE_MARS, ephem.SEFLG_SPEED)
# Calculation with LEB
ephem.set_calc_mode("leb")
pos_leb, _ = ephem.calc_ut(jd, ephem.SE_MARS, ephem.SEFLG_SPEED)
diff_arcsec = abs(pos_sky[0] - pos_leb[0]) * 3600
print(f"Mars (Skyfield): {pos_sky[0]:.9f}°")
print(f"Mars (LEB): {pos_leb[0]:.9f}°")
print(f"Difference: {diff_arcsec:.9f}\"")
ephem.set_calc_mode("auto")
Mars (Skyfield): 342.845795946°
Mars (LEB): 342.845795946°
Difference: 0.000000000"
Activating LEB #
There are three ways to activate LEB mode:
1. Download the precomputed LEB file (recommended):
import libephemeris as ephem
# Download the LEB file for the medium tier (~175 MB)
ephem.download_leb_for_tier("medium")
# The file is saved in ~/.libephemeris/leb/ephemeris_medium.leb
# and automatically activated for this session
2. Set the path manually:
import libephemeris as ephem
ephem.set_leb_file("/path/to/file/ephemeris_medium.leb")
3. Via environment variable:
export LIBEPHEMERIS_LEB=/path/to/file/ephemeris_medium.leb
Auto-discovery #
If you have downloaded a LEB file with download_leb_for_tier(), the library automatically finds it in the standard location (~/.libephemeris/leb/ephemeris_{tier}.leb) without needing any configuration.
Automatic Fallback #
Not all bodies and flag combinations are supported by LEB. In particular, LEB does not support:
SEFLG_TOPOCTR(topocentric positions)SEFLG_XYZ(Cartesian coordinates)SEFLG_RADIANS(angles in radians)SEFLG_NONUT(without nutation)
When you encounter one of these cases, the library automatically falls back to Skyfield without errors — the transition is transparent.
Three LEB Tiers #
Like the JPL ephemerides, LEB files also exist in three versions:
base(~53 MB) — covers 1850–2150medium(~175 MB) — covers 1550–2650extended(~1.6 GB) — covers -5000 to +5000
14.5 Calculation Modes and Configuration #
The library supports four calculation modes, which can be controlled with set_calc_mode():
"auto" (default) #
Tries LEB first if a .leb file is configured (or auto-discovered), then falls back to the Horizons API if no local DE440 file is available, then to Skyfield. This is the recommended mode for normal use:
import libephemeris as ephem
ephem.set_calc_mode("auto")
print(f"Mode: {ephem.get_calc_mode()}")
Mode: auto
"skyfield" #
Always forces the Skyfield path, even if a LEB file is available. Useful for debugging, precision comparisons, or when you want to be certain you are using the full pipeline:
import libephemeris as ephem
ephem.set_calc_mode("skyfield")
print(f"Mode: {ephem.get_calc_mode()}")
jd = ephem.julday(2024, 4, 8, 12.0)
pos, _ = ephem.calc_ut(jd, ephem.SE_SUN, ephem.SEFLG_SPEED)
print(f"Sun (Skyfield forced): {pos[0]:.6f}°")
ephem.set_calc_mode("auto") # restore
Mode: skyfield
Sun (Skyfield forced): 19.140437°
"leb" #
Requires a valid LEB file. The library tries the configured file, then auto-discovery, then auto-download of LEB2 for the active tier. Raises RuntimeError only if no LEB can be resolved. Bodies not present in the LEB file still fall back to Skyfield:
import libephemeris as ephem
ephem.set_calc_mode("leb")
print(f"Mode: {ephem.get_calc_mode()}")
ephem.set_calc_mode("auto") # restore
Mode: leb
"horizons" #
Always uses the NASA JPL Horizons REST API for calculations. This mode requires an internet connection and does not need any local ephemeris files. It supports planets, asteroids, Mean Node, Mean Apogee, and Uranians. Bodies or flags not supported by Horizons (e.g., SEFLG_TOPOCTR, fixed stars) fall back to Skyfield:
import libephemeris as ephem
ephem.set_calc_mode("horizons")
print(f"Mode: {ephem.get_calc_mode()}")
jd = ephem.julday(2024, 4, 8, 12.0)
pos, _ = ephem.calc_ut(jd, ephem.SE_SUN, ephem.SEFLG_SPEED)
print(f"Sun (Horizons): {pos[0]:.6f}°")
ephem.set_calc_mode("auto") # restore
Mode: horizons
Sun (Horizons): 19.140437°
Environment Variable #
The mode can also be set via LIBEPHEMERIS_MODE:
export LIBEPHEMERIS_MODE=horizons
14.6 EphemerisContext: Thread-Safe Calculations #
For multi-threaded applications (web servers, APIs, parallel calculations), the library offers EphemerisContext — a context that maintains its own isolated state (observer position, sidereal mode, angle cache) while sharing expensive resources (ephemeris files, timescale) in a thread-safe manner.
Basic Use #
import libephemeris as ephem
from libephemeris import EphemerisContext, SE_SUN, SEFLG_SPEED
ctx = EphemerisContext()
ctx.set_topo(12.5, 41.9, 0) # Rome
jd = ephem.julday(2024, 4, 8, 12.0)
pos, flag = ctx.calc_ut(jd, SE_SUN, SEFLG_SPEED)
print(f"Sun (from Rome, via context): {pos[0]:.4f}°")
Sun (from Rome, via context): 19.1404°
Sidereal Calculation in Context #
Each context can have its own sidereal mode:
import libephemeris as ephem
from libephemeris import EphemerisContext, SE_SUN, SEFLG_SPEED, SEFLG_SIDEREAL
from libephemeris.constants import SE_SIDM_LAHIRI
ctx = EphemerisContext()
ctx.set_sid_mode(SE_SIDM_LAHIRI)
jd = ephem.julday(2024, 4, 8, 12.0)
pos, _ = ctx.calc_ut(jd, SE_SUN, SEFLG_SPEED | SEFLG_SIDEREAL)
print(f"Sidereal Sun (Lahiri): {pos[0]:.4f}°")
Sidereal Sun (Lahiri): 354.9458°
Houses in Context #
import libephemeris as ephem
from libephemeris import EphemerisContext
ctx = EphemerisContext()
jd = ephem.julday(2024, 4, 8, 12.0)
cusps, ascmc = ctx.houses(jd, 41.9, 12.5, ord('P'))
print(f"Ascendant: {ascmc[0]:.4f}°")
print(f"Midheaven: {ascmc[1]:.4f}°")
Ascendant: 133.0806°
Midheaven: 31.9078°
Parallel Calculations with Threads #
import libephemeris as ephem
from libephemeris import EphemerisContext, SE_SUN, SEFLG_SPEED
import threading
results = {}
def calc_for_city(name, lon, lat):
ctx = EphemerisContext()
ctx.set_topo(lon, lat, 0)
jd = ephem.julday(2024, 4, 8, 12.0)
pos, _ = ctx.calc_ut(jd, SE_SUN, SEFLG_SPEED)
results[name] = pos[0]
cities = [
("Rome", 12.5, 41.9),
("New York", -74.0, 40.7),
("Tokyo", 139.7, 35.7),
]
threads = []
for name, lon, lat in cities:
t = threading.Thread(target=calc_for_city, args=(name, lon, lat))
threads.append(t)
t.start()
for t in threads:
t.join()
for name, lon in results.items():
print(f"{name}: Sun at {lon:.4f}°")
Rome: Sun at 19.1404°
New York: Sun at 19.1404°
Tokyo: Sun at 19.1404°
Note: Geocentric positions are the same for all cities because the
SEFLG_TOPOCTRflag was not used. The difference can be seen in the houses and topocentric positions.
LEB in Context #
Each context can have its own LEB file:
from libephemeris import EphemerisContext
ctx = EphemerisContext()
ctx.set_leb_file("/path/to/file/ephemeris_medium.leb")
# Calculations on this context will use LEB
Closing Shared Resources #
from libephemeris import EphemerisContext
# Closes files and resources shared by all contexts
EphemerisContext.close()
14.7 Download and Data Management #
LibEphemeris automatically downloads the necessary files on the first run, but it also offers functions to manage downloads explicitly.
Downloading Data for a Tier #
import libephemeris as ephem
# Download everything needed for the "medium" tier:
# - de440.bsp (JPL ephemerides)
# - planet_centers_medium.bsp (planet centers)
# - SPK for all minor bodies
ephem.download_for_tier("medium")
Downloading the LEB File #
import libephemeris as ephem
# Download the precomputed LEB ephemerides for the "medium" tier
# (~175 MB, automatically activated after download)
ephem.download_leb_for_tier("medium")
Data Directory #
By default, all files are saved in ~/.libephemeris/. You can change this directory with the environment variable:
export LIBEPHEMERIS_DATA_DIR=/data/ephemerides
Or verify the current directory:
import libephemeris as ephem
print(f"Data directory: {ephem.get_library_path()}")
Data directory: /Users/giacomo/.libephemeris
Complete Reset #
The close() function closes all files and resets the global state. Useful for freeing resources or restarting with a clean configuration:
import libephemeris as ephem
# Do some calculations...
jd = ephem.julday(2024, 4, 8, 12.0)
pos, _ = ephem.calc_ut(jd, ephem.SE_SUN, 0)
print(f"Before close: {pos[0]:.6f}°")
# Close everything
ephem.close()
# The next calculation automatically reloads the ephemerides
pos2, _ = ephem.calc_ut(jd, ephem.SE_SUN, 0)
print(f"After close: {pos2[0]:.6f}°")
print(f"Identical result: {abs(pos[0] - pos2[0]) < 0.000001}")
Before close: 19.140437°
After close: 19.140437°
Identical result: True
Environment Variables Summary #
LIBEPHEMERIS_DATA_DIR— base directory for all data (default:~/.libephemeris)LIBEPHEMERIS_PRECISION— precision tier:base,medium,extendedLIBEPHEMERIS_EPHEMERIS— ephemeris file name (e.g.,de441.bsp)LIBEPHEMERIS_LEB— path to the.lebfile for binary modeLIBEPHEMERIS_MODE— calculation mode:auto,skyfield,leb,horizonsLIBEPHEMERIS_AUTO_SPK—1/0to enable/disable automatic SPK downloadLIBEPHEMERIS_SPK_DIR— cache directory for minor bodies’ SPK files
Summary #
- Sub-arcsecond precision for all bodies in the 1900–2100 range — no compromises for modern use
- Three precision tiers:
base(1849–2150, 31 MB),medium(1550–2650, 114 MB),extended(-13200 to +17191, 3.1 GB) - LEB (precomputed binary ephemerides): speeds up calculations maintaining precision identical to Skyfield; activated with
set_leb_file()ordownload_leb_for_tier() - Four calculation modes:
auto(default — LEB, then Horizons, then Skyfield),skyfield(forces Skyfield),leb(requires LEB),horizons(NASA JPL Horizons API, requires internet) - EphemerisContext: thread-safe context for parallel calculations, with isolated state for observer, sidereal mode, and cache
- Automatic download of data on first use; explicit management with
download_for_tier()anddownload_leb_for_tier() close()for complete reset of resources and state
Functions Introduced #
set_precision_tier(tier)/get_precision_tier()— selects the precision tier ("base","medium","extended")set_calc_mode(mode)/get_calc_mode()— sets the calculation mode ("auto","skyfield","leb","horizons")set_leb_file(filepath)— activates the precomputed binary ephemeridesdownload_for_tier(tier)— downloads all data for a tierdownload_leb_for_tier(tier)— downloads the precomputed LEB fileEphemerisContext()— isolated context for thread-safe calculationsEphemerisContext.calc_ut(jd, body, flag)— position calculation in contextEphemerisContext.houses(jd, lat, lon, hsys)— houses in contextEphemerisContext.set_topo(lon, lat, alt)— observer in contextEphemerisContext.set_sid_mode(mode)— sidereal mode in contextEphemerisContext.set_leb_file(filepath)— LEB for contextEphemerisContext.close()— closes shared resourcesclose()— closes all resources and resets the global stateget_library_path()— path of the data directory