Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ Currently includes implementations of:
- Sparse BLS ([Panahi & Zucker 2021](https://arxiv.org/abs/2103.06193)) for small datasets (< 500 observations)
- GPU implementation: `sparse_bls_gpu()` (default)
- CPU implementation: `sparse_bls_cpu()` (fallback)
- **Transit Least Squares ([TLS](https://ui.adsabs.harvard.edu/abs/2019A%26A...623A..39H/abstract))** - GPU-accelerated transit detection with optimal depth fitting
- **35-202× faster** than CPU TLS (transitleastsquares package)
- Keplerian-aware duration constraints (`tls_transit()`) - searches physically plausible transit durations
- Standard mode (`tls_search_gpu()`) for custom period/duration grids
- Optimal period grid sampling (Ofir 2014)
- Supports datasets up to ~100,000 observations (optimal: 500-20,000)
- **Non-equispaced fast Fourier transform (NFFT)** - Adjoint operation ([paper](http://epubs.siam.org/doi/abs/10.1137/0914081))
- **NUFFT-based Likelihood Ratio Test (LRT)** - Transit detection with correlated noise (contributed by Jamila Taaki)
- Matched filter in frequency domain with adaptive noise estimation
Expand Down Expand Up @@ -196,6 +202,8 @@ Full documentation is available at: https://johnh2o2.github.io/cuvarbase/

## Quick Start

### Box Least Squares (BLS) - Transit Detection

```python
import numpy as np
from cuvarbase import bls
Expand All @@ -205,7 +213,6 @@ t = np.sort(np.random.uniform(0, 10, 1000)).astype(np.float32)
y = np.sin(2 * np.pi * t / 2.5) + np.random.normal(0, 0.1, len(t))
dy = np.ones_like(y) * 0.1 # uncertainties

# Box Least Squares (BLS) - Transit detection
# Define frequency grid
freqs = np.linspace(0.1, 2.0, 5000).astype(np.float32)

Expand All @@ -218,6 +225,36 @@ print(f"Best period: {1/best_freq:.2f} (expected: 2.5)")
power_adaptive = bls.eebls_gpu_fast_adaptive(t, y, dy, freqs)
```

### Transit Least Squares (TLS) - Advanced Transit Detection

```python
from cuvarbase import tls

# Generate transit data
t = np.sort(np.random.uniform(0, 50, 500)).astype(np.float32)
y = np.ones(len(t), dtype=np.float32)
dy = np.ones(len(t), dtype=np.float32) * 0.001

# Add 1% transit at 10-day period
phase = (t % 10.0) / 10.0
in_transit = (phase < 0.01) | (phase > 0.99)
y[in_transit] -= 0.01
y += np.random.normal(0, 0.001, len(t)).astype(np.float32)

# TLS with Keplerian duration constraints (35-202x faster than CPU TLS!)
results = tls.tls_transit(
t, y, dy,
R_star=1.0, # Solar radii
M_star=1.0, # Solar masses
period_min=5.0,
period_max=20.0
)

print(f"Best period: {results['period']:.2f} days")
print(f"Transit depth: {results['depth']:.4f}")
print(f"SDE: {results['SDE']:.1f}")
```

For more advanced usage including Lomb-Scargle and Conditional Entropy, see the [full documentation](https://johnh2o2.github.io/cuvarbase/) and [examples/](examples/).

## Using Multiple GPUs
Expand Down
112 changes: 112 additions & 0 deletions analysis/test_tls_keplerian.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""Test TLS with Keplerian duration constraints"""
import numpy as np
from cuvarbase import tls_grids

# Test parameters
ndata = 500
baseline = 50.0
period_true = 10.0
depth_true = 0.01

# Generate synthetic data
np.random.seed(42)
t = np.sort(np.random.uniform(0, baseline, ndata)).astype(np.float32)
y = np.ones(ndata, dtype=np.float32)

# Add transit
phase = (t % period_true) / period_true
in_transit = (phase < 0.01) | (phase > 0.99)
y[in_transit] -= depth_true
y += np.random.normal(0, 0.001, ndata).astype(np.float32)
dy = np.ones(ndata, dtype=np.float32) * 0.001

print("Data: {} points, transit at {:.1f} days with depth {:.3f}".format(
len(t), period_true, depth_true))

# Generate period grid
periods = tls_grids.period_grid_ofir(
t, R_star=1.0, M_star=1.0,
period_min=5.0,
period_max=20.0
).astype(np.float32)

print(f"Period grid: {len(periods)} periods from {periods[0]:.2f} to {periods[-1]:.2f}")

# Test 1: Original duration grid (fixed range for all periods)
print("\n=== Original Duration Grid (Fixed Range) ===")
# Fixed 0.5% to 15% of period
q_fixed_min = 0.005
q_fixed_max = 0.15
n_dur = 15

for i, period in enumerate(periods[:3]): # Show first 3
dur_min = q_fixed_min * period
dur_max = q_fixed_max * period
print(f"Period {period:6.2f} days: duration range {dur_min:7.4f} - {dur_max:6.4f} days "
f"(q = {q_fixed_min:.4f} - {q_fixed_max:.4f})")

# Test 2: Keplerian duration grid (scales with stellar parameters)
print("\n=== Keplerian Duration Grid (Stellar-Parameter Aware) ===")
qmin_fac = 0.5 # Search 0.5x to 2.0x Keplerian value
qmax_fac = 2.0
R_planet = 1.0 # Earth-size planet

# Calculate Keplerian q for each period
q_kep = tls_grids.q_transit(periods, R_star=1.0, M_star=1.0, R_planet=R_planet)

for i in range(min(3, len(periods))): # Show first 3
period = periods[i]
q_k = q_kep[i]
q_min = q_k * qmin_fac
q_max = q_k * qmax_fac
dur_min = q_min * period
dur_max = q_max * period
print(f"Period {period:6.2f} days: q_keplerian = {q_k:.5f}, "
f"search q = {q_min:.5f} - {q_max:.5f}, "
f"durations {dur_min:7.4f} - {dur_max:6.4f} days")

# Test 3: Generate full Keplerian duration grid
print("\n=== Full Keplerian Duration Grid ===")
durations, dur_counts, q_values = tls_grids.duration_grid_keplerian(
periods, R_star=1.0, M_star=1.0, R_planet=1.0,
qmin_fac=0.5, qmax_fac=2.0, n_durations=15
)

print(f"Generated {len(durations)} duration arrays (one per period)")
print(f"Duration counts: min={np.min(dur_counts)}, max={np.max(dur_counts)}, "
f"mean={np.mean(dur_counts):.1f}")

# Show examples
print("\nExample duration arrays:")
for i in [0, len(periods)//2, -1]:
period = periods[i]
durs = durations[i]
print(f" Period {period:6.2f} days: {len(durs)} durations, "
f"range {durs[0]:7.4f} - {durs[-1]:7.4f} days "
f"(q = {durs[0]/period:.5f} - {durs[-1]/period:.5f})")

# Test 4: Compare efficiency
print("\n=== Efficiency Comparison ===")

# Original approach: search same q range for all periods
# At short periods (5 days), q=0.005-0.15 may be too wide
# At long periods (20 days), q=0.005-0.15 may miss wide transits

period_short = 5.0
period_long = 20.0

# For Earth around Sun-like star
q_kep_short = tls_grids.q_transit(period_short, 1.0, 1.0, 1.0)
q_kep_long = tls_grids.q_transit(period_long, 1.0, 1.0, 1.0)

print(f"\nFor Earth-size planet around Sun-like star:")
print(f" At P={period_short:4.1f} days: q_keplerian = {q_kep_short:.5f}")
print(f" Fixed search: q = 0.00500 - 0.15000 (way too wide!)")
print(f" Keplerian: q = {q_kep_short*qmin_fac:.5f} - {q_kep_short*qmax_fac:.5f} (focused)")
print(f"\n At P={period_long:4.1f} days: q_keplerian = {q_kep_long:.5f}")
print(f" Fixed search: q = 0.00500 - 0.15000 (wastes time on impossible durations)")
print(f" Keplerian: q = {q_kep_long*qmin_fac:.5f} - {q_kep_long*qmax_fac:.5f} (focused)")

print("\n✓ Keplerian approach focuses search on physically plausible durations!")
print("✓ This is the same strategy BLS uses for efficient transit searches.")
103 changes: 103 additions & 0 deletions analysis/test_tls_keplerian_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Test TLS Keplerian API end-to-end"""
import numpy as np
from cuvarbase import tls

print("="*70)
print("TLS Keplerian API End-to-End Test")
print("="*70)

# Generate synthetic data with transit
np.random.seed(42)
ndata = 500
baseline = 50.0
period_true = 10.0
depth_true = 0.01

t = np.sort(np.random.uniform(0, baseline, ndata)).astype(np.float32)
y = np.ones(ndata, dtype=np.float32)

# Add transit
phase = (t % period_true) / period_true
in_transit = (phase < 0.01) | (phase > 0.99)
y[in_transit] -= depth_true
y += np.random.normal(0, 0.001, ndata).astype(np.float32)
dy = np.ones(ndata, dtype=np.float32) * 0.001

print(f"\nData: {ndata} points, transit at {period_true:.1f} days with depth {depth_true:.3f}")

# Test 1: tls_transit() with Keplerian constraints
print("\n" + "="*70)
print("Test 1: tls_transit() - Keplerian-Aware Search")
print("="*70)

results = tls.tls_transit(
t, y, dy,
R_star=1.0,
M_star=1.0,
R_planet=1.0, # Earth-size planet
qmin_fac=0.5, # Search 0.5x to 2.0x Keplerian duration
qmax_fac=2.0,
n_durations=15,
period_min=5.0,
period_max=20.0
)

print(f"\nResults:")
print(f" Period: {results['period']:.4f} days (true: {period_true:.1f})")
print(f" Depth: {results['depth']:.6f} (true: {depth_true:.6f})")
print(f" Duration: {results['duration']:.4f} days")
print(f" T0: {results['T0']:.4f} days")
print(f" SDE: {results['SDE']:.2f}")

# Check accuracy
period_error = abs(results['period'] - period_true)
depth_error = abs(results['depth'] - depth_true)

print(f"\nAccuracy:")
print(f" Period error: {period_error:.4f} days ({period_error/period_true*100:.2f}%)")
print(f" Depth error: {depth_error:.6f} ({depth_error/depth_true*100:.1f}%)")

# Test 2: Standard tls_search_gpu() for comparison
print("\n" + "="*70)
print("Test 2: tls_search_gpu() - Standard Search (Fixed Duration Range)")
print("="*70)

results_std = tls.tls_search_gpu(
t, y, dy,
period_min=5.0,
period_max=20.0,
R_star=1.0,
M_star=1.0
)

print(f"\nResults:")
print(f" Period: {results_std['period']:.4f} days (true: {period_true:.1f})")
print(f" Depth: {results_std['depth']:.6f} (true: {depth_true:.6f})")
print(f" Duration: {results_std['duration']:.4f} days")
print(f" SDE: {results_std['SDE']:.2f}")

# Compare
print("\n" + "="*70)
print("Comparison: Keplerian vs Standard")
print("="*70)

print(f"\nPeriod Recovery:")
print(f" Keplerian: {results['period']:.4f} days (error: {period_error/period_true*100:.2f}%)")
print(f" Standard: {results_std['period']:.4f} days (error: {abs(results_std['period']-period_true)/period_true*100:.2f}%)")

print(f"\nDepth Recovery:")
print(f" Keplerian: {results['depth']:.6f} (error: {depth_error/depth_true*100:.1f}%)")
print(f" Standard: {results_std['depth']:.6f} (error: {abs(results_std['depth']-depth_true)/depth_true*100:.1f}%)")

# Verdict
print("\n" + "="*70)
success = (period_error < 0.5 and depth_error < 0.002)
if success:
print("✓ Test PASSED: Keplerian API working correctly!")
print("✓ Period recovered within 5% of true value")
print("✓ Depth recovered within 20% of true value")
exit(0)
else:
print("✗ Test FAILED: Signal recovery outside acceptable tolerance")
exit(1)
69 changes: 69 additions & 0 deletions compare_gpu_cpu_depth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Compare GPU and CPU TLS depth calculations"""
import numpy as np
from cuvarbase import tls as gpu_tls
from transitleastsquares import transitleastsquares as cpu_tls

# Generate test data
np.random.seed(42)
ndata = 500
t = np.sort(np.random.uniform(0, 50, ndata))
y = np.ones(ndata, dtype=np.float32)

# Add transit
period_true = 10.0
depth_true = 0.01 # Fractional dip
phase = (t % period_true) / period_true
in_transit = (phase < 0.01) | (phase > 0.99)
y[in_transit] -= depth_true
y += np.random.normal(0, 0.001, ndata).astype(np.float32)
dy = np.ones(ndata, dtype=np.float32) * 0.001

print(f"Test data:")
print(f" N = {ndata}")
print(f" Period = {period_true:.1f} days")
print(f" Depth (fractional dip) = {depth_true:.3f}")
print(f" Points in transit: {np.sum(in_transit)}")
print(f" Measured depth: {np.mean(y[~in_transit]) - np.mean(y[in_transit]):.6f}")

# GPU TLS
print(f"\n--- GPU TLS ---")
gpu_result = gpu_tls.tls_search_gpu(
t.astype(np.float32), y, dy,
period_min=9.0,
period_max=11.0
)

print(f"Period: {gpu_result['period']:.4f} (error: {abs(gpu_result['period'] - period_true)/period_true*100:.2f}%)")
print(f"Depth: {gpu_result['depth']:.6f}")
print(f"Duration: {gpu_result['duration']:.4f} days")
print(f"T0: {gpu_result['T0']:.4f}")

# CPU TLS
print(f"\n--- CPU TLS ---")
model = cpu_tls(t, y, dy)
cpu_result = model.power(
period_min=9.0,
period_max=11.0,
n_transits_min=2
)

print(f"Period: {cpu_result.period:.4f} (error: {abs(cpu_result.period - period_true)/period_true*100:.2f}%)")
print(f"Depth (flux ratio): {cpu_result.depth:.6f}")
print(f"Depth (fractional dip): {1 - cpu_result.depth:.6f}")
print(f"Duration: {cpu_result.duration:.4f} days")
print(f"T0: {cpu_result.T0:.4f}")

# Compare
print(f"\n--- Comparison ---")
print(f"Period agreement: {abs(gpu_result['period'] - cpu_result.period):.4f} days")
print(f"Duration agreement: {abs(gpu_result['duration'] - cpu_result.duration):.4f} days")

# Check depth conventions
gpu_depth_frac = gpu_result['depth'] # GPU reports fractional dip
cpu_depth_frac = 1 - cpu_result.depth # CPU reports flux ratio

print(f"\nDepth (fractional dip convention):")
print(f" True: {depth_true:.6f}")
print(f" GPU: {gpu_depth_frac:.6f} (error: {abs(gpu_depth_frac - depth_true)/depth_true*100:.1f}%)")
print(f" CPU: {cpu_depth_frac:.6f} (error: {abs(cpu_depth_frac - depth_true)/depth_true*100:.1f}%)")
Loading