Box Least Squares (BLS) periodogram for detecting transiting exoplanets and eclipsing binaries. Use when searching for periodic box-shaped dips in light curves. Alternative to Transit Least Squares, available in astropy.timeseries. Based on Kovács et al. (2002).
The Box Least Squares (BLS) periodogram is a statistical tool for detecting transiting exoplanets and eclipsing binaries in photometric time series data. BLS models a transit as a periodic upside-down top hat (box shape) and finds the period, duration, depth, and reference time that best fit the data.
BLS is built into Astropy and provides an alternative to Transit Least Squares (TLS). Both search for transits, but with different implementations and performance characteristics.
Key parameters BLS searches for:
BLS is part of Astropy:
pip install astropy
import numpy as np
import astropy.units as u
from astropy.timeseries import BoxLeastSquares
# Prepare data
# time, flux, and flux_err should be numpy arrays or Quantities
t = time * u.day # Add units if not already present
y = flux
dy = flux_err # Optional but recommended
# Create BLS object
model = BoxLeastSquares(t, y, dy=dy)
# Automatic period search with specified duration
duration = 0.2 * u.day # Expected transit duration
periodogram = model.autopower(duration)
# Extract results
best_period = periodogram.period[np.argmax(periodogram.power)]
print(f"Best period: {best_period:.5f}")
Recommended for initial searches. Automatically determines appropriate period grid:
# Specify duration (or multiple durations)
duration = 0.2 * u.day
periodogram = model.autopower(duration)
# Or search multiple durations
durations = [0.1, 0.15, 0.2, 0.25] * u.day
periodogram = model.autopower(durations)
For more control over the search:
# Define custom period grid
periods = np.linspace(2.0, 10.0, 1000) * u.day
duration = 0.2 * u.day
periodogram = model.power(periods, duration)
Warning: Period grid quality matters! Too coarse and you'll miss the true period.
BLS supports two objective functions:
Maximizes the statistical likelihood of the model fit:
periodogram = model.autopower(0.2 * u.day, objective='likelihood')
Uses the SNR with which the transit depth is measured:
periodogram = model.autopower(0.2 * u.day, objective='snr')
The SNR objective can improve reliability in the presence of correlated noise.
import numpy as np
import matplotlib.pyplot as plt
import astropy.units as u
from astropy.timeseries import BoxLeastSquares
# Load and prepare data
data = np.loadtxt('light_curve.txt')
time = data[:, 0] * u.day
flux = data[:, 1]
flux_err = data[:, 3]
# Create BLS model
model = BoxLeastSquares(time, flux, dy=flux_err)
# Run BLS with automatic period grid
# Try multiple durations to find best fit
durations = np.linspace(0.05, 0.3, 10) * u.day
periodogram = model.autopower(durations, objective='likelihood')
# Find peak
max_power_idx = np.argmax(periodogram.power)
best_period = periodogram.period[max_power_idx]
best_duration = periodogram.duration[max_power_idx]
best_t0 = periodogram.transit_time[max_power_idx]
max_power = periodogram.power[max_power_idx]
print(f"Period: {best_period:.5f}")
print(f"Duration: {best_duration:.5f}")
print(f"T0: {best_t0:.5f}")
print(f"Power: {max_power:.2f}")
# Plot periodogram
import matplotlib.pyplot as plt
plt.plot(periodogram.period, periodogram.power)
plt.xlabel('Period [days]')
plt.ylabel('BLS Power')
plt.show()
Use compute_stats() to calculate detailed statistics about a candidate transit:
# Get statistics for the best period
stats = model.compute_stats(
periodogram.period[max_power_idx],
periodogram.duration[max_power_idx],
periodogram.transit_time[max_power_idx]
)
# Key statistics for validation
print(f"Depth: {stats['depth']:.6f}")
print(f"Depth uncertainty: {stats['depth_err']:.6f}")
print(f"SNR: {stats['depth_snr']:.2f}")
print(f"Odd/Even mismatch: {stats['depth_odd'] - stats['depth_even']:.6f}")
print(f"Number of transits: {stats['transit_count']}")
# Check for false positives
if abs(stats['depth_odd'] - stats['depth_even']) > 3 * stats['depth_err']:
print("Warning: Significant odd-even mismatch - may not be planetary")
Validation criteria:
The BLS periodogram is sensitive to period grid spacing. The autoperiod() method provides a conservative grid:
# Get automatic period grid
periods = model.autoperiod(durations, minimum_period=1*u.day, maximum_period=10*u.day)
print(f"Period grid has {len(periods)} points")
# Use this grid with power()
periodogram = model.power(periods, durations)
Tips:
autopower() for initial searchesTo compare multiple peaks:
# Find top 5 peaks
sorted_idx = np.argsort(periodogram.power)[::-1]
top_5 = sorted_idx[:5]
print("Top 5 candidates:")
for i, idx in enumerate(top_5):
period = periodogram.period[idx]
power = periodogram.power[idx]
duration = periodogram.duration[idx]
stats = model.compute_stats(period, duration, periodogram.transit_time[idx])
print(f"\n{i+1}. Period: {period:.5f}")
print(f" Power: {power:.2f}")
print(f" Duration: {duration:.5f}")
print(f" SNR: {stats['depth_snr']:.2f}")
print(f" Transits: {stats['transit_count']}")
After finding a candidate, phase-fold to visualize the transit:
# Fold the light curve at the best period
phase = ((time.value - best_t0.value) % best_period.value) / best_period.value
# Plot to verify transit shape
import matplotlib.pyplot as plt
plt.plot(phase, flux, '.')
plt.xlabel('Phase')
plt.ylabel('Flux')
plt.show()
Both methods search for transits, but differ in implementation:
Pros:
Cons:
Pros:
Cons:
Recommendation: Try both! TLS is often more sensitive, but BLS is faster and built-in.
BLS works best with preprocessed data. Consider this pipeline:
compute_stats() to check candidate qualityflatten())Causes:
Solutions:
flatten() windowCauses:
Solutions:
Cause:
Solution:
stats['depth_odd'] vs stats['depth_even']pip install astropy numpy matplotlib
# Optional: lightkurve for preprocessing
pip install lightkurve
Use BLS when:
compute_stats)Use TLS when:
Use Lomb-Scargle when:
For exoplanet detection, both BLS and TLS are valid choices. Try both and compare results!