From c89516a425d113672d3d7564dc6cd9318bd9036c Mon Sep 17 00:00:00 2001 From: John Hoffman Date: Mon, 6 Jul 2026 13:58:47 -0500 Subject: [PATCH 1/6] TLS: survey-scale fast path (batch-native phase-binned kernel + exact refinement) Rewrite Transit Least Squares for survey-scale throughput. The legacy kernel did two full O(ndata) passes per (duration, t0) trial and capped light curves at ~3,500 points; the new default (`use_fast=True`) removes both limits and processes a whole survey chunk in one launch. Architecture (cuvarbase/kernels/tls_fast.cu + tls_search_batch): - One block per (light curve, period); fold once into shared-memory phase bins, then scan every trial against bin-averaged integrated template tables (S1=int T, S2=int T^2) with a closed-form chi2 (chi2_0 - num^2/den). Trial cost is independent of ndata; no shared- memory cap on light-curve length. - Period grid split into bin-count bands so long-period searches don't pay the finest band's per-trial cost. - Exact float-float ("double-single") fold: ~1e-8 phase error at 4-year baselines with no 1/64-rate double math on consumer GPUs. - Cancellation-free score output (num^2/den); chi2 reconstructed in float64 host-side against a float64 chi2_0. - Second exact kernel re-fits the top-K candidate periods per light curve on a finer local (duration, t0) grid. Refinement sharpens the reported parameters only; the SDE/FAP statistics are computed from the uniform coarse spectrum so the detection statistic's scale stays consistent with the legacy kernel. Support fixes: SDE median-detrend window capped at 91 (reference TLS convention) instead of a pathological nperiods/10 window (minutes -> ~0.1 s at 190k periods); duration_grid_keplerian vectorized (1.1 s -> 40 ms at 190k periods); per-light-curve statistics run on a thread pool; 64-bit batch offsets; qmax<1, power-of-two block_size, and non-negative refine_top_k validated with clear errors. Measured end-to-end (scripts/benchmark_tls_survey.py, 100% injected- transit recovery in every regime; RTX A5000): TESS FFI 1.2 ms/LC (~800 LC/s), K2 3.1 ms, TESS 2-min 2.8 ms, TESS 1-yr 18 ms, Kepler 4-yr (65k pts, 172k periods) 0.17 s/LC vs ~522 s for reference TLS on a 16-core CPU. Full suite 68/68 on RTX 4000 Ada (incl. golden tests vs the reference transitleastsquares package); core suite green on A5000 and V100. Block-size heuristic swept and tuned per compute capability (sm_70/86/89). See analysis/TLS_COST_ANALYSIS.md for the GPU-vs-CPU and GPU-vs-GTLS cost comparison. The legacy per-point kernel is retained behind use_fast=False; the module stays EXPERIMENTAL pending an injection-recovery validation campaign at reference-matched epoch fidelity (item D3). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01WEGJJrPcrvkJGeMAEryRPG --- CHANGELOG.rst | 1 + analysis/TLS_COST_ANALYSIS.md | 111 + .../tls_survey_jul2026/tls_survey_a5000.json | 2464 +++++++++++++++++ .../tls_survey_a5000_final.json | 2269 +++++++++++++++ .../tls_survey_rtx4000ada.json | 2269 +++++++++++++++ .../tls_survey_jul2026/tls_survey_v100.json | 2269 +++++++++++++++ .../tls_survey_jul2026/tls_survey_v100b.json | 367 +++ cuvarbase/kernels/tls_fast.cu | 576 ++++ cuvarbase/tests/test_tls_basic.py | 29 +- cuvarbase/tests/test_tls_fast.py | 187 ++ cuvarbase/tls.py | 768 ++++- cuvarbase/tls_grids.py | 22 +- cuvarbase/tls_models.py | 56 + cuvarbase/tls_stats.py | 79 +- scripts/benchmark_tls_survey.py | 480 ++++ scripts/setup-remote.sh | 4 +- scripts/tls_fast_smoke.py | 177 ++ scripts/tls_kernel_sweep.py | 97 + scripts/tls_profile_stages.py | 204 ++ 19 files changed, 12379 insertions(+), 50 deletions(-) create mode 100644 analysis/TLS_COST_ANALYSIS.md create mode 100644 benchmarks/results/tls_survey_jul2026/tls_survey_a5000.json create mode 100644 benchmarks/results/tls_survey_jul2026/tls_survey_a5000_final.json create mode 100644 benchmarks/results/tls_survey_jul2026/tls_survey_rtx4000ada.json create mode 100644 benchmarks/results/tls_survey_jul2026/tls_survey_v100.json create mode 100644 benchmarks/results/tls_survey_jul2026/tls_survey_v100b.json create mode 100644 cuvarbase/kernels/tls_fast.cu create mode 100644 cuvarbase/tests/test_tls_fast.py create mode 100644 scripts/benchmark_tls_survey.py create mode 100644 scripts/tls_fast_smoke.py create mode 100644 scripts/tls_kernel_sweep.py create mode 100644 scripts/tls_profile_stages.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a7731b9..db2f09d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -48,6 +48,7 @@ What's new in cuvarbase * CE is now in **maintenance mode**: it keeps working, but no new development is planned — for an actively developed GPU CE/AOV search see `periodfind `_ * **Experimental** (UserWarning on import; not recommended for science use yet) * GPU Transit Least Squares (``cuvarbase.tls``) with Ofir (2014) period grids + * **TLS rewritten for survey-scale throughput (Jul 2026):** a new batch-native fast path (``tls_fast.cu`` + ``tls_search_batch()``) is now the default for ``tls_search``/``tls_search_gpu``/``tls_transit`` (opt out with ``use_fast=False``). Each (lightcurve, period) block folds once into shared-memory phase bins and scans every (duration, t0) trial against bin-averaged integrated-template tables with a closed-form chi2 (``chi2 = chi2_0 - num^2/den``), so trial cost is independent of ndata — the legacy kernel's two full O(ndata) passes per trial and its ~3,500-point shared-memory cap are both gone (Kepler-length and 2-min-cadence TESS lightcurves run natively). The period grid is split into bin-count bands so long-period searches don't pay the finest band's cost; folding uses an exact float-float decomposition (~1e-8 phase error at 4-year baselines, no 1/64-rate double math); the kernel outputs the cancellation-free delta-chi2 and the host reconstructs chi2 in float64. A second exact kernel re-fits the top-K candidate periods per lightcurve on a finer local (duration, t0) grid (``refine_top_k``, default 50; ``refine_oversample`` default 33, near the reference package's t0 stepping) — refinement sharpens the reported parameters while the SDE/FAP statistics come from the uniform coarse spectrum, keeping the detection statistic's scale consistent with the legacy kernel (chi2 correlation 0.998 measured). SDE detrending now uses the reference ``transitleastsquares`` 91-point median window instead of a pathological ``nperiods/10`` window (minutes -> ~0.1 s at 190k periods), ``duration_grid_keplerian`` is vectorized (1.1 s -> 40 ms at 190k periods), and per-lightcurve statistics run on a thread pool. Measured end-to-end on an RTX A5000 (``scripts/benchmark_tls_survey.py``, 100% injected-transit recovery in every regime): TESS-FFI sector 1.2 ms/lightcurve (~800 LC/s; reference CPU package: 14.7 s), K2 90-d 3.1 ms, TESS 2-min 2.8 ms, 1-yr/30-min 18 ms, Kepler 4-yr/65k-pt/188k-period 0.17 s vs ~522 s for reference TLS on a 16-core CPU (~3,000x) and 33 s/LC reported by the concurrent GTLS CuPy implementation (arXiv:2607.00348) on a faster RTX 4090. Batch API validation: empty/mismatched inputs, ``qmax < 1``, power-of-two ``block_size``, and non-negative ``refine_top_k`` are enforced with clear errors; offsets are 64-bit so >2^31-point batches chunk correctly * TLS epoch (t0) grid is now duration-scaled (stride = duration / oversample, floor 30, cap 20,000 epochs): the previous fixed 30-epoch grid missed transits narrower than ~1/30 of the period entirely, which broke Keplerian-mode searches for most periods > ~3.5 d. The oversample factor is caller-tunable via ``t0_oversample`` on ``tls_search``/``tls_search_gpu``/``compile_tls`` (default 3.0, favoring speed; the reference ``transitleastsquares`` steps ~33x finer — raise it for sensitivity-critical searches). Mirrored in ``tls_grids.t0_grid_size()`` * Removed the TLS kernels' bitonic phase sort: it was incomplete for non-power-of-2 sizes and its output order was never consumed — pure wasted per-period work; results are unchanged * Added golden accuracy tests against the reference ``transitleastsquares`` package (``test_tls_golden.py``) diff --git a/analysis/TLS_COST_ANALYSIS.md b/analysis/TLS_COST_ANALYSIS.md new file mode 100644 index 0000000..3ce6bd1 --- /dev/null +++ b/analysis/TLS_COST_ANALYSIS.md @@ -0,0 +1,111 @@ +# TLS cost analysis: GPU vs CPU, and vs the only other GPU TLS + +**Question.** With the survey-scale fast TLS path (`tls_search_batch`), is it +cheaper to run a Transit Least Squares search on a rented GPU than on a CPU? +And is cuvarbase now not just the fastest but the *cheapest* TLS available? + +**Short answer.** Yes on both counts, by a wide margin. At its default +fidelity, cuvarbase GPU TLS costs **$0.06–$7.45 per million light curves** +depending on regime, versus **$11,000–$99,000 per million** for the reference +CPU `transitleastsquares` package — a **13,000× to 200,000×** cost reduction. +It is also ~600× cheaper than GTLS, the only other GPU TLS. Read the fidelity +caveat at the end before quoting the largest ratios. + +## Method + +Throughput is the measured end-to-end survey wall time (grid generation + +preprocessing + host↔device transfer + kernels + per-LC statistics), +`scripts/benchmark_tls_survey.py`, 100% injected-transit recovery in every +regime on every GPU. Raw JSON in `benchmarks/results/tls_survey_jul2026/`. + +Cost per million light curves is `(ms_per_lc × 1000 / 3600) × $/hr`. + +- **GPU $/hr** are the prices actually paid on RunPod this session: A5000 + **$0.16**, RTX 4000 Ada **$0.20**, Tesla V100 **$0.23**. +- **CPU $/hr** uses AWS on-demand as a defensible public anchor: 64 vCPU = + `c6i.16xlarge` **$2.72/hr** (the reference runs used `use_threads=cpu_count()` + = 64 on the pod); the compute-bound Kepler row uses 16 vCPU `c6i.4xlarge` + **$0.68/hr** to match the published 522 s / 16-core Kepler baseline. +- **Reference CPU** = the `transitleastsquares` package (Hippke & Heller 2019), + same forced Ofir period grid, `oversampling_factor=3`, all cores. Measured on + the pod for four regimes; the 4-year Kepler point (>15 min/LC) uses the + published 522 s figure (Ryzen 9 7950X, 16 cores; GTLS paper arXiv:2607.00348). + +## Cost per million light curves + +| Regime (ndata, periods) | A5000 | Ada | V100 | Cheapest GPU | Reference CPU | GPU savings | +|---|---:|---:|---:|---|---:|---:| +| TESS FFI (1.3K, 2.5K) | **$0.056** | $0.170 | $0.091 | A5000 $0.056 | $11,110 | ~199,000× | +| K2 90-d (4.3K, 9.7K) | **$0.138** | $0.356 | $0.247 | A5000 $0.138 | $16,226 | ~118,000× | +| TESS 2-min (19.7K, 2.5K)| **$0.125** | $0.283 | $0.197 | A5000 $0.125 | $15,643 | ~125,000× | +| TESS 1-yr (17K, 42K) | **$0.819** | $1.519 | $1.054 | A5000 $0.819 | $83,930 | ~102,000× | +| Kepler 4-yr (65K, 172K) | **$7.45** | $10.99 | $9.30 | A5000 $7.45 | $98,600 | ~13,000× | + +Two robust conclusions: + +1. **GPU TLS is dramatically cheaper than CPU TLS.** The ratio is the + throughput advantage (~3,000–12,000×) multiplied by the hourly-cost + advantage (a $0.16/hr GPU beats a multi-core CPU box), so it holds under any + reasonable CPU price — even pricing the CPU at the GPU's $0.16/hr leaves the + throughput gap intact. + +2. **The RTX A5000 is the cost sweet spot.** It is not always the fastest + (the V100 edges it on the biggest regime), but at $0.16/hr it is the + cheapest to operate in every regime. Fastest-per-dollar ≠ fastest. + +At A5000 rates, a full **TESS FFI sector–scale run of ~1 million light curves +costs about 6 cents** of GPU time; a **Kepler-depth 4-year, 65K-point, 172K-period +search of a million targets costs about $7.45** — versus roughly $100,000 for +the same million on the reference CPU pipeline. + +## Versus the only other GPU TLS (GTLS) + +GTLS (arXiv:2607.00348, submitted 1 Jul 2026; CuPy) is the sole other GPU TLS. +On a ~1500-day / 67K-point / 190K-period Kepler-class light curve it reports +**33.3 s/LC on an RTX 4090** (15.7× over reference TLS). cuvarbase does the +comparable Kepler-4yr configuration in **168 ms/LC on an A5000**. + +| | Time/LC | GPU $/hr | $/million LC | +|---|---:|---:|---:| +| GTLS, RTX 4090 | 33.3 s | ~$0.50 | ~$4,625 | +| cuvarbase, A5000 | 0.168 s | $0.16 | **$7.45** | + +≈ **620× cheaper** than GTLS, on a cheaper GPU. cuvarbase wins on hardware-hours +(hand-written kernels + phase-binned scan vs CuPy per-point) and on hardware +price (A5000 < 4090). + +## The honest caveat: fidelity + +The largest ratios are partly a fidelity trade, and the comparison is only fair +if that is stated: + +- **Default epoch grid.** cuvarbase's default coarse scan steps the transit + epoch at `t0_oversample=3` (~3 positions per transit duration), then runs an + **exact per-point refinement** at `refine_oversample=33` on the top candidate + periods. The reference package steps ~100× finer (`T0_FIT_MARGIN=0.01`) + *uniformly*. So cuvarbase evaluates far fewer coarse trials, which is a large + part of why it is faster — not implementation efficiency alone. +- **What we verified.** 100% injected-transit recovery in all five regimes on + all three GPUs; agreement with the reference package on the golden configs + (period error <1%, both packages flag the detection significant); coarse + chi² spectrum correlated 0.998 with the legacy per-point cuvarbase kernel. + This is strong evidence the default fidelity is science-useful, but it is not + a bit-for-bit statistical match to the reference's ~100× epoch grid, and no + full injection–recovery completeness campaign has been run yet (that is the + open D3 validation item; the module remains flagged EXPERIMENTAL). +- **Matched fidelity is still a win.** Raising `t0_oversample` toward the + reference grid costs roughly linearly in the coarse scan (~70% of Kepler-4yr + time). A reference-matched run is an estimated ~5–10× slower — order + **1–1.7 s/LC on an A5000, ~$40–75/million** — still **>20× faster/cheaper + than GTLS** and **>300× cheaper than the reference CPU**. (Estimate from the + stage profile; not yet measured end-to-end.) + +## Bottom line + +At default fidelity cuvarbase is, on the evidence here, both the fastest and the +cheapest TLS available — thousands of times cheaper than CPU TLS and ~600× +cheaper than the only other GPU TLS. Even conservatively adjusted to the +reference's finer epoch grid, it remains the cheapest by a large margin. The one +thing still owed before dropping the EXPERIMENTAL flag is a full +injection–recovery validation at matched fidelity (item D3), not a speed or cost +result. diff --git a/benchmarks/results/tls_survey_jul2026/tls_survey_a5000.json b/benchmarks/results/tls_survey_jul2026/tls_survey_a5000.json new file mode 100644 index 0000000..6df9fe4 --- /dev/null +++ b/benchmarks/results/tls_survey_jul2026/tls_survey_a5000.json @@ -0,0 +1,2464 @@ +{ + "script": "benchmark_tls_survey.py", + "timestamp": "2026-07-06T17:37:04", + "args": { + "regimes": "tess-ffi,k2,tess-2min,tess-yr,kepler-4yr", + "nlc": null, + "impls": "new,old,reference", + "ref_nlc": 1, + "old_nlc": 5, + "n_iter": 1, + "output": "tls_survey_a5000.json", + "quick": false, + "ref_all": false + }, + "regimes": { + "tess-ffi": { + "config": { + "ndata": 1310, + "baseline": 27.4, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 100 + }, + "nlc": 100, + "nperiods": 2486, + "impls": { + "new": { + "nlc": 100, + "total_s": 0.46537021710537374, + "times_s": [ + 0.46537021710537374 + ], + "ms_per_lc": 4.653702171053737, + "lc_per_s": 214.88268119521067, + "n_injected": 50, + "n_recovered": 50, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.194946765899658, + "median_sde_noise": 5.454893112182617, + "per_lc": [ + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005532100331038237, + "SDE": 14.04796028137207, + "chi2_min": 1336.756591796875 + }, + { + "injected": false, + "period": 0.6225233674049377, + "T0": 0.542300333010111, + "duration": 0.037806544452905655, + "depth": 0.0005694815190508962, + "SDE": 4.8387885093688965, + "chi2_min": 1315.3824462890625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0050460100173950195, + "SDE": 14.31605339050293, + "chi2_min": 1393.1041259765625 + }, + { + "injected": false, + "period": 2.0671958923339844, + "T0": 1.240600977982922, + "duration": 0.04742983356118202, + "depth": 0.001077972468920052, + "SDE": 5.3955254554748535, + "chi2_min": 1313.3258056640625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005532099865376949, + "SDE": 13.555395126342773, + "chi2_min": 1225.93603515625 + }, + { + "injected": false, + "period": 6.83004903793335, + "T0": 3.3232153407358425, + "duration": 0.14845600724220276, + "depth": 0.0010422980412840843, + "SDE": 5.729757308959961, + "chi2_min": 1240.1666259765625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005443216301500797, + "SDE": 14.323617935180664, + "chi2_min": 1269.003173828125 + }, + { + "injected": false, + "period": 1.3832758665084839, + "T0": 1.2062232874457806, + "duration": 0.0404709056019783, + "depth": 0.0010051717981696129, + "SDE": 6.807766437530518, + "chi2_min": 1265.138427734375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005494272336363792, + "SDE": 14.132584571838379, + "chi2_min": 1249.695068359375 + }, + { + "injected": false, + "period": 9.259944915771484, + "T0": 5.364545925151106, + "duration": 0.1282770186662674, + "depth": 0.0013460032641887665, + "SDE": 4.305014610290527, + "chi2_min": 1306.898681640625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005376006942242384, + "SDE": 13.244661331176758, + "chi2_min": 1383.9041748046875 + }, + { + "injected": false, + "period": 3.789888858795166, + "T0": 2.661259939658663, + "duration": 0.06252489984035492, + "depth": 0.001284691970795393, + "SDE": 6.250540256500244, + "chi2_min": 1278.526123046875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005833100527524948, + "SDE": 14.197710990905762, + "chi2_min": 1307.3194580078125 + }, + { + "injected": false, + "period": 0.694443941116333, + "T0": 0.5433730154117882, + "duration": 0.03825102746486664, + "depth": 0.0007502393564209342, + "SDE": 5.395812034606934, + "chi2_min": 1375.8001708984375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005461629945784807, + "SDE": 14.322482109069824, + "chi2_min": 1400.463134765625 + }, + { + "injected": false, + "period": 0.9697667956352234, + "T0": 0.6234215032222714, + "duration": 0.04170956835150719, + "depth": 0.0007615161594003439, + "SDE": 5.9738569259643555, + "chi2_min": 1345.572998046875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005138101056218147, + "SDE": 14.855719566345215, + "chi2_min": 1374.564697265625 + }, + { + "injected": false, + "period": 8.122530937194824, + "T0": 7.832263005316975, + "duration": 0.08061356842517853, + "depth": 0.0015345076099038124, + "SDE": 5.285528182983398, + "chi2_min": 1408.313232421875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005546604748815298, + "SDE": 14.231895446777344, + "chi2_min": 1320.038330078125 + }, + { + "injected": false, + "period": 6.6303558349609375, + "T0": 1.8257501173602577, + "duration": 0.09650145471096039, + "depth": 0.0013269076589494944, + "SDE": 6.51860237121582, + "chi2_min": 1293.273681640625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005588605999946594, + "SDE": 14.391213417053223, + "chi2_min": 1236.130126953125 + }, + { + "injected": false, + "period": 3.265150547027588, + "T0": 1.6735373315951279, + "duration": 0.0594947412610054, + "depth": 0.0011398136848583817, + "SDE": 4.954017162322998, + "chi2_min": 1306.6485595703125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005358254071325064, + "SDE": 14.025896072387695, + "chi2_min": 1378.85791015625 + }, + { + "injected": false, + "period": 0.9069516062736511, + "T0": 0.4873173068018559, + "duration": 0.041811149567365646, + "depth": 0.0007045392994768918, + "SDE": 7.13300895690918, + "chi2_min": 1326.76806640625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005511259660124779, + "SDE": 14.469883918762207, + "chi2_min": 1315.769775390625 + }, + { + "injected": false, + "period": 6.818082332611084, + "T0": 0.19493036610213732, + "duration": 0.06887488067150116, + "depth": 0.0014663777546957135, + "SDE": 4.655307292938232, + "chi2_min": 1332.3330078125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005003536585718393, + "SDE": 14.677703857421875, + "chi2_min": 1274.4622802734375 + }, + { + "injected": false, + "period": 1.3818519115447998, + "T0": 0.3538014247310741, + "duration": 0.040457021445035934, + "depth": 0.0008182454621419311, + "SDE": 5.503757476806641, + "chi2_min": 1271.795166015625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005669722333550453, + "SDE": 15.375238418579102, + "chi2_min": 1376.2421875 + }, + { + "injected": false, + "period": 9.743551254272461, + "T0": 5.753463031568231, + "duration": 0.09000206738710403, + "depth": 0.0015667061088606715, + "SDE": 5.434820652008057, + "chi2_min": 1213.2532958984375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.1792505532503128, + "depth": 0.005271698348224163, + "SDE": 14.827378273010254, + "chi2_min": 1277.588134765625 + }, + { + "injected": false, + "period": 3.751904010772705, + "T0": 3.4951949631796992, + "duration": 0.05644047260284424, + "depth": 0.0012609886471182108, + "SDE": 4.813412189483643, + "chi2_min": 1313.6197509765625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.1792505532503128, + "depth": 0.0049079651944339275, + "SDE": 14.521245002746582, + "chi2_min": 1385.3743896484375 + }, + { + "injected": false, + "period": 12.579136848449707, + "T0": 4.226824228778526, + "duration": 0.12552805244922638, + "depth": 0.0016740288119763136, + "SDE": 5.552786827087402, + "chi2_min": 1264.4737548828125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.004762173630297184, + "SDE": 14.101175308227539, + "chi2_min": 1356.297607421875 + }, + { + "injected": false, + "period": 2.6725428104400635, + "T0": 0.14406507034685756, + "duration": 0.06784135103225708, + "depth": 0.0009474533726461232, + "SDE": 6.514476299285889, + "chi2_min": 1290.8275146484375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005513247102499008, + "SDE": 14.359392166137695, + "chi2_min": 1438.327392578125 + }, + { + "injected": false, + "period": 0.6743714809417725, + "T0": 0.47082369012325387, + "duration": 0.038828302174806595, + "depth": 0.0007056481554172933, + "SDE": 6.8672099113464355, + "chi2_min": 1296.7098388671875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0056505389511585236, + "SDE": 14.969831466674805, + "chi2_min": 1335.3681640625 + }, + { + "injected": false, + "period": 3.817335605621338, + "T0": 0.36329775865793934, + "duration": 0.056766659021377563, + "depth": 0.001113416044972837, + "SDE": 4.920238018035889, + "chi2_min": 1314.1776123046875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00567090418189764, + "SDE": 14.075288772583008, + "chi2_min": 1247.64794921875 + }, + { + "injected": false, + "period": 8.94373893737793, + "T0": 4.204453678020741, + "duration": 0.07539563626050949, + "depth": 0.0017777399625629187, + "SDE": 4.999449729919434, + "chi2_min": 1302.4117431640625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005472837947309017, + "SDE": 14.669474601745605, + "chi2_min": 1300.7388916015625 + }, + { + "injected": false, + "period": 5.107228755950928, + "T0": 0.25593410245988935, + "duration": 0.0625511109828949, + "depth": 0.0013419573660939932, + "SDE": 5.736141681671143, + "chi2_min": 1301.5399169921875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005429895129054785, + "SDE": 13.518494606018066, + "chi2_min": 1338.432861328125 + }, + { + "injected": false, + "period": 8.641767501831055, + "T0": 4.278318558921512, + "duration": 0.0822959765791893, + "depth": 0.0016973762540146708, + "SDE": 4.410860061645508, + "chi2_min": 1327.6866455078125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005096357315778732, + "SDE": 13.462990760803223, + "chi2_min": 1241.7001953125 + }, + { + "injected": false, + "period": 10.220306396484375, + "T0": 4.387660793062423, + "duration": 0.07882454991340637, + "depth": 0.001610369305126369, + "SDE": 4.691564559936523, + "chi2_min": 1268.614013671875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0051406449638307095, + "SDE": 14.042201042175293, + "chi2_min": 1313.3265380859375 + }, + { + "injected": false, + "period": 9.36865520477295, + "T0": 9.22744536080586, + "duration": 0.1387048214673996, + "depth": 0.0016929314006119967, + "SDE": 6.051858425140381, + "chi2_min": 1253.9193115234375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005635188426822424, + "SDE": 14.192182540893555, + "chi2_min": 1322.7122802734375 + }, + { + "injected": false, + "period": 0.8091202974319458, + "T0": 0.4492196189062554, + "duration": 0.04125908017158508, + "depth": 0.0006857005064375699, + "SDE": 5.732414722442627, + "chi2_min": 1371.5711669921875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.1792505532503128, + "depth": 0.00515005411580205, + "SDE": 13.765022277832031, + "chi2_min": 1289.6336669921875 + }, + { + "injected": false, + "period": 0.8660217523574829, + "T0": 0.2381559870601997, + "duration": 0.06924363225698471, + "depth": 0.000580161577090621, + "SDE": 6.823153495788574, + "chi2_min": 1302.041015625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005201749037951231, + "SDE": 14.920594215393066, + "chi2_min": 1310.6806640625 + }, + { + "injected": false, + "period": 6.253728866577148, + "T0": 5.870805941826802, + "duration": 0.07388520240783691, + "depth": 0.0012998901074752212, + "SDE": 4.990450859069824, + "chi2_min": 1209.4974365234375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005448551848530769, + "SDE": 13.58593463897705, + "chi2_min": 1390.4388427734375 + }, + { + "injected": false, + "period": 12.104578018188477, + "T0": 9.877395361733647, + "duration": 0.09207913279533386, + "depth": 0.0019856933504343033, + "SDE": 5.132411479949951, + "chi2_min": 1289.7191162109375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.005371259059756994, + "SDE": 14.033452987670898, + "chi2_min": 1326.8525390625 + }, + { + "injected": false, + "period": 6.688271522521973, + "T0": 2.686991147148291, + "duration": 0.08342314511537552, + "depth": 0.0015159600879997015, + "SDE": 6.502882957458496, + "chi2_min": 1376.671142578125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00503845140337944, + "SDE": 14.093024253845215, + "chi2_min": 1418.729248046875 + }, + { + "injected": false, + "period": 2.585336923599243, + "T0": 0.923334637585171, + "duration": 0.13419054448604584, + "depth": 0.0006379691185429692, + "SDE": 5.249675273895264, + "chi2_min": 1297.920654296875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0051691788248717785, + "SDE": 14.503507614135742, + "chi2_min": 1335.154541015625 + }, + { + "injected": false, + "period": 1.1863054037094116, + "T0": 0.5719344635827, + "duration": 0.04040246084332466, + "depth": 0.000764436786994338, + "SDE": 4.693112850189209, + "chi2_min": 1279.0152587890625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005427448078989983, + "SDE": 13.62148380279541, + "chi2_min": 1318.234130859375 + }, + { + "injected": false, + "period": 2.7281653881073, + "T0": 1.4686617796954948, + "duration": 0.06830879300832748, + "depth": 0.000905516732018441, + "SDE": 5.483219146728516, + "chi2_min": 1256.6185302734375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005423018243163824, + "SDE": 13.093084335327148, + "chi2_min": 1284.7784423828125 + }, + { + "injected": false, + "period": 4.76552152633667, + "T0": 1.0766223941304887, + "duration": 0.06748615950345993, + "depth": 0.0013720967108383775, + "SDE": 5.666236877441406, + "chi2_min": 1268.8609619140625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.1792505532503128, + "depth": 0.00524185923859477, + "SDE": 13.730987548828125, + "chi2_min": 1468.949462890625 + }, + { + "injected": false, + "period": 5.954711437225342, + "T0": 4.0025587667688, + "duration": 0.06583552062511444, + "depth": 0.0015804577851668, + "SDE": 4.635071754455566, + "chi2_min": 1338.395751953125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3166635296710325, + "duration": 0.17486760020256042, + "depth": 0.005472453776746988, + "SDE": 13.930768966674805, + "chi2_min": 1231.414794921875 + }, + { + "injected": false, + "period": 0.7494922876358032, + "T0": 0.14383159056938233, + "duration": 0.06598714739084244, + "depth": 0.0004580834647640586, + "SDE": 5.942159652709961, + "chi2_min": 1183.9090576171875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00511926831677556, + "SDE": 13.571946144104004, + "chi2_min": 1362.548095703125 + }, + { + "injected": false, + "period": 2.8218910694122314, + "T0": 0.3113438265453894, + "duration": 0.07258859276771545, + "depth": 0.0010486795799806714, + "SDE": 6.7492289543151855, + "chi2_min": 1301.56494140625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005257213022559881, + "SDE": 14.49308967590332, + "chi2_min": 1288.70556640625 + }, + { + "injected": false, + "period": 7.533771991729736, + "T0": 2.4072671396336887, + "duration": 0.07120505720376968, + "depth": 0.0016296287067234516, + "SDE": 5.135932445526123, + "chi2_min": 1290.2734375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005214039236307144, + "SDE": 13.498370170593262, + "chi2_min": 1333.4521484375 + }, + { + "injected": false, + "period": 2.043039560317993, + "T0": 0.20179237219836565, + "duration": 0.046089161187410355, + "depth": 0.0010053182486444712, + "SDE": 5.474965572357178, + "chi2_min": 1390.2979736328125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.302647882457407, + "duration": 0.19306959211826324, + "depth": 0.005054525565356016, + "SDE": 13.49543285369873, + "chi2_min": 1318.240234375 + }, + { + "injected": false, + "period": 9.047518730163574, + "T0": 4.986207971831902, + "duration": 0.12417609244585037, + "depth": 0.0012691729934886098, + "SDE": 5.239851474761963, + "chi2_min": 1229.02001953125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.0051093571819365025, + "SDE": 15.082120895385742, + "chi2_min": 1269.2886962890625 + }, + { + "injected": false, + "period": 13.309419631958008, + "T0": 8.291711655584322, + "duration": 0.0860782042145729, + "depth": 0.0020279965829104185, + "SDE": 5.076119422912598, + "chi2_min": 1284.479736328125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005712154787033796, + "SDE": 13.651554107666016, + "chi2_min": 1391.5516357421875 + }, + { + "injected": false, + "period": 3.42227840423584, + "T0": 1.513876878042396, + "duration": 0.05473672226071358, + "depth": 0.0012891051592305303, + "SDE": 5.590603828430176, + "chi2_min": 1319.033935546875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005534757860004902, + "SDE": 14.234055519104004, + "chi2_min": 1310.23583984375 + }, + { + "injected": false, + "period": 13.635416984558105, + "T0": 4.660816806064474, + "duration": 0.18693023920059204, + "depth": 0.0013690313789993525, + "SDE": 5.641323566436768, + "chi2_min": 1203.140625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.1792505532503128, + "depth": 0.005294170696288347, + "SDE": 13.605644226074219, + "chi2_min": 1339.434326171875 + }, + { + "injected": false, + "period": 12.15611457824707, + "T0": 4.847994174497785, + "duration": 0.15128548443317413, + "depth": 0.0013391560642048717, + "SDE": 5.003466606140137, + "chi2_min": 1278.67333984375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005288905929774046, + "SDE": 14.548331260681152, + "chi2_min": 1395.673583984375 + }, + { + "injected": false, + "period": 9.978133201599121, + "T0": 3.989828930425375, + "duration": 0.23240043222904205, + "depth": 0.001154838828369975, + "SDE": 6.545147895812988, + "chi2_min": 1311.4876708984375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00547023443505168, + "SDE": 13.900687217712402, + "chi2_min": 1303.812255859375 + }, + { + "injected": false, + "period": 1.227888584136963, + "T0": 0.24339220282239182, + "duration": 0.0622534416615963, + "depth": 0.0007609375170432031, + "SDE": 6.777478218078613, + "chi2_min": 1254.4796142578125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004985042382031679, + "SDE": 14.515047073364258, + "chi2_min": 1355.400634765625 + }, + { + "injected": false, + "period": 5.7309746742248535, + "T0": 2.468525694331092, + "duration": 0.06500045210123062, + "depth": 0.0014444179832935333, + "SDE": 5.217517375946045, + "chi2_min": 1338.3822021484375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.004980752710253, + "SDE": 14.50423812866211, + "chi2_min": 1379.7135009765625 + }, + { + "injected": false, + "period": 3.958618640899658, + "T0": 1.5509364717303242, + "duration": 0.0574585385620594, + "depth": 0.001268385211005807, + "SDE": 5.37081241607666, + "chi2_min": 1282.2177734375 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005414345767349005, + "SDE": 15.570334434509277, + "chi2_min": 1338.760498046875 + }, + { + "injected": false, + "period": 12.286243438720703, + "T0": 0.6104409236496622, + "duration": 0.08381339907646179, + "depth": 0.001824214938096702, + "SDE": 6.343598365783691, + "chi2_min": 1332.7315673828125 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005437149666249752, + "SDE": 14.980276107788086, + "chi2_min": 1358.66748046875 + }, + { + "injected": false, + "period": 6.318080902099609, + "T0": 0.0469745786837219, + "duration": 0.06714829802513123, + "depth": 0.0013039936311542988, + "SDE": 4.6731061935424805, + "chi2_min": 1247.87646484375 + } + ] + }, + "old": { + "nlc": 5, + "total_s": 0.4165675728581846, + "per_call_s": [ + 0.08606805291492492, + 0.08085096499416977, + 0.08287583803758025, + 0.08341802400536835, + 0.08335469290614128 + ], + "ms_per_lc": 83.35469290614128, + "lc_per_s": 11.99692500968141, + "extrapolated": true, + "note": "per-LC median over 5 LCs", + "n_injected": 3, + "n_recovered": 3, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.214750289916992, + "median_sde_noise": 5.1948442459106445, + "per_lc": [ + { + "injected": true, + "period": 7.70003080368042, + "T0": 0.30158731341362, + "duration": 0.18374329805374146, + "depth": 0.005273154005408287, + "SDE": 14.214750289916992, + "chi2_min": 1366.7169189453125 + }, + { + "injected": false, + "period": 0.6225233674049377, + "T0": 0.875, + "duration": 0.039725493639707565, + "depth": 0.0005230337847024202, + "SDE": 4.7374186515808105, + "chi2_min": 1317.3992919921875 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 0.30158731341362, + "duration": 0.18374329805374146, + "depth": 0.004768824204802513, + "SDE": 14.500447273254395, + "chi2_min": 1427.902587890625 + }, + { + "injected": false, + "period": 4.131380081176758, + "T0": 0.30049261450767517, + "duration": 0.06124073639512062, + "depth": 0.001288519473746419, + "SDE": 5.6522698402404785, + "chi2_min": 1313.3834228515625 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 0.30158731341362, + "duration": 0.18374329805374146, + "depth": 0.005183448549360037, + "SDE": 13.707527160644531, + "chi2_min": 1279.4794921875 + } + ] + }, + "reference": { + "nlc": 1, + "total_s": 14.70412063098047, + "per_call_s": [ + 14.70412063098047 + ], + "ms_per_lc": 14704.12063098047, + "lc_per_s": 0.0680081471783546, + "extrapolated": true, + "note": "per-LC median over 1 LCs", + "n_injected": 1, + "n_recovered": 1, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 17.433094918772902, + "median_sde_noise": null, + "per_lc": [ + { + "injected": true, + "period": 7.707421629851162, + "T0": 2.3130252400704143, + "duration": 0.17014672798958713, + "depth": 0.9944062766363239, + "SDE": 17.433094918772902, + "chi2_min": null + } + ] + } + } + }, + "k2": { + "config": { + "ndata": 4320, + "baseline": 90.0, + "cadence": 0.020833333333333332, + "noise": 0.0008, + "inject_period": 12.4, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 45.0, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 9672, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.7236975010018796, + "times_s": [ + 0.7236975010018796 + ], + "ms_per_lc": 14.473950020037591, + "lc_per_s": 69.08964025823013, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 25.313722610473633, + "median_sde_noise": 6.645591735839844, + "per_lc": [ + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004352097399532795, + "SDE": 26.41286849975586, + "chi2_min": 4387.0771484375 + }, + { + "injected": false, + "period": 1.3685027360916138, + "T0": 0.9995278422767413, + "duration": 0.044523872435092926, + "depth": 0.0003865647013299167, + "SDE": 6.258840084075928, + "chi2_min": 4326.8408203125 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004342587199062109, + "SDE": 25.128662109375, + "chi2_min": 4408.6591796875 + }, + { + "injected": false, + "period": 1.567154884338379, + "T0": 0.6710433548500703, + "duration": 0.046581678092479706, + "depth": 0.00046604909584857523, + "SDE": 10.426676750183105, + "chi2_min": 4249.2431640625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004444643389433622, + "SDE": 25.042335510253906, + "chi2_min": 4386.859375 + }, + { + "injected": false, + "period": 17.946809768676758, + "T0": 11.771186328499653, + "duration": 0.1949620395898819, + "depth": 0.0006966719520278275, + "SDE": 6.579500675201416, + "chi2_min": 4314.2119140625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.00431243097409606, + "SDE": 26.375844955444336, + "chi2_min": 4341.9091796875 + }, + { + "injected": false, + "period": 7.888498306274414, + "T0": 7.531212754637977, + "duration": 0.08388378471136093, + "depth": 0.0007113401079550385, + "SDE": 6.401275157928467, + "chi2_min": 4085.73876953125 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.204982191324234, + "depth": 0.004326032940298319, + "SDE": 26.117460250854492, + "chi2_min": 4431.56103515625 + }, + { + "injected": false, + "period": 1.7248963117599487, + "T0": 0.09257843054204251, + "duration": 0.14293743669986725, + "depth": 0.0002644819614943117, + "SDE": 8.722923278808594, + "chi2_min": 4256.3544921875 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19997018575668335, + "depth": 0.004363611340522766, + "SDE": 24.74551773071289, + "chi2_min": 4470.55859375 + }, + { + "injected": false, + "period": 40.31962203979492, + "T0": 12.79962811146072, + "duration": 0.1850813776254654, + "depth": 0.001074489438906312, + "SDE": 6.051089763641357, + "chi2_min": 4326.759765625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.0042357980273664, + "SDE": 25.487497329711914, + "chi2_min": 4373.689453125 + }, + { + "injected": false, + "period": 13.618805885314941, + "T0": 12.356419633092571, + "duration": 0.10573731362819672, + "depth": 0.0009254583856090903, + "SDE": 6.189550876617432, + "chi2_min": 4297.984375 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004302197601646185, + "SDE": 24.595088958740234, + "chi2_min": 4261.193359375 + }, + { + "injected": false, + "period": 8.089868545532227, + "T0": 4.885035835718213, + "duration": 0.09111291915178299, + "depth": 0.0007423317874781787, + "SDE": 7.2754950523376465, + "chi2_min": 4085.354248046875 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0042641134932637215, + "SDE": 23.71144676208496, + "chi2_min": 4317.3330078125 + }, + { + "injected": false, + "period": 15.671825408935547, + "T0": 11.627483488450707, + "duration": 0.36358538269996643, + "depth": 0.0004862357454840094, + "SDE": 5.54551887512207, + "chi2_min": 4251.71435546875 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21007460355758667, + "depth": 0.0042679025791585445, + "SDE": 25.32743263244629, + "chi2_min": 4252.84814453125 + }, + { + "injected": false, + "period": 2.754897117614746, + "T0": 2.5983700068006783, + "duration": 0.08354046940803528, + "depth": 0.00038990896428003907, + "SDE": 6.645591735839844, + "chi2_min": 4304.2802734375 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.0041894433088600636, + "SDE": 25.313722610473633, + "chi2_min": 4441.63818359375 + }, + { + "injected": false, + "period": 7.901713848114014, + "T0": 5.554735825406851, + "duration": 0.10750557482242584, + "depth": 0.0006463827448897064, + "SDE": 6.50754976272583, + "chi2_min": 4282.33251953125 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.00431278720498085, + "SDE": 25.72516441345215, + "chi2_min": 4257.3173828125 + }, + { + "injected": false, + "period": 2.0137128829956055, + "T0": 1.3472117866492113, + "duration": 0.05591322481632233, + "depth": 0.0004975693300366402, + "SDE": 10.68542194366455, + "chi2_min": 4280.7685546875 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004413061309605837, + "SDE": 25.41738510131836, + "chi2_min": 4409.75390625 + }, + { + "injected": false, + "period": 16.01749610900879, + "T0": 4.732432745412893, + "duration": 0.09385470300912857, + "depth": 0.0008603514288552105, + "SDE": 5.5665788650512695, + "chi2_min": 4099.48876953125 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19997018575668335, + "depth": 0.004251678939908743, + "SDE": 26.01239776611328, + "chi2_min": 4506.17333984375 + }, + { + "injected": false, + "period": 10.265218734741211, + "T0": 3.1706764167317942, + "duration": 0.0893411710858345, + "depth": 0.000808034441433847, + "SDE": 8.316948890686035, + "chi2_min": 4252.4541015625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004466493614017963, + "SDE": 24.81790542602539, + "chi2_min": 4458.2509765625 + }, + { + "injected": false, + "period": 30.56487464904785, + "T0": 7.08230056832997, + "duration": 0.1285296082496643, + "depth": 0.0011698486050590873, + "SDE": 5.639177322387695, + "chi2_min": 4209.0498046875 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0043472908437252045, + "SDE": 25.853296279907227, + "chi2_min": 4367.650390625 + }, + { + "injected": false, + "period": 36.307926177978516, + "T0": 19.91623931560639, + "duration": 0.29323291778564453, + "depth": 0.000972222478594631, + "SDE": 6.6832427978515625, + "chi2_min": 4425.97998046875 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004358186852186918, + "SDE": 25.013660430908203, + "chi2_min": 4418.55126953125 + }, + { + "injected": false, + "period": 3.8394787311553955, + "T0": 3.5172907226911434, + "duration": 0.08880756050348282, + "depth": 0.0004855781444348395, + "SDE": 6.711667537689209, + "chi2_min": 4251.2158203125 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.204982191324234, + "depth": 0.0042197867296636105, + "SDE": 27.17059326171875, + "chi2_min": 4318.37890625 + }, + { + "injected": false, + "period": 1.1502342224121094, + "T0": 0.1958829591745257, + "duration": 0.04996860772371292, + "depth": 0.00035351532278582454, + "SDE": 7.501292705535889, + "chi2_min": 4534.52490234375 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004392570350319147, + "SDE": 26.404085159301758, + "chi2_min": 4508.17822265625 + }, + { + "injected": false, + "period": 5.259787082672119, + "T0": 1.6824305120182999, + "duration": 0.07700253278017044, + "depth": 0.0005853439797647297, + "SDE": 6.906394958496094, + "chi2_min": 4316.62890625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.0045080301351845264, + "SDE": 25.257204055786133, + "chi2_min": 4411.8984375 + }, + { + "injected": false, + "period": 14.582316398620605, + "T0": 8.464501963214104, + "duration": 0.08873925358057022, + "depth": 0.0010340927401557565, + "SDE": 7.145528793334961, + "chi2_min": 4359.5087890625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004241089802235365, + "SDE": 25.864206314086914, + "chi2_min": 4505.0234375 + }, + { + "injected": false, + "period": 12.45097541809082, + "T0": 9.849355446693153, + "duration": 0.14158391952514648, + "depth": 0.0007936642505228519, + "SDE": 8.454061508178711, + "chi2_min": 4300.4619140625 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.7385776951641674, + "duration": 0.21007460355758667, + "depth": 0.004096492659300566, + "SDE": 24.88788414001465, + "chi2_min": 4323.14990234375 + }, + { + "injected": false, + "period": 2.9074718952178955, + "T0": 1.8813054051288134, + "duration": 0.07331513613462448, + "depth": 0.0004352267715148628, + "SDE": 6.141500949859619, + "chi2_min": 4107.63916015625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.00426055071875453, + "SDE": 24.827207565307617, + "chi2_min": 4553.47021484375 + }, + { + "injected": false, + "period": 3.8546807765960693, + "T0": 3.842780059564987, + "duration": 0.20632818341255188, + "depth": 0.0003478155704215169, + "SDE": 5.401646614074707, + "chi2_min": 4368.59912109375 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004260950721800327, + "SDE": 24.611242294311523, + "chi2_min": 4400.51953125 + }, + { + "injected": false, + "period": 5.128723621368408, + "T0": 1.3880361595794426, + "duration": 0.11346686631441116, + "depth": 0.0004658329125959426, + "SDE": 6.783279895782471, + "chi2_min": 4368.8291015625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19997018575668335, + "depth": 0.004382762126624584, + "SDE": 24.896284103393555, + "chi2_min": 4545.61572265625 + }, + { + "injected": false, + "period": 1.5529592037200928, + "T0": 1.0412210721013935, + "duration": 0.09288113564252853, + "depth": 0.0003009258070960641, + "SDE": 6.297451496124268, + "chi2_min": 4323.4833984375 + } + ] + }, + "old": { + "skipped": "ndata cap" + }, + "reference": { + "nlc": 1, + "total_s": 21.475801305961795, + "per_call_s": [ + 21.475801305961795 + ], + "ms_per_lc": 21475.801305961795, + "lc_per_s": 0.046564036691957786, + "extrapolated": true, + "note": "per-LC median over 1 LCs", + "n_injected": 1, + "n_recovered": 1, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 30.963206358691103, + "median_sde_noise": null, + "per_lc": [ + { + "injected": true, + "period": 12.401754384696078, + "T0": 3.7201060631824645, + "duration": 0.19268825330556646, + "depth": 0.9956597462534411, + "SDE": 30.963206358691103, + "chi2_min": null + } + ] + } + } + }, + "tess-2min": { + "config": { + "ndata": 19710, + "baseline": 27.4, + "cadence": 0.001388888888888889, + "noise": 0.002, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 2497, + "impls": { + "new": { + "nlc": 50, + "total_s": 1.6093172129476443, + "times_s": [ + 1.6093172129476443 + ], + "ms_per_lc": 32.186344258952886, + "lc_per_s": 31.069076747411042, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.725510597229004, + "median_sde_noise": 5.81863260269165, + "per_lc": [ + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005485763773322105, + "SDE": 14.625007629394531, + "chi2_min": 19986.646484375 + }, + { + "injected": false, + "period": 13.100994110107422, + "T0": 5.437705039673006, + "duration": 0.10182765871286392, + "depth": 0.000987470499239862, + "SDE": 7.187177658081055, + "chi2_min": 19991.27734375 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005453882738947868, + "SDE": 13.996513366699219, + "chi2_min": 20026.580078125 + }, + { + "injected": false, + "period": 0.727395236492157, + "T0": 0.3019376498355886, + "duration": 0.042890265583992004, + "depth": 0.00036336382618173957, + "SDE": 5.8260416984558105, + "chi2_min": 19321.81640625 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0054051559418439865, + "SDE": 14.713297843933105, + "chi2_min": 19669.765625 + }, + { + "injected": false, + "period": 2.640021324157715, + "T0": 0.4917686880570642, + "duration": 0.0764675959944725, + "depth": 0.00048310792772099376, + "SDE": 6.617506980895996, + "chi2_min": 19593.427734375 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0054057748056948185, + "SDE": 14.754934310913086, + "chi2_min": 19828.859375 + }, + { + "injected": false, + "period": 9.231582641601562, + "T0": 1.668764258751935, + "duration": 0.07619598507881165, + "depth": 0.0008226699428632855, + "SDE": 4.714282989501953, + "chi2_min": 20048.86328125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005362111143767834, + "SDE": 14.330925941467285, + "chi2_min": 19451.40625 + }, + { + "injected": false, + "period": 1.5595471858978271, + "T0": 0.281584903400649, + "duration": 0.06259265542030334, + "depth": 0.0003787315508816391, + "SDE": 6.048738479614258, + "chi2_min": 19451.8203125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005432445090264082, + "SDE": 14.942078590393066, + "chi2_min": 19835.658203125 + }, + { + "injected": false, + "period": 3.7597527503967285, + "T0": 3.551468894538374, + "duration": 0.09266479313373566, + "depth": 0.0005465586436912417, + "SDE": 5.56866979598999, + "chi2_min": 19562.216796875 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005425632931292057, + "SDE": 14.515064239501953, + "chi2_min": 19661.443359375 + }, + { + "injected": false, + "period": 5.938205242156982, + "T0": 1.8585079530804478, + "duration": 0.0977407768368721, + "depth": 0.0006803354481235147, + "SDE": 5.1869988441467285, + "chi2_min": 19645.716796875 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.00532762985676527, + "SDE": 14.817203521728516, + "chi2_min": 19854.83984375 + }, + { + "injected": false, + "period": 0.6239511370658875, + "T0": 0.1721244509631692, + "duration": 0.03426845371723175, + "depth": 0.00035405956441536546, + "SDE": 6.364353656768799, + "chi2_min": 19814.71875 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005372270941734314, + "SDE": 14.703107833862305, + "chi2_min": 19759.875 + }, + { + "injected": false, + "period": 1.7957980632781982, + "T0": 0.0708701585838174, + "duration": 0.049966782331466675, + "depth": 0.0004548485158011317, + "SDE": 5.303484916687012, + "chi2_min": 19886.9140625 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0056047262623906136, + "SDE": 14.878658294677734, + "chi2_min": 19847.30859375 + }, + { + "injected": false, + "period": 1.420212745666504, + "T0": 0.05407682774567846, + "duration": 0.060670167207717896, + "depth": 0.00034811272053048015, + "SDE": 5.3370771408081055, + "chi2_min": 19686.30078125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005634222645312548, + "SDE": 15.200629234313965, + "chi2_min": 20082.625 + }, + { + "injected": false, + "period": 1.944218635559082, + "T0": 1.0748527127935859, + "duration": 0.04533376544713974, + "depth": 0.0005115983076393604, + "SDE": 6.226770401000977, + "chi2_min": 19728.376953125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.00562666542828083, + "SDE": 14.830127716064453, + "chi2_min": 19887.3671875 + }, + { + "injected": false, + "period": 5.338284969329834, + "T0": 1.2329951883274148, + "duration": 0.2083013355731964, + "depth": 0.00045530812349170446, + "SDE": 5.81863260269165, + "chi2_min": 19457.34765625 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005390736740082502, + "SDE": 14.687355041503906, + "chi2_min": 19622.46484375 + }, + { + "injected": false, + "period": 1.9001942873001099, + "T0": 1.6714522132299692, + "duration": 0.1670754998922348, + "depth": 0.0003015141701325774, + "SDE": 6.128674507141113, + "chi2_min": 19683.650390625 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005256369709968567, + "SDE": 14.609891891479492, + "chi2_min": 19651.33203125 + }, + { + "injected": false, + "period": 2.132042646408081, + "T0": 1.8807319745508266, + "duration": 0.051615022122859955, + "depth": 0.0005292932619340718, + "SDE": 5.569215297698975, + "chi2_min": 19577.80078125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005402362439781427, + "SDE": 14.439190864562988, + "chi2_min": 19436.732421875 + }, + { + "injected": false, + "period": 2.430079221725464, + "T0": 2.2653281420587916, + "duration": 0.13144873082637787, + "depth": 0.00038963276892900467, + "SDE": 6.2423787117004395, + "chi2_min": 19901.732421875 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17922694981098175, + "depth": 0.005089751910418272, + "SDE": 14.021259307861328, + "chi2_min": 20220.5703125 + }, + { + "injected": false, + "period": 9.231582641601562, + "T0": 3.019791075150806, + "duration": 0.07810574769973755, + "depth": 0.0008826106204651296, + "SDE": 5.6142354011535645, + "chi2_min": 19451.4296875 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005317771341651678, + "SDE": 15.05292797088623, + "chi2_min": 19643.59765625 + }, + { + "injected": false, + "period": 8.733040809631348, + "T0": 1.2241375973440398, + "duration": 0.07479896396398544, + "depth": 0.0009044495527632535, + "SDE": 5.337958335876465, + "chi2_min": 19883.51953125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005483758170157671, + "SDE": 14.85096549987793, + "chi2_min": 19639.47265625 + }, + { + "injected": false, + "period": 1.0538432598114014, + "T0": 0.8465298903467868, + "duration": 0.04974828660488129, + "depth": 0.00036485373857431114, + "SDE": 7.628678798675537, + "chi2_min": 19180.42578125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0054765734821558, + "SDE": 14.937609672546387, + "chi2_min": 19795.39453125 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 1.438457715678659, + "duration": 0.07876930385828018, + "depth": 0.00038677072734571993, + "SDE": 7.276026725769043, + "chi2_min": 19453.857421875 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0053500221110880375, + "SDE": 14.881278991699219, + "chi2_min": 20215.64453125 + }, + { + "injected": false, + "period": 9.213768005371094, + "T0": 3.8128516220795063, + "duration": 0.07614698261022568, + "depth": 0.0008834435138851404, + "SDE": 5.695574760437012, + "chi2_min": 19582.09765625 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0054364390671253204, + "SDE": 14.983465194702148, + "chi2_min": 19810.994140625 + }, + { + "injected": false, + "period": 1.186469316482544, + "T0": 0.444925993680954, + "duration": 0.04687422513961792, + "depth": 0.0003952206752728671, + "SDE": 5.102463245391846, + "chi2_min": 19782.31640625 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0054940530098974705, + "SDE": 14.933070182800293, + "chi2_min": 19692.0625 + }, + { + "injected": false, + "period": 1.7699131965637207, + "T0": 1.468044076797554, + "duration": 0.04972551763057709, + "depth": 0.0005097914836369455, + "SDE": 6.834410667419434, + "chi2_min": 19890.083984375 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0053437440656125546, + "SDE": 14.725510597229004, + "chi2_min": 19796.72265625 + }, + { + "injected": false, + "period": 1.6712085008621216, + "T0": 0.23874408222442334, + "duration": 0.05521129071712494, + "depth": 0.0004521652008406818, + "SDE": 6.857421398162842, + "chi2_min": 19370.162109375 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005551040638238192, + "SDE": 14.696364402770996, + "chi2_min": 19685.80078125 + }, + { + "injected": false, + "period": 0.9706023335456848, + "T0": 0.9521787137664184, + "duration": 0.07192568480968475, + "depth": 0.00030540055013261735, + "SDE": 5.069840908050537, + "chi2_min": 20024.76953125 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005364942830055952, + "SDE": 14.3045015335083, + "chi2_min": 19449.197265625 + }, + { + "injected": false, + "period": 3.3878562450408936, + "T0": 2.242408262616337, + "duration": 0.054552532732486725, + "depth": 0.0006831723148934543, + "SDE": 5.030765056610107, + "chi2_min": 19772.6015625 + } + ] + }, + "old": { + "skipped": "ndata cap" + }, + "reference": { + "nlc": 1, + "total_s": 20.703610604046844, + "per_call_s": [ + 20.703610604046844 + ], + "ms_per_lc": 20703.610604046844, + "lc_per_s": 0.048300753869691425, + "extrapolated": true, + "note": "per-LC median over 1 LCs", + "n_injected": 1, + "n_recovered": 1, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 15.311789171693592, + "median_sde_noise": null, + "per_lc": [ + { + "injected": true, + "period": 7.697715495144377, + "T0": 2.3152336551975656, + "duration": 0.15490975491224693, + "depth": 0.9942281780912013, + "SDE": 15.311789171693592, + "chi2_min": null + } + ] + } + } + }, + "tess-yr": { + "config": { + "ndata": 16850, + "baseline": 351.0, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 21.7, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 175.0, + "nlc": 20 + }, + "nlc": 20, + "nperiods": 42001, + "impls": { + "new": { + "nlc": 20, + "total_s": 2.329541099956259, + "times_s": [ + 2.329541099956259 + ], + "ms_per_lc": 116.47705499781296, + "lc_per_s": 8.585381902201911, + "n_injected": 10, + "n_recovered": 10, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 52.50939750671387, + "median_sde_noise": 8.135910511016846, + "per_lc": [ + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004285216797143221, + "SDE": 51.96808624267578, + "chi2_min": 17045.296875 + }, + { + "injected": false, + "period": 42.48393630981445, + "T0": 24.362156216019912, + "duration": 0.15449827909469604, + "depth": 0.0008234648266807199, + "SDE": 8.125212669372559, + "chi2_min": 16853.18359375 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004315285012125969, + "SDE": 53.290836334228516, + "chi2_min": 17079.37109375 + }, + { + "injected": false, + "period": 3.215691566467285, + "T0": 2.7022411507869606, + "duration": 0.05919283628463745, + "depth": 0.00034805055474862456, + "SDE": 7.1917243003845215, + "chi2_min": 16563.556640625 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004378132522106171, + "SDE": 51.542381286621094, + "chi2_min": 16690.8125 + }, + { + "injected": false, + "period": 115.44019317626953, + "T0": 40.610479753671825, + "duration": 0.19526782631874084, + "depth": 0.0012061776360496879, + "SDE": 8.25966739654541, + "chi2_min": 16943.583984375 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004271373152732849, + "SDE": 53.543060302734375, + "chi2_min": 16960.623046875 + }, + { + "injected": false, + "period": 13.20435905456543, + "T0": 1.339143794434733, + "duration": 0.2093072384595871, + "depth": 0.00039137076237238944, + "SDE": 8.067180633544922, + "chi2_min": 16618.20703125 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004417840391397476, + "SDE": 53.131507873535156, + "chi2_min": 17073.46484375 + }, + { + "injected": false, + "period": 1.0810301303863525, + "T0": 0.8323784887314218, + "duration": 0.03727799281477928, + "depth": 0.00030445540323853493, + "SDE": 11.814228057861328, + "chi2_min": 16803.59375 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004163388162851334, + "SDE": 51.01511764526367, + "chi2_min": 17136.814453125 + }, + { + "injected": false, + "period": 35.858131408691406, + "T0": 3.57559711400188, + "duration": 0.3145294189453125, + "depth": 0.0005174473044462502, + "SDE": 7.057640552520752, + "chi2_min": 16644.427734375 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2409670054912567, + "depth": 0.004405491054058075, + "SDE": 53.55682373046875, + "chi2_min": 17381.18359375 + }, + { + "injected": false, + "period": 1.4476821422576904, + "T0": 1.3089501248304032, + "duration": 0.04536651819944382, + "depth": 0.0002738697803579271, + "SDE": 8.49048900604248, + "chi2_min": 17168.869140625 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.00433772336691618, + "SDE": 52.23996353149414, + "chi2_min": 17070.54296875 + }, + { + "injected": false, + "period": 158.6757354736328, + "T0": 94.3506197494853, + "duration": 0.4914354979991913, + "depth": 0.0008981031714938581, + "SDE": 7.7233781814575195, + "chi2_min": 16733.61328125 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004398007411509752, + "SDE": 52.778831481933594, + "chi2_min": 17030.78125 + }, + { + "injected": false, + "period": 71.84587860107422, + "T0": 33.78735158730137, + "duration": 0.17517852783203125, + "depth": 0.0010416145669296384, + "SDE": 8.146608352661133, + "chi2_min": 16928.7578125 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.00430144090205431, + "SDE": 50.41120910644531, + "chi2_min": 16781.74609375 + }, + { + "injected": false, + "period": 111.1105728149414, + "T0": 106.63255052949808, + "duration": 0.23501992225646973, + "depth": 0.0012142080813646317, + "SDE": 10.223708152770996, + "chi2_min": 16825.001953125 + } + ] + }, + "old": { + "skipped": "ndata cap" + }, + "reference": { + "nlc": 1, + "total_s": 111.08370612398721, + "per_call_s": [ + 111.08370612398721 + ], + "ms_per_lc": 111083.70612398721, + "lc_per_s": 0.009002220351595397, + "extrapolated": true, + "note": "per-LC median over 1 LCs", + "n_injected": 1, + "n_recovered": 1, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 54.94634090944878, + "median_sde_noise": null, + "per_lc": [ + { + "injected": true, + "period": 21.70040209536575, + "T0": 6.508462894486594, + "duration": 0.23561813482228744, + "depth": 0.9957636265216906, + "SDE": 54.94634090944878, + "chi2_min": null + } + ] + } + } + }, + "kepler-4yr": { + "config": { + "ndata": 65440, + "baseline": 1363.0, + "cadence": 0.020833333333333332, + "noise": 0.0006, + "inject_period": 41.3, + "inject_depth": 0.003, + "period_min": 0.6, + "period_max": 500.0, + "nlc": 10 + }, + "nlc": 10, + "nperiods": 171688, + "impls": { + "new": { + "nlc": 10, + "total_s": 15.758871630998328, + "times_s": [ + 15.758871630998328 + ], + "ms_per_lc": 1575.8871630998328, + "lc_per_s": 0.6345631993301857, + "n_injected": 5, + "n_recovered": 5, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 99.27885437011719, + "median_sde_noise": 8.790546417236328, + "per_lc": [ + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003353152424097061, + "SDE": 99.46661376953125, + "chi2_min": 65888.8671875 + }, + { + "injected": false, + "period": 55.92778396606445, + "T0": 50.18960453722707, + "duration": 0.16932646930217743, + "depth": 0.0003129727265331894, + "SDE": 8.594135284423828, + "chi2_min": 65561.171875 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003339743707329035, + "SDE": 100.876953125, + "chi2_min": 66294.484375 + }, + { + "injected": false, + "period": 131.69247436523438, + "T0": 60.8605908603713, + "duration": 0.20403198897838593, + "depth": 0.000416730938013643, + "SDE": 8.790546417236328, + "chi2_min": 65907.7109375 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0033489209599792957, + "SDE": 98.255615234375, + "chi2_min": 66182.921875 + }, + { + "injected": false, + "period": 96.05792236328125, + "T0": 50.61724713786316, + "duration": 0.24719244241714478, + "depth": 0.000355874712113291, + "SDE": 11.384912490844727, + "chi2_min": 65324.4375 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003390833968296647, + "SDE": 98.79753112792969, + "chi2_min": 66280.8515625 + }, + { + "injected": false, + "period": 164.4737091064453, + "T0": 46.597844866974356, + "duration": 0.23087593913078308, + "depth": 0.0004219160182401538, + "SDE": 8.307422637939453, + "chi2_min": 65834.796875 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003344569355249405, + "SDE": 99.27885437011719, + "chi2_min": 66655.078125 + }, + { + "injected": false, + "period": 2.184530735015869, + "T0": 1.3664060570654044, + "duration": 0.10407035797834396, + "depth": 7.646237645531073e-05, + "SDE": 9.044288635253906, + "chi2_min": 65125.0859375 + } + ] + }, + "old": { + "skipped": "ndata cap" + }, + "reference": { + "skipped": "expected CPU runtime >~15 min; pass --ref-all to run" + } + } + } + }, + "env": { + "python": "3.11.10", + "numpy": "2.4.6", + "hostname": "b09d596f0cce", + "cpu_count": 64, + "cuvarbase": "1.0.0", + "pycuda": "2026.1", + "cuda_driver_version": 13000, + "gpu": "NVIDIA RTX A5000", + "compute_capability": "8.6", + "nvcc": "Cuda compilation tools, release 12.4, V12.4.131" + } +} \ No newline at end of file diff --git a/benchmarks/results/tls_survey_jul2026/tls_survey_a5000_final.json b/benchmarks/results/tls_survey_jul2026/tls_survey_a5000_final.json new file mode 100644 index 0000000..f663b32 --- /dev/null +++ b/benchmarks/results/tls_survey_jul2026/tls_survey_a5000_final.json @@ -0,0 +1,2269 @@ +{ + "script": "benchmark_tls_survey.py", + "timestamp": "2026-07-06T18:06:16", + "args": { + "regimes": "tess-ffi,k2,tess-2min,tess-yr,kepler-4yr", + "nlc": null, + "impls": "new", + "ref_nlc": 1, + "old_nlc": 5, + "n_iter": 1, + "output": "tls_survey_a5000_final.json", + "quick": false, + "ref_all": false + }, + "regimes": { + "tess-ffi": { + "config": { + "ndata": 1310, + "baseline": 27.4, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 100 + }, + "nlc": 100, + "nperiods": 2486, + "impls": { + "new": { + "nlc": 100, + "total_s": 0.12544582097325474, + "times_s": [ + 0.12544582097325474 + ], + "ms_per_lc": 1.2544582097325474, + "lc_per_s": 797.156885930223, + "n_injected": 50, + "n_recovered": 50, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.215956254814571, + "median_sde_noise": 5.443090003635092, + "per_lc": [ + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005532108247280121, + "SDE": 14.065390751689842, + "chi2_min": 1336.7557435313201 + }, + { + "injected": false, + "period": 0.6225233674049377, + "T0": 0.542300333010111, + "duration": 0.037806544452905655, + "depth": 0.0005694830324500799, + "SDE": 4.9503392833795115, + "chi2_min": 1315.3825465302093 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005046018864959478, + "SDE": 14.353723878887898, + "chi2_min": 1393.1030901098823 + }, + { + "injected": false, + "period": 4.131380081176758, + "T0": 1.2414492021177352, + "duration": 0.06124073639512062, + "depth": 0.00128852017223835, + "SDE": 5.341763840837246, + "chi2_min": 1313.3827822895487 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005532108247280121, + "SDE": 13.579242124218501, + "chi2_min": 1225.935215715939 + }, + { + "injected": false, + "period": 6.83004903793335, + "T0": 3.3232153407358425, + "duration": 0.15217697620391846, + "depth": 0.001029684324748814, + "SDE": 5.390536614360936, + "chi2_min": 1240.1715078378356 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005443227011710405, + "SDE": 14.371898343022762, + "chi2_min": 1269.001673041523 + }, + { + "injected": false, + "period": 1.3832758665084839, + "T0": 1.2062232874457806, + "duration": 0.0404709056019783, + "depth": 0.001005177036859095, + "SDE": 6.775107403625014, + "chi2_min": 1265.1381991709252 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00549427792429924, + "SDE": 14.17301640116733, + "chi2_min": 1249.6949432980077 + }, + { + "injected": false, + "period": 3.7197482585906982, + "T0": 1.5470102355227056, + "duration": 0.08362992107868195, + "depth": 0.0011062233243137598, + "SDE": 4.313070302235385, + "chi2_min": 1306.5296519060553 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00537601625546813, + "SDE": 13.259800657398372, + "chi2_min": 1383.9029934304917 + }, + { + "injected": false, + "period": 3.789888858795166, + "T0": 2.661259939658663, + "duration": 0.06252489984035492, + "depth": 0.001284703379496932, + "SDE": 5.880022989926391, + "chi2_min": 1278.525860381444 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005833111237734556, + "SDE": 14.21540329245873, + "chi2_min": 1307.3178590593716 + }, + { + "injected": false, + "period": 0.694443941116333, + "T0": 0.5409164779852347, + "duration": 0.043291110545396805, + "depth": 0.0007188625750131905, + "SDE": 5.5175769196799695, + "chi2_min": 1375.8015752078236 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0054616364650428295, + "SDE": 14.35625719464975, + "chi2_min": 1400.4625896043808 + }, + { + "injected": false, + "period": 0.9697667956352234, + "T0": 0.6234215032222714, + "duration": 0.04170956835150719, + "depth": 0.0007615210488438606, + "SDE": 5.514336562681269, + "chi2_min": 1345.5726786362516 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005138108506798744, + "SDE": 14.902654451537112, + "chi2_min": 1374.5640473015765 + }, + { + "injected": false, + "period": 8.122530937194824, + "T0": 7.832263005316975, + "duration": 0.08061356842517853, + "depth": 0.001534505863673985, + "SDE": 5.39251678972409, + "chi2_min": 1408.3131930415202 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005546618718653917, + "SDE": 14.285447421157015, + "chi2_min": 1320.0360381533574 + }, + { + "injected": false, + "period": 6.6303558349609375, + "T0": 1.8257501173602577, + "duration": 0.09650145471096039, + "depth": 0.0013269108021631837, + "SDE": 6.504767090686456, + "chi2_min": 1293.2735987886438 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0055886139161884785, + "SDE": 14.409292504387171, + "chi2_min": 1236.1293730040288 + }, + { + "injected": false, + "period": 3.265150547027588, + "T0": 1.6735373315951279, + "duration": 0.0594947412610054, + "depth": 0.001139817526564002, + "SDE": 4.939560202740602, + "chi2_min": 1306.6482895325398 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005358264781534672, + "SDE": 14.037121530449207, + "chi2_min": 1378.8564958646666 + }, + { + "injected": false, + "period": 0.9069516062736511, + "T0": 0.4873173068018559, + "duration": 0.04285912215709686, + "depth": 0.0006940899766050279, + "SDE": 7.2372310451470225, + "chi2_min": 1326.8709772525024 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005511269439011812, + "SDE": 14.500004799996965, + "chi2_min": 1315.768630715941 + }, + { + "injected": false, + "period": 6.818082332611084, + "T0": 0.19493036610213732, + "duration": 0.06887488067150116, + "depth": 0.001466379500925541, + "SDE": 4.695511531922886, + "chi2_min": 1332.3330495656523 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005003546830266714, + "SDE": 14.708295692721075, + "chi2_min": 1274.4610292007467 + }, + { + "injected": false, + "period": 1.3818519115447998, + "T0": 0.3538014247310741, + "duration": 0.040457021445035934, + "depth": 0.0008182328892871737, + "SDE": 6.284758092136365, + "chi2_min": 1271.7957420160972 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005669730249792337, + "SDE": 15.411972700141082, + "chi2_min": 1376.2414849254828 + }, + { + "injected": false, + "period": 9.743551254272461, + "T0": 5.753463031568231, + "duration": 0.09000206738710403, + "depth": 0.0015667054103687406, + "SDE": 5.385836408139612, + "chi2_min": 1213.2532298462943 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.18374329805374146, + "depth": 0.005207412876188755, + "SDE": 14.868149526782913, + "chi2_min": 1277.8260611819942 + }, + { + "injected": false, + "period": 3.751904010772705, + "T0": 3.4951949631796992, + "duration": 0.05644047260284424, + "depth": 0.0012609766563400626, + "SDE": 4.929817718229013, + "chi2_min": 1313.6198509818798 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.18374329805374146, + "depth": 0.004843113943934441, + "SDE": 14.53650301024676, + "chi2_min": 1386.7924769026886 + }, + { + "injected": false, + "period": 12.579136848449707, + "T0": 4.226824228778526, + "duration": 0.12552805244922638, + "depth": 0.0016740263672545552, + "SDE": 5.493663217546093, + "chi2_min": 1264.473948842068 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.004762181080877781, + "SDE": 14.148837291732566, + "chi2_min": 1356.2967423676214 + }, + { + "injected": false, + "period": 2.6725428104400635, + "T0": 0.14387011199786048, + "duration": 0.05565265938639641, + "depth": 0.0010715341195464134, + "SDE": 6.350914261360382, + "chi2_min": 1289.7367575326448 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005513254553079605, + "SDE": 14.373714189776125, + "chi2_min": 1438.3267171983498 + }, + { + "injected": false, + "period": 0.6743714809417725, + "T0": 0.47082369012325387, + "duration": 0.038828302174806595, + "depth": 0.0007056446629576385, + "SDE": 6.976825838284036, + "chi2_min": 1296.7100488048077 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005650545936077833, + "SDE": 15.005956551762928, + "chi2_min": 1335.367667329123 + }, + { + "injected": false, + "period": 3.817335605621338, + "T0": 0.3609841388095205, + "duration": 0.06267549097537994, + "depth": 0.0010340978624299169, + "SDE": 5.272298794654755, + "chi2_min": 1315.3716891212723 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005670913495123386, + "SDE": 14.08645401077787, + "chi2_min": 1247.6468554586909 + }, + { + "injected": false, + "period": 8.94373893737793, + "T0": 4.204453678020741, + "duration": 0.07539563626050949, + "depth": 0.0017777406610548496, + "SDE": 4.767811022728785, + "chi2_min": 1302.41173783788 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005472847260534763, + "SDE": 14.71144953376925, + "chi2_min": 1300.7378989330853 + }, + { + "injected": false, + "period": 1.083532452583313, + "T0": 0.43690821784262823, + "duration": 0.05275972932577133, + "depth": 0.0006725414423272014, + "SDE": 5.844557773042149, + "chi2_min": 1303.3270971047903 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005429901648312807, + "SDE": 13.581099288039903, + "chi2_min": 1338.4324806558413 + }, + { + "injected": false, + "period": 3.751904010772705, + "T0": 1.6210399439368501, + "duration": 0.05644047260284424, + "depth": 0.0013095358153805137, + "SDE": 4.520674549124327, + "chi2_min": 1327.7450767907026 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005096363835036755, + "SDE": 13.503372284160607, + "chi2_min": 1241.699730173737 + }, + { + "injected": false, + "period": 5.536378383636475, + "T0": 2.694534725723358, + "duration": 0.07094460725784302, + "depth": 0.0013462472707033157, + "SDE": 4.509943126680122, + "chi2_min": 1267.498920703772 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005140651483088732, + "SDE": 14.075288318406649, + "chi2_min": 1313.3261367914786 + }, + { + "injected": false, + "period": 9.36865520477295, + "T0": 9.22744536080586, + "duration": 0.1387048214673996, + "depth": 0.0016929280245676637, + "SDE": 6.098590264772476, + "chi2_min": 1253.9192065986254 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005635196343064308, + "SDE": 14.216509217170412, + "chi2_min": 1322.711499255337 + }, + { + "injected": false, + "period": 0.8091202974319458, + "T0": 0.4492196189062554, + "duration": 0.04125908017158508, + "depth": 0.0006857021362520754, + "SDE": 5.719165778385463, + "chi2_min": 1371.571016880274 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.18374329805374146, + "depth": 0.005087862256914377, + "SDE": 13.78431724494761, + "chi2_min": 1289.7056162390122 + }, + { + "injected": false, + "period": 0.8660217523574829, + "T0": 0.2383568104642464, + "duration": 0.06924357265233994, + "depth": 0.000580215360969305, + "SDE": 6.840602505257836, + "chi2_min": 1302.0461920367197 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005201755557209253, + "SDE": 14.966413194156804, + "chi2_min": 1310.6803436341113 + }, + { + "injected": false, + "period": 6.253728866577148, + "T0": 5.870805941826802, + "duration": 0.07388520240783691, + "depth": 0.0012998866150155663, + "SDE": 5.263553497728533, + "chi2_min": 1209.4973824857957 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005448559299111366, + "SDE": 13.623365245620368, + "chi2_min": 1390.4382307294914 + }, + { + "injected": false, + "period": 12.104578018188477, + "T0": 9.877395361733647, + "duration": 0.09207913279533386, + "depth": 0.001985683338716626, + "SDE": 5.286762580365337, + "chi2_min": 1289.7191295226003 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.005371253006160259, + "SDE": 14.089673848773986, + "chi2_min": 1326.8539697531978 + }, + { + "injected": false, + "period": 1.4328784942626953, + "T0": 1.2479081935049408, + "duration": 0.04094899445772171, + "depth": 0.0009649074636399746, + "SDE": 6.146771137919548, + "chi2_min": 1376.8769192147242 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005038459785282612, + "SDE": 14.131821442560385, + "chi2_min": 1418.7284940720592 + }, + { + "injected": false, + "period": 2.585336923599243, + "T0": 0.923334637585171, + "duration": 0.13419054448604584, + "depth": 0.0006379721453413367, + "SDE": 5.25585444090269, + "chi2_min": 1297.9203697652006 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0051691848784685135, + "SDE": 14.536860915078682, + "chi2_min": 1335.154218438021 + }, + { + "injected": false, + "period": 1.1863054037094116, + "T0": 0.5719344635827, + "duration": 0.04040246084332466, + "depth": 0.000764436786994338, + "SDE": 4.749610142414918, + "chi2_min": 1279.015192561413 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0054274569265544415, + "SDE": 13.665248005840278, + "chi2_min": 1318.2331768535635 + }, + { + "injected": false, + "period": 2.7281653881073, + "T0": 1.4686617796954948, + "duration": 0.06830879300832748, + "depth": 0.0009055134141817689, + "SDE": 5.017592787824173, + "chi2_min": 1256.61867619463 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005423026625066996, + "SDE": 13.10426825832789, + "chi2_min": 1284.7775721516596 + }, + { + "injected": false, + "period": 4.76552152633667, + "T0": 1.076287644484374, + "duration": 0.06748615950345993, + "depth": 0.001369902165606618, + "SDE": 5.838513866931736, + "chi2_min": 1268.924454853593 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005303625948727131, + "SDE": 13.768917576085652, + "chi2_min": 1469.3855329483195 + }, + { + "injected": false, + "period": 5.954711437225342, + "T0": 4.0025587667688, + "duration": 0.06583552062511444, + "depth": 0.0015804474242031574, + "SDE": 4.6201746689994785, + "chi2_min": 1338.3957556418723 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3166635296710325, + "duration": 0.17486760020256042, + "depth": 0.00547246215865016, + "SDE": 13.970445047605669, + "chi2_min": 1231.4136864085472 + }, + { + "injected": false, + "period": 0.7494922876358032, + "T0": 0.14383159056938233, + "duration": 0.06598714739084244, + "depth": 0.00045808343566022813, + "SDE": 5.95458977497551, + "chi2_min": 1183.9089290347831 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005119277164340019, + "SDE": 13.60063329091786, + "chi2_min": 1362.5470870930055 + }, + { + "injected": false, + "period": 2.8218910694122314, + "T0": 0.3113438265453894, + "duration": 0.07258859276771545, + "depth": 0.0010486793471500278, + "SDE": 6.400881085852153, + "chi2_min": 1301.5649681369214 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005257221404463053, + "SDE": 14.531584381902036, + "chi2_min": 1288.7046099807844 + }, + { + "injected": false, + "period": 7.533771991729736, + "T0": 2.4072671396336887, + "duration": 0.07120505720376968, + "depth": 0.0016296235844492912, + "SDE": 5.648053038946863, + "chi2_min": 1290.2735534736444 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005214043892920017, + "SDE": 13.50181630864038, + "chi2_min": 1333.4523123305858 + }, + { + "injected": false, + "period": 2.045438051223755, + "T0": 0.19033353368033445, + "duration": 0.04844745248556137, + "depth": 0.0009628356783650815, + "SDE": 5.736909765326355, + "chi2_min": 1390.8257634245467 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.302647882457407, + "duration": 0.19306959211826324, + "depth": 0.005054534412920475, + "SDE": 13.526842247909006, + "chi2_min": 1318.2392164019288 + }, + { + "injected": false, + "period": 9.047518730163574, + "T0": 4.986207971831902, + "duration": 0.12417609244585037, + "depth": 0.0012691774172708392, + "SDE": 5.13026950088749, + "chi2_min": 1229.019885531746 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.005109364632517099, + "SDE": 15.091513974060161, + "chi2_min": 1269.2879464986113 + }, + { + "injected": false, + "period": 4.433368682861328, + "T0": 3.8600783631616196, + "duration": 0.059669382870197296, + "depth": 0.0014127626782283187, + "SDE": 5.318909771098138, + "chi2_min": 1282.9852399707484 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00571216456592083, + "SDE": 13.68735145866488, + "chi2_min": 1391.5502979665098 + }, + { + "injected": false, + "period": 3.42227840423584, + "T0": 1.513876878042396, + "duration": 0.05473672226071358, + "depth": 0.0012891065562143922, + "SDE": 5.639916325583477, + "chi2_min": 1319.0337806769976 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005534767638891935, + "SDE": 14.290255137567247, + "chi2_min": 1310.234607494101 + }, + { + "injected": false, + "period": 13.635416984558105, + "T0": 4.660816806064474, + "duration": 0.1823594868183136, + "depth": 0.001385409850627184, + "SDE": 5.697635637815957, + "chi2_min": 1203.2160460078271 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005357676651328802, + "SDE": 13.642723230186835, + "chi2_min": 1339.592726568851 + }, + { + "injected": false, + "period": 12.15611457824707, + "T0": 4.847994174497785, + "duration": 0.15128548443317413, + "depth": 0.0013391552492976189, + "SDE": 5.115254037085747, + "chi2_min": 1278.6733893042583 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0052889143116772175, + "SDE": 14.590804903503678, + "chi2_min": 1395.672670674071 + }, + { + "injected": false, + "period": 9.978133201599121, + "T0": 3.989828930425375, + "duration": 0.23240043222904205, + "depth": 0.001154838944785297, + "SDE": 6.614594199597157, + "chi2_min": 1311.4876419213956 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0054702446796, + "SDE": 13.901584858470857, + "chi2_min": 1303.8109990226483 + }, + { + "injected": false, + "period": 1.227888584136963, + "T0": 0.24339220282239182, + "duration": 0.06381379067897797, + "depth": 0.0007502195076085627, + "SDE": 6.699389113051831, + "chi2_min": 1254.5677523085624 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0049850489012897015, + "SDE": 14.54917558185404, + "chi2_min": 1355.400272127356 + }, + { + "injected": false, + "period": 2.2177605628967285, + "T0": 1.0043958503840145, + "duration": 0.06375162303447723, + "depth": 0.0008952956413850188, + "SDE": 5.1197930289375355, + "chi2_min": 1339.4699305699805 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.004980760160833597, + "SDE": 14.525513153103892, + "chi2_min": 1379.7126956069637 + }, + { + "injected": false, + "period": 3.958618640899658, + "T0": 1.5509364717303242, + "duration": 0.0574585385620594, + "depth": 0.0012683860259130597, + "SDE": 4.945356773245486, + "chi2_min": 1282.2177474202194 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005414358805865049, + "SDE": 15.56512061302323, + "chi2_min": 1338.7585082855128 + }, + { + "injected": false, + "period": 12.286243438720703, + "T0": 0.6104409236496622, + "duration": 0.08381339907646179, + "depth": 0.0018242186633870006, + "SDE": 6.411775274553496, + "chi2_min": 1332.7316477155935 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005437161773443222, + "SDE": 15.014119441824953, + "chi2_min": 1358.6656671348069 + }, + { + "injected": false, + "period": 6.318080902099609, + "T0": 0.0469745786837219, + "duration": 0.06714829802513123, + "depth": 0.0013039918849244714, + "SDE": 4.801563086659648, + "chi2_min": 1247.8763909035313 + } + ] + } + } + }, + "k2": { + "config": { + "ndata": 4320, + "baseline": 90.0, + "cadence": 0.020833333333333332, + "noise": 0.0008, + "inject_period": 12.4, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 45.0, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 9672, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.1552676890278235, + "times_s": [ + 0.1552676890278235 + ], + "ms_per_lc": 3.10535378055647, + "lc_per_s": 322.0245004808447, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 25.520731091469347, + "median_sde_noise": 6.570467023129993, + "per_lc": [ + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004404039587825537, + "SDE": 26.177501702735942, + "chi2_min": 4387.543069049461 + }, + { + "injected": false, + "period": 1.3685027360916138, + "T0": 0.9995278422767413, + "duration": 0.044523872435092926, + "depth": 0.0003865754115395248, + "SDE": 6.356819437068086, + "chi2_min": 4326.839595866841 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21533998847007751, + "depth": 0.004162405151873827, + "SDE": 25.623561478480863, + "chi2_min": 4456.472117348283 + }, + { + "injected": false, + "period": 1.567154884338379, + "T0": 0.6710433548500703, + "duration": 0.046581678092479706, + "depth": 0.0004660347185563296, + "SDE": 9.811690226985908, + "chi2_min": 4249.244214398832 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004389334935694933, + "SDE": 25.122103186161528, + "chi2_min": 4387.086710296348 + }, + { + "injected": false, + "period": 17.946809768676758, + "T0": 11.771186328499653, + "duration": 0.19984866678714752, + "depth": 0.000686309882439673, + "SDE": 6.695235055383752, + "chi2_min": 4314.305852264322 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004312425851821899, + "SDE": 25.631966539805752, + "chi2_min": 4341.908664728141 + }, + { + "injected": false, + "period": 7.888498306274414, + "T0": 7.531212754637977, + "duration": 0.08388378471136093, + "depth": 0.0007113477913662791, + "SDE": 6.570467023129993, + "chi2_min": 4085.738149098137 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.204982191324234, + "depth": 0.004326025955379009, + "SDE": 26.146999811672558, + "chi2_min": 4431.564516407139 + }, + { + "injected": false, + "period": 1.7248963117599487, + "T0": 0.09257843054204251, + "duration": 0.14293743669986725, + "depth": 0.00026448414428159595, + "SDE": 8.812640097544833, + "chi2_min": 4256.353800798309 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.0044168573804199696, + "SDE": 24.497821119953528, + "chi2_min": 4472.887610904101 + }, + { + "injected": false, + "period": 40.31962203979492, + "T0": 12.79962811146072, + "duration": 0.1850813776254654, + "depth": 0.001074486062861979, + "SDE": 5.672305475883705, + "chi2_min": 4326.760088810315 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004235788248479366, + "SDE": 24.954817970912853, + "chi2_min": 4373.691853085323 + }, + { + "injected": false, + "period": 13.618805885314941, + "T0": 12.353813122829024, + "duration": 0.11110421270132065, + "depth": 0.000906570116057992, + "SDE": 6.260135704065025, + "chi2_min": 4297.932157246619 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004246351309120655, + "SDE": 24.37666636200077, + "chi2_min": 4262.907247088052 + }, + { + "injected": false, + "period": 8.089868545532227, + "T0": 4.885035835718213, + "duration": 0.0888851210474968, + "depth": 0.0007501470972783864, + "SDE": 6.336239682418151, + "chi2_min": 4085.4030426856834 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.00426411023363471, + "SDE": 24.126784966742193, + "chi2_min": 4317.331950603972 + }, + { + "injected": false, + "period": 15.671825408935547, + "T0": 11.628149511437869, + "duration": 0.363585501909256, + "depth": 0.0004864199145231396, + "SDE": 5.5538323507584915, + "chi2_min": 4251.70786210115 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21533998847007751, + "depth": 0.004226861987262964, + "SDE": 25.562998807201637, + "chi2_min": 4253.178250383264 + }, + { + "injected": false, + "period": 2.754897117614746, + "T0": 2.5983700068006783, + "duration": 0.08354046940803528, + "depth": 0.0003899047151207924, + "SDE": 6.612445721876433, + "chi2_min": 4304.2806226586845 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004189441911876202, + "SDE": 25.342519823222744, + "chi2_min": 4441.637445074186 + }, + { + "injected": false, + "period": 7.901713848114014, + "T0": 5.554735825406851, + "duration": 0.10750557482242584, + "depth": 0.0006463858881033957, + "SDE": 6.610492881251317, + "chi2_min": 4282.332309697514 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004312783945351839, + "SDE": 25.520731091469347, + "chi2_min": 4257.315695769827 + }, + { + "injected": false, + "period": 2.0137128829956055, + "T0": 1.3472117866492113, + "duration": 0.05591322481632233, + "depth": 0.000497570086736232, + "SDE": 10.587910496607027, + "chi2_min": 4280.768136245288 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7128754423422947, + "duration": 0.2263185828924179, + "depth": 0.004149800632148981, + "SDE": 25.510778485710183, + "chi2_min": 4445.6471137767185 + }, + { + "injected": false, + "period": 14.453306198120117, + "T0": 7.792941111104028, + "duration": 0.23816171288490295, + "depth": 0.0005341128562577069, + "SDE": 5.291739440555412, + "chi2_min": 4100.002558732529 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0041998703964054585, + "SDE": 26.34910531242964, + "chi2_min": 4507.700801853651 + }, + { + "injected": false, + "period": 10.265218734741211, + "T0": 3.1706764167317942, + "duration": 0.09158045053482056, + "depth": 0.0007960263756103814, + "SDE": 8.393836683396165, + "chi2_min": 4252.540272747918 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004519070964306593, + "SDE": 24.893549086341817, + "chi2_min": 4459.222228646795 + }, + { + "injected": false, + "period": 30.56487464904785, + "T0": 7.08230056832997, + "duration": 0.12538689374923706, + "depth": 0.0011783387744799256, + "SDE": 6.166500977316034, + "chi2_min": 4209.062413090763 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004347286187112331, + "SDE": 25.645459284219083, + "chi2_min": 4367.650080260724 + }, + { + "injected": false, + "period": 36.307926177978516, + "T0": 19.91623931560639, + "duration": 0.29323291778564453, + "depth": 0.0009722203249111772, + "SDE": 5.961172666064783, + "chi2_min": 4425.980193094114 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0043581826612353325, + "SDE": 25.620072108700576, + "chi2_min": 4418.55047630803 + }, + { + "injected": false, + "period": 3.8394787311553955, + "T0": 3.5170847570018395, + "duration": 0.08451777696609497, + "depth": 0.0004938149941153824, + "SDE": 7.286316523814045, + "chi2_min": 4251.628013641141 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7056692181227504, + "duration": 0.2263185828924179, + "depth": 0.003965914715081453, + "SDE": 26.83157990977236, + "chi2_min": 4355.02648468021 + }, + { + "injected": false, + "period": 1.1502342224121094, + "T0": 0.1958829591745257, + "duration": 0.04874681308865547, + "depth": 0.0003573001886252314, + "SDE": 6.8037354352473, + "chi2_min": 4534.605633556768 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004336000885814428, + "SDE": 26.782616306883728, + "chi2_min": 4509.665909110085 + }, + { + "injected": false, + "period": 5.259787082672119, + "T0": 1.6857420942828583, + "duration": 0.08501768857240677, + "depth": 0.0005589709035120904, + "SDE": 6.523270780656528, + "chi2_min": 4316.4516532573625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0044480483047664165, + "SDE": 25.552776261980224, + "chi2_min": 4414.776178403963 + }, + { + "injected": false, + "period": 14.582316398620605, + "T0": 8.464501963214104, + "duration": 0.08873925358057022, + "depth": 0.001034076907671988, + "SDE": 6.749566604524634, + "chi2_min": 4359.509288619014 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004289723001420498, + "SDE": 25.876474909413623, + "chi2_min": 4506.705052521887 + }, + { + "injected": false, + "period": 12.45097541809082, + "T0": 9.849355446693153, + "duration": 0.13812200725078583, + "depth": 0.0008029531454667449, + "SDE": 7.419173008465264, + "chi2_min": 4300.465232424514 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.7385776951641674, + "duration": 0.21533998847007751, + "depth": 0.004041068255901337, + "SDE": 25.209039951602133, + "chi2_min": 4325.576950239149 + }, + { + "injected": false, + "period": 2.9074718952178955, + "T0": 1.8813054051288134, + "duration": 0.07331513613462448, + "depth": 0.00043522383202798665, + "SDE": 5.477008302567191, + "chi2_min": 4107.6394164915755 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004308898001909256, + "SDE": 25.054476910642528, + "chi2_min": 4555.486684237196 + }, + { + "injected": false, + "period": 3.8546807765960693, + "T0": 3.842780059564987, + "duration": 0.20632818341255188, + "depth": 0.0003478149010334164, + "SDE": 5.267234453913335, + "chi2_min": 4368.599291771086 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21533998847007751, + "depth": 0.004108169581741095, + "SDE": 24.629152402589423, + "chi2_min": 4430.720853986382 + }, + { + "injected": false, + "period": 5.158578872680664, + "T0": 1.2311123437995661, + "duration": 0.10296858102083206, + "depth": 0.0004856034356635064, + "SDE": 7.478173387504096, + "chi2_min": 4369.114303804741 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.204982191324234, + "depth": 0.004322155378758907, + "SDE": 25.02897262869357, + "chi2_min": 4547.568737473479 + }, + { + "injected": false, + "period": 1.5529592037200928, + "T0": 1.0412210721013935, + "duration": 0.09288113564252853, + "depth": 0.00030092347878962755, + "SDE": 5.96437632528338, + "chi2_min": 4323.484146598777 + } + ] + } + } + }, + "tess-2min": { + "config": { + "ndata": 19710, + "baseline": 27.4, + "cadence": 0.001388888888888889, + "noise": 0.002, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 2497, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.1409610400442034, + "times_s": [ + 0.1409610400442034 + ], + "ms_per_lc": 2.819220800884068, + "lc_per_s": 354.70793904699275, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.745711407566798, + "median_sde_noise": 5.684664617948238, + "per_lc": [ + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005485764238983393, + "SDE": 14.639897918026437, + "chi2_min": 19986.645856211093 + }, + { + "injected": false, + "period": 13.100994110107422, + "T0": 5.437705039673006, + "duration": 0.10437987744808197, + "depth": 0.00097375811310485, + "SDE": 6.51029884384331, + "chi2_min": 19991.359227174664 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005453883670270443, + "SDE": 14.01951975362099, + "chi2_min": 20026.577512524516 + }, + { + "injected": false, + "period": 0.727395236492157, + "T0": 0.3019376498355886, + "duration": 0.0418415404856205, + "depth": 0.0003676938358694315, + "SDE": 5.684664617948238, + "chi2_min": 19321.845612071113 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.00540515873581171, + "SDE": 14.726376767600666, + "chi2_min": 19669.763819709846 + }, + { + "injected": false, + "period": 2.640021324157715, + "T0": 0.4917686880570642, + "duration": 0.07459784299135208, + "depth": 0.0004884963855147362, + "SDE": 6.6571320597978705, + "chi2_min": 19593.49303085074 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005405776668339968, + "SDE": 14.777059905804203, + "chi2_min": 19828.85640417305 + }, + { + "injected": false, + "period": 9.231582641601562, + "T0": 1.668764258751935, + "duration": 0.07619598507881165, + "depth": 0.0008226633653976023, + "SDE": 4.722181891853178, + "chi2_min": 20048.86317008199 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005362111143767834, + "SDE": 14.354143808352125, + "chi2_min": 19451.406444908673 + }, + { + "injected": false, + "period": 1.5595471858978271, + "T0": 0.281584903400649, + "duration": 0.06259265542030334, + "depth": 0.00037873227847740054, + "SDE": 5.943673964398746, + "chi2_min": 19451.819498490713 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005432446021586657, + "SDE": 14.968046777015651, + "chi2_min": 19835.65647686846 + }, + { + "injected": false, + "period": 3.7597527503967285, + "T0": 3.551468894538374, + "duration": 0.09266479313373566, + "depth": 0.0005465572467073798, + "SDE": 4.939380282163157, + "chi2_min": 19562.21720545303 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005425633862614632, + "SDE": 14.533890807013856, + "chi2_min": 19661.441555852696 + }, + { + "injected": false, + "period": 5.938205242156982, + "T0": 1.8585079530804478, + "duration": 0.0977407768368721, + "depth": 0.0006803313735872507, + "SDE": 5.2725429446240595, + "chi2_min": 19645.717433759728 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005327630788087845, + "SDE": 14.84010498188191, + "chi2_min": 19854.839841139597 + }, + { + "injected": false, + "period": 0.6239511370658875, + "T0": 0.1721244509631692, + "duration": 0.03426845371723175, + "depth": 0.0003540570323821157, + "SDE": 6.394813243780807, + "chi2_min": 19814.719961456234 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005372271407395601, + "SDE": 14.723770552185087, + "chi2_min": 19759.874102802845 + }, + { + "injected": false, + "period": 1.7957980632781982, + "T0": 0.0708701585838174, + "duration": 0.048745013773441315, + "depth": 0.00045976031105965376, + "SDE": 6.211765916081824, + "chi2_min": 19886.988311534038 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005604728125035763, + "SDE": 14.90068399024373, + "chi2_min": 19847.307987816148 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 0.7042438654324634, + "duration": 0.04801042377948761, + "depth": 0.0004377198056317866, + "SDE": 4.988946210902911, + "chi2_min": 19686.281949983317 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005634222645312548, + "SDE": 15.213471098874043, + "chi2_min": 20082.62622246992 + }, + { + "injected": false, + "period": 1.944218635559082, + "T0": 1.0748527127935859, + "duration": 0.04533376544713974, + "depth": 0.0005116008687764406, + "SDE": 5.995294257451506, + "chi2_min": 19728.37795666568 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005626666825264692, + "SDE": 14.850588900732845, + "chi2_min": 19887.365185557555 + }, + { + "injected": false, + "period": 5.338284969329834, + "T0": 1.2329951883274148, + "duration": 0.2083013355731964, + "depth": 0.0004553081525955349, + "SDE": 5.2985176588408684, + "chi2_min": 19457.346986131895 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005390736740082502, + "SDE": 14.700392737248483, + "chi2_min": 19622.466311638673 + }, + { + "injected": false, + "period": 1.9001942873001099, + "T0": 1.6714522132299692, + "duration": 0.16299022734165192, + "depth": 0.0003048948128707707, + "SDE": 6.331010289372725, + "chi2_min": 19683.71679549537 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0052563706412911415, + "SDE": 14.6409129125974, + "chi2_min": 19651.33208119841 + }, + { + "injected": false, + "period": 2.132042646408081, + "T0": 1.8807319745508266, + "duration": 0.051615022122859955, + "depth": 0.0005292857531458139, + "SDE": 5.194728043439782, + "chi2_min": 19577.79985606737 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005402364302426577, + "SDE": 14.456168855601687, + "chi2_min": 19436.729586657333 + }, + { + "injected": false, + "period": 2.430079221725464, + "T0": 2.2736688394607825, + "duration": 0.11330521106719971, + "depth": 0.0004198209207970649, + "SDE": 6.671596328367839, + "chi2_min": 19901.711850304608 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005152017343789339, + "SDE": 14.03824231166757, + "chi2_min": 20221.619944051825 + }, + { + "injected": false, + "period": 9.231582641601562, + "T0": 3.019791075150806, + "duration": 0.07619598507881165, + "depth": 0.0008931891061365604, + "SDE": 5.574140967715965, + "chi2_min": 19451.45299624408 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005317769479006529, + "SDE": 15.08019479384714, + "chi2_min": 19643.599153703726 + }, + { + "injected": false, + "period": 8.733040809631348, + "T0": 1.2241375973440398, + "duration": 0.07479896396398544, + "depth": 0.0009044561302289367, + "SDE": 5.23786999611775, + "chi2_min": 19883.518882539494 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005483759101480246, + "SDE": 14.86514059054868, + "chi2_min": 19639.472178093947 + }, + { + "injected": false, + "period": 1.0538432598114014, + "T0": 0.844823486495514, + "duration": 0.05492657795548439, + "depth": 0.0003412514925003052, + "SDE": 7.413836817207922, + "chi2_min": 19181.17701679314 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005476575344800949, + "SDE": 14.959634171354576, + "chi2_min": 19795.391763729953 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 1.437746418930118, + "duration": 0.07496435195207596, + "depth": 0.00039721315260976553, + "SDE": 7.340803590611788, + "chi2_min": 19453.765267702773 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0053500221110880375, + "SDE": 14.900270608006013, + "chi2_min": 20215.64466906669 + }, + { + "injected": false, + "period": 9.213768005371094, + "T0": 3.8128516220795063, + "duration": 0.07614698261022568, + "depth": 0.0008834492764435709, + "SDE": 5.369271320042039, + "chi2_min": 19582.097059867356 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005436438601464033, + "SDE": 15.008995156381834, + "chi2_min": 19810.99344150698 + }, + { + "injected": false, + "period": 1.186469316482544, + "T0": 0.4394331011325221, + "duration": 0.06308804452419281, + "depth": 0.0003383326402399689, + "SDE": 4.907890099364573, + "chi2_min": 19782.61215164576 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005494052078574896, + "SDE": 14.948855903371246, + "chi2_min": 19692.064174238843 + }, + { + "injected": false, + "period": 1.7699131965637207, + "T0": 1.468044076797554, + "duration": 0.04850965365767479, + "depth": 0.0005157210398465395, + "SDE": 6.399392674323263, + "chi2_min": 19890.123616665805 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005343742202967405, + "SDE": 14.745711407566798, + "chi2_min": 19796.72417997518 + }, + { + "injected": false, + "period": 1.6712085008621216, + "T0": 0.23874408222442334, + "duration": 0.05521129071712494, + "depth": 0.00045216272701509297, + "SDE": 6.500801607297511, + "chi2_min": 19370.162059362396 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005551040638238192, + "SDE": 14.71396727997725, + "chi2_min": 19685.802620606155 + }, + { + "injected": false, + "period": 0.9706023335456848, + "T0": 0.9548758508475785, + "duration": 0.07941237837076187, + "depth": 0.000291008735075593, + "SDE": 5.375327682145727, + "chi2_min": 20024.64889769606 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005364943295717239, + "SDE": 14.324658514335809, + "chi2_min": 19449.195780767288 + }, + { + "injected": false, + "period": 3.3878562450408936, + "T0": 2.2401032092012088, + "duration": 0.06023088097572327, + "depth": 0.0006338467355817556, + "SDE": 5.240741638867381, + "chi2_min": 19773.893695076826 + } + ] + } + } + }, + "tess-yr": { + "config": { + "ndata": 16850, + "baseline": 351.0, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 21.7, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 175.0, + "nlc": 20 + }, + "nlc": 20, + "nperiods": 42001, + "impls": { + "new": { + "nlc": 20, + "total_s": 0.3685928500490263, + "times_s": [ + 0.3685928500490263 + ], + "ms_per_lc": 18.429642502451316, + "lc_per_s": 54.260412260682244, + "n_injected": 10, + "n_recovered": 10, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 52.45753681269514, + "median_sde_noise": 8.462352256235231, + "per_lc": [ + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.00428521353751421, + "SDE": 51.74706350296344, + "chi2_min": 17045.304161600427 + }, + { + "injected": false, + "period": 71.1637191772461, + "T0": 34.1104773348261, + "duration": 0.19279845058918, + "depth": 0.0008943733409978449, + "SDE": 8.028487044071968, + "chi2_min": 16855.451608264993 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004315289203077555, + "SDE": 53.5792260804763, + "chi2_min": 17079.369669986347 + }, + { + "injected": false, + "period": 3.215691566467285, + "T0": 2.7022411507869606, + "duration": 0.05919283628463745, + "depth": 0.00034801653237082064, + "SDE": 6.9967664700766345, + "chi2_min": 16563.562434798696 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004378140438348055, + "SDE": 51.41700284707218, + "chi2_min": 16690.807726532905 + }, + { + "injected": false, + "period": 1.5765894651412964, + "T0": 1.1244638781501877, + "duration": 0.046674944460392, + "depth": 0.00028764992021024227, + "SDE": 9.104482354902814, + "chi2_min": 16943.34078928957 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004271374084055424, + "SDE": 53.34992640941852, + "chi2_min": 16960.625747997896 + }, + { + "injected": false, + "period": 13.20435905456543, + "T0": 1.339143794434733, + "duration": 0.2093072384595871, + "depth": 0.00039136491250246763, + "SDE": 8.470054948457452, + "chi2_min": 16618.206479002816 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004417836666107178, + "SDE": 52.79030624710437, + "chi2_min": 17073.47447652764 + }, + { + "injected": false, + "period": 1.0810301303863525, + "T0": 0.8323784887314218, + "duration": 0.03727799281477928, + "depth": 0.00030446742312051356, + "SDE": 12.367130312287468, + "chi2_min": 16803.589990291133 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.0041633895598351955, + "SDE": 51.30555986400533, + "chi2_min": 17136.81803806736 + }, + { + "injected": false, + "period": 35.858131408691406, + "T0": 3.57559711400188, + "duration": 0.30683863162994385, + "depth": 0.0005235545104369521, + "SDE": 7.264360808610101, + "chi2_min": 16644.461869314277 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.0043487101793289185, + "SDE": 53.18143500109468, + "chi2_min": 17383.538593224726 + }, + { + "injected": false, + "period": 1.4476821422576904, + "T0": 1.3089501248304032, + "duration": 0.04536651819944382, + "depth": 0.00027384364511817694, + "SDE": 8.454649564013012, + "chi2_min": 17168.875156750462 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.0043377201072871685, + "SDE": 52.12476737828592, + "chi2_min": 17070.551816905958 + }, + { + "injected": false, + "period": 158.73745727539062, + "T0": 94.37186024718721, + "duration": 0.3933356702327728, + "depth": 0.0010018610628321767, + "SDE": 7.243525177058378, + "chi2_min": 16733.71459318848 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004398010671138763, + "SDE": 52.85238242983881, + "chi2_min": 17030.781448475514 + }, + { + "injected": false, + "period": 143.65647888183594, + "T0": 105.68818444369845, + "duration": 0.23189450800418854, + "depth": 0.0014297961024567485, + "SDE": 9.032557767575465, + "chi2_min": 16928.81373207052 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004301433917135, + "SDE": 50.820057611926856, + "chi2_min": 16781.757778621162 + }, + { + "injected": false, + "period": 1.0684576034545898, + "T0": 0.9389372063940868, + "duration": 0.04099807143211365, + "depth": 0.000271445867838338, + "SDE": 9.54321175516312, + "chi2_min": 16826.68702423067 + } + ] + } + } + }, + "kepler-4yr": { + "config": { + "ndata": 65440, + "baseline": 1363.0, + "cadence": 0.020833333333333332, + "noise": 0.0006, + "inject_period": 41.3, + "inject_depth": 0.003, + "period_min": 0.6, + "period_max": 500.0, + "nlc": 10 + }, + "nlc": 10, + "nperiods": 171688, + "impls": { + "new": { + "nlc": 10, + "total_s": 1.6760276049608365, + "times_s": [ + 1.6760276049608365 + ], + "ms_per_lc": 167.60276049608365, + "lc_per_s": 5.966488839683323, + "n_injected": 5, + "n_recovered": 5, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 98.29821370846379, + "median_sde_noise": 8.722158471115197, + "per_lc": [ + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003353167325258255, + "SDE": 98.45695586534704, + "chi2_min": 65888.8686123984 + }, + { + "injected": false, + "period": 55.92778396606445, + "T0": 50.19175134709553, + "duration": 0.16932636499404907, + "depth": 0.0003138199681416154, + "SDE": 8.722158471115197, + "chi2_min": 65560.97205817862 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003339726710692048, + "SDE": 100.12524704417672, + "chi2_min": 66294.66735087954 + }, + { + "injected": false, + "period": 131.69247436523438, + "T0": 60.8605908603713, + "duration": 0.20403198897838593, + "depth": 0.0004167061997577548, + "SDE": 8.661975684684567, + "chi2_min": 65907.71342974457 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0033489372581243515, + "SDE": 97.03154092729665, + "chi2_min": 66182.91527186599 + }, + { + "injected": false, + "period": 96.05792236328125, + "T0": 50.60741073171448, + "duration": 0.2130729854106903, + "depth": 0.00037875358248129487, + "SDE": 11.123223491414974, + "chi2_min": 65325.414736351566 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003390850266441703, + "SDE": 98.0194282869755, + "chi2_min": 66280.838278687 + }, + { + "injected": false, + "period": 164.4737091064453, + "T0": 46.597844866974356, + "duration": 0.23087593913078308, + "depth": 0.0004218894464429468, + "SDE": 7.961574440597722, + "chi2_min": 65834.8010161592 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0033445751760154963, + "SDE": 98.29821370846379, + "chi2_min": 66655.12926137832 + }, + { + "injected": false, + "period": 2.184530735015869, + "T0": 1.3664060570654044, + "duration": 0.10407035797834396, + "depth": 7.64523065299727e-05, + "SDE": 9.621362334057467, + "chi2_min": 65125.09877587645 + } + ] + } + } + } + }, + "env": { + "python": "3.11.10", + "numpy": "2.4.6", + "hostname": "b09d596f0cce", + "cpu_count": 64, + "cuvarbase": "1.0.0", + "pycuda": "2026.1", + "cuda_driver_version": 13000, + "gpu": "NVIDIA RTX A5000", + "compute_capability": "8.6", + "nvcc": "Cuda compilation tools, release 12.4, V12.4.131" + } +} \ No newline at end of file diff --git a/benchmarks/results/tls_survey_jul2026/tls_survey_rtx4000ada.json b/benchmarks/results/tls_survey_jul2026/tls_survey_rtx4000ada.json new file mode 100644 index 0000000..60f9aca --- /dev/null +++ b/benchmarks/results/tls_survey_jul2026/tls_survey_rtx4000ada.json @@ -0,0 +1,2269 @@ +{ + "script": "benchmark_tls_survey.py", + "timestamp": "2026-07-06T18:12:17", + "args": { + "regimes": "tess-ffi,k2,tess-2min,tess-yr,kepler-4yr", + "nlc": null, + "impls": "new", + "ref_nlc": 1, + "old_nlc": 5, + "n_iter": 1, + "output": "tls_survey_rtx4000ada.json", + "quick": false, + "ref_all": false + }, + "regimes": { + "tess-ffi": { + "config": { + "ndata": 1310, + "baseline": 27.4, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 100 + }, + "nlc": 100, + "nperiods": 2486, + "impls": { + "new": { + "nlc": 100, + "total_s": 0.30543674528598785, + "times_s": [ + 0.30543674528598785 + ], + "ms_per_lc": 3.0543674528598785, + "lc_per_s": 327.40003140868845, + "n_injected": 50, + "n_recovered": 50, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.2159561918949, + "median_sde_noise": 5.443089791132511, + "per_lc": [ + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005532108247280121, + "SDE": 14.065390650435956, + "chi2_min": 1336.7557435313201 + }, + { + "injected": false, + "period": 0.6225233674049377, + "T0": 0.542300333010111, + "duration": 0.037806544452905655, + "depth": 0.0005694830324500799, + "SDE": 4.950339285343851, + "chi2_min": 1315.3825465302093 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005046018864959478, + "SDE": 14.353723862800633, + "chi2_min": 1393.1030901098823 + }, + { + "injected": false, + "period": 4.131380081176758, + "T0": 1.2414492021177352, + "duration": 0.06124073639512062, + "depth": 0.00128852017223835, + "SDE": 5.341763809516535, + "chi2_min": 1313.3827822895487 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005532108247280121, + "SDE": 13.579242113724062, + "chi2_min": 1225.935215715939 + }, + { + "injected": false, + "period": 6.83004903793335, + "T0": 3.3232153407358425, + "duration": 0.15217697620391846, + "depth": 0.001029684324748814, + "SDE": 5.390537626148937, + "chi2_min": 1240.1715078378356 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005443227011710405, + "SDE": 14.371898325267232, + "chi2_min": 1269.001673041523 + }, + { + "injected": false, + "period": 1.3832758665084839, + "T0": 1.2062232874457806, + "duration": 0.0404709056019783, + "depth": 0.001005177036859095, + "SDE": 6.775107320253302, + "chi2_min": 1265.1381991709252 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00549427792429924, + "SDE": 14.173016542042589, + "chi2_min": 1249.6949432980077 + }, + { + "injected": false, + "period": 3.7197482585906982, + "T0": 1.5470102355227056, + "duration": 0.08362992107868195, + "depth": 0.0011062233243137598, + "SDE": 4.313070342273851, + "chi2_min": 1306.5296519060553 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00537601625546813, + "SDE": 13.259800800819253, + "chi2_min": 1383.9029934304917 + }, + { + "injected": false, + "period": 3.789888858795166, + "T0": 2.661259939658663, + "duration": 0.06252489984035492, + "depth": 0.001284703379496932, + "SDE": 5.880023046069064, + "chi2_min": 1278.525860381444 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005833111237734556, + "SDE": 14.215403265364708, + "chi2_min": 1307.3178590593716 + }, + { + "injected": false, + "period": 0.694443941116333, + "T0": 0.5409164779852347, + "duration": 0.043291110545396805, + "depth": 0.0007188625750131905, + "SDE": 5.517576909494449, + "chi2_min": 1375.8015752078236 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0054616364650428295, + "SDE": 14.356257153174388, + "chi2_min": 1400.4625896043808 + }, + { + "injected": false, + "period": 0.9697667956352234, + "T0": 0.6234215032222714, + "duration": 0.04170956835150719, + "depth": 0.0007615210488438606, + "SDE": 5.514336641376309, + "chi2_min": 1345.5726786362516 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005138108506798744, + "SDE": 14.9026541561844, + "chi2_min": 1374.5640473015765 + }, + { + "injected": false, + "period": 8.122530937194824, + "T0": 7.832263005316975, + "duration": 0.08061356842517853, + "depth": 0.001534505863673985, + "SDE": 5.392517416890103, + "chi2_min": 1408.3131930415202 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005546618718653917, + "SDE": 14.285447453898277, + "chi2_min": 1320.0360381533574 + }, + { + "injected": false, + "period": 6.6303558349609375, + "T0": 1.8257501173602577, + "duration": 0.09650145471096039, + "depth": 0.0013269108021631837, + "SDE": 6.504767079255062, + "chi2_min": 1293.2735987886438 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0055886139161884785, + "SDE": 14.409292548217513, + "chi2_min": 1236.1293730040288 + }, + { + "injected": false, + "period": 3.265150547027588, + "T0": 1.6735373315951279, + "duration": 0.0594947412610054, + "depth": 0.001139817526564002, + "SDE": 4.939560189166223, + "chi2_min": 1306.6482895325398 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005358264781534672, + "SDE": 14.037121524171635, + "chi2_min": 1378.8564958646666 + }, + { + "injected": false, + "period": 0.9069516062736511, + "T0": 0.4873173068018559, + "duration": 0.04285912215709686, + "depth": 0.0006940899766050279, + "SDE": 7.237230693107558, + "chi2_min": 1326.8709772525024 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005511269439011812, + "SDE": 14.500004810570525, + "chi2_min": 1315.768630715941 + }, + { + "injected": false, + "period": 6.818082332611084, + "T0": 0.19493036610213732, + "duration": 0.06887488067150116, + "depth": 0.001466379500925541, + "SDE": 4.695511534603615, + "chi2_min": 1332.3330495656523 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005003546830266714, + "SDE": 14.708295639901666, + "chi2_min": 1274.4610292007467 + }, + { + "injected": false, + "period": 1.3818519115447998, + "T0": 0.3538014247310741, + "duration": 0.040457021445035934, + "depth": 0.0008182328892871737, + "SDE": 6.284759724974738, + "chi2_min": 1271.7957420160972 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005669730249792337, + "SDE": 15.41197265877213, + "chi2_min": 1376.2414849254828 + }, + { + "injected": false, + "period": 9.743551254272461, + "T0": 5.753463031568231, + "duration": 0.09000206738710403, + "depth": 0.0015667054103687406, + "SDE": 5.3858364105068715, + "chi2_min": 1213.2532298462943 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.18374329805374146, + "depth": 0.005207412876188755, + "SDE": 14.868149544133214, + "chi2_min": 1277.8260611819942 + }, + { + "injected": false, + "period": 3.751904010772705, + "T0": 3.4951949631796992, + "duration": 0.05644047260284424, + "depth": 0.0012609766563400626, + "SDE": 4.929817836339992, + "chi2_min": 1313.6198509818798 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.18374329805374146, + "depth": 0.004843113943934441, + "SDE": 14.536503086957408, + "chi2_min": 1386.7924769026886 + }, + { + "injected": false, + "period": 12.579136848449707, + "T0": 4.226824228778526, + "duration": 0.12552805244922638, + "depth": 0.0016740263672545552, + "SDE": 5.493662165374919, + "chi2_min": 1264.473948842068 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.004762181080877781, + "SDE": 14.148837331556905, + "chi2_min": 1356.2967423676214 + }, + { + "injected": false, + "period": 2.6725428104400635, + "T0": 0.14387011199786048, + "duration": 0.05565265938639641, + "depth": 0.0010715341195464134, + "SDE": 6.350914315973196, + "chi2_min": 1289.7367575326448 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005513254553079605, + "SDE": 14.373714227286166, + "chi2_min": 1438.3267171983498 + }, + { + "injected": false, + "period": 0.6743714809417725, + "T0": 0.47082369012325387, + "duration": 0.038828302174806595, + "depth": 0.0007056446629576385, + "SDE": 6.976825853970327, + "chi2_min": 1296.7100488048077 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005650545936077833, + "SDE": 15.005956603817001, + "chi2_min": 1335.367667329123 + }, + { + "injected": false, + "period": 3.817335605621338, + "T0": 0.3609841388095205, + "duration": 0.06267549097537994, + "depth": 0.0010340978624299169, + "SDE": 5.272298829267682, + "chi2_min": 1315.3716891212723 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005670913495123386, + "SDE": 14.08645398117616, + "chi2_min": 1247.6468554586909 + }, + { + "injected": false, + "period": 8.94373893737793, + "T0": 4.204453678020741, + "duration": 0.07539563626050949, + "depth": 0.0017777406610548496, + "SDE": 4.767810978884853, + "chi2_min": 1302.41173783788 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005472847260534763, + "SDE": 14.711449416620528, + "chi2_min": 1300.7378989330853 + }, + { + "injected": false, + "period": 1.083532452583313, + "T0": 0.43690821784262823, + "duration": 0.05275972932577133, + "depth": 0.0006725414423272014, + "SDE": 5.844557826021007, + "chi2_min": 1303.3270971047903 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005429901648312807, + "SDE": 13.581099031295597, + "chi2_min": 1338.4324806558413 + }, + { + "injected": false, + "period": 3.751904010772705, + "T0": 1.6210399439368501, + "duration": 0.05644047260284424, + "depth": 0.0013095358153805137, + "SDE": 4.52067456400783, + "chi2_min": 1327.7450767907026 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005096363835036755, + "SDE": 13.503372276972755, + "chi2_min": 1241.699730173737 + }, + { + "injected": false, + "period": 5.536378383636475, + "T0": 2.694534725723358, + "duration": 0.07094460725784302, + "depth": 0.0013462472707033157, + "SDE": 4.509943163563067, + "chi2_min": 1267.498920703772 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005140651483088732, + "SDE": 14.075288228225297, + "chi2_min": 1313.3261367914786 + }, + { + "injected": false, + "period": 9.36865520477295, + "T0": 9.22744536080586, + "duration": 0.1387048214673996, + "depth": 0.0016929280245676637, + "SDE": 6.098590225698274, + "chi2_min": 1253.9192065986254 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005635196343064308, + "SDE": 14.216509118425094, + "chi2_min": 1322.711499255337 + }, + { + "injected": false, + "period": 0.8091202974319458, + "T0": 0.4492196189062554, + "duration": 0.04125908017158508, + "depth": 0.0006857021362520754, + "SDE": 5.7191670355570885, + "chi2_min": 1371.571016880274 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.18374329805374146, + "depth": 0.005087862256914377, + "SDE": 13.784317299008805, + "chi2_min": 1289.7056162390122 + }, + { + "injected": false, + "period": 0.8660217523574829, + "T0": 0.2383568104642464, + "duration": 0.06924357265233994, + "depth": 0.000580215360969305, + "SDE": 6.840602631288061, + "chi2_min": 1302.0461920367197 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005201755557209253, + "SDE": 14.966413216209395, + "chi2_min": 1310.6803436341113 + }, + { + "injected": false, + "period": 6.253728866577148, + "T0": 5.870805941826802, + "duration": 0.07388520240783691, + "depth": 0.0012998866150155663, + "SDE": 5.263553605921748, + "chi2_min": 1209.4973824857957 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005448559299111366, + "SDE": 13.623365131240881, + "chi2_min": 1390.4382307294914 + }, + { + "injected": false, + "period": 12.104578018188477, + "T0": 9.877395361733647, + "duration": 0.09207913279533386, + "depth": 0.001985683338716626, + "SDE": 5.286761852211583, + "chi2_min": 1289.7191295226003 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.005371253006160259, + "SDE": 14.089673680989746, + "chi2_min": 1326.8539697531978 + }, + { + "injected": false, + "period": 1.4328784942626953, + "T0": 1.2479081935049408, + "duration": 0.04094899445772171, + "depth": 0.0009649074636399746, + "SDE": 6.146771193677066, + "chi2_min": 1376.8769192147242 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005038459785282612, + "SDE": 14.131821472224129, + "chi2_min": 1418.7284940720592 + }, + { + "injected": false, + "period": 2.585336923599243, + "T0": 0.923334637585171, + "duration": 0.13419054448604584, + "depth": 0.0006379721453413367, + "SDE": 5.255854439678939, + "chi2_min": 1297.9203697652006 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0051691848784685135, + "SDE": 14.53686084600899, + "chi2_min": 1335.154218438021 + }, + { + "injected": false, + "period": 1.1863054037094116, + "T0": 0.5719344635827, + "duration": 0.04040246084332466, + "depth": 0.000764436786994338, + "SDE": 4.7496092994714445, + "chi2_min": 1279.015192561413 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0054274569265544415, + "SDE": 13.665247925323182, + "chi2_min": 1318.2331768535635 + }, + { + "injected": false, + "period": 2.7281653881073, + "T0": 1.4686617796954948, + "duration": 0.06830879300832748, + "depth": 0.0009055134141817689, + "SDE": 5.017593764460343, + "chi2_min": 1256.61867619463 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005423026625066996, + "SDE": 13.104268255806913, + "chi2_min": 1284.7775721516596 + }, + { + "injected": false, + "period": 4.76552152633667, + "T0": 1.076287644484374, + "duration": 0.06748615950345993, + "depth": 0.001369902165606618, + "SDE": 5.838514395192009, + "chi2_min": 1268.924454853593 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005303625948727131, + "SDE": 13.768917687037854, + "chi2_min": 1469.3855329483195 + }, + { + "injected": false, + "period": 5.954711437225342, + "T0": 4.0025587667688, + "duration": 0.06583552062511444, + "depth": 0.0015804474242031574, + "SDE": 4.620174626379325, + "chi2_min": 1338.3957556418723 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3166635296710325, + "duration": 0.17486760020256042, + "depth": 0.00547246215865016, + "SDE": 13.970445375123868, + "chi2_min": 1231.4136864085472 + }, + { + "injected": false, + "period": 0.7494922876358032, + "T0": 0.14383159056938233, + "duration": 0.06598714739084244, + "depth": 0.00045808343566022813, + "SDE": 5.954589818857165, + "chi2_min": 1183.9089290347831 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005119277164340019, + "SDE": 13.600633570914596, + "chi2_min": 1362.5470870930055 + }, + { + "injected": false, + "period": 2.8218910694122314, + "T0": 0.3113438265453894, + "duration": 0.07258859276771545, + "depth": 0.0010486793471500278, + "SDE": 6.400880360273832, + "chi2_min": 1301.5649681369214 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005257221404463053, + "SDE": 14.531584442008283, + "chi2_min": 1288.7046099807844 + }, + { + "injected": false, + "period": 7.533771991729736, + "T0": 2.4072671396336887, + "duration": 0.07120505720376968, + "depth": 0.0016296235844492912, + "SDE": 5.648052966355431, + "chi2_min": 1290.2735534736444 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005214043892920017, + "SDE": 13.501816275269112, + "chi2_min": 1333.4523123305858 + }, + { + "injected": false, + "period": 2.045438051223755, + "T0": 0.19033353368033445, + "duration": 0.04844745248556137, + "depth": 0.0009628356783650815, + "SDE": 5.736909643733822, + "chi2_min": 1390.8257634245467 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.302647882457407, + "duration": 0.19306959211826324, + "depth": 0.005054534412920475, + "SDE": 13.52684222988886, + "chi2_min": 1318.2392164019288 + }, + { + "injected": false, + "period": 9.047518730163574, + "T0": 4.986207971831902, + "duration": 0.12417609244585037, + "depth": 0.0012691774172708392, + "SDE": 5.130269511847086, + "chi2_min": 1229.019885531746 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.005109364632517099, + "SDE": 15.091514124837634, + "chi2_min": 1269.2879464986113 + }, + { + "injected": false, + "period": 4.433368682861328, + "T0": 3.8600783631616196, + "duration": 0.059669382870197296, + "depth": 0.0014127626782283187, + "SDE": 5.318909711833129, + "chi2_min": 1282.9852399707484 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00571216456592083, + "SDE": 13.68735144752792, + "chi2_min": 1391.5502979665098 + }, + { + "injected": false, + "period": 3.42227840423584, + "T0": 1.513876878042396, + "duration": 0.05473672226071358, + "depth": 0.0012891065562143922, + "SDE": 5.639916284783208, + "chi2_min": 1319.0337806769976 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005534767638891935, + "SDE": 14.290255163436195, + "chi2_min": 1310.234607494101 + }, + { + "injected": false, + "period": 13.635416984558105, + "T0": 4.660816806064474, + "duration": 0.1823594868183136, + "depth": 0.001385409850627184, + "SDE": 5.697635676846859, + "chi2_min": 1203.2160460078271 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005357676651328802, + "SDE": 13.642723226547098, + "chi2_min": 1339.592726568851 + }, + { + "injected": false, + "period": 12.15611457824707, + "T0": 4.847994174497785, + "duration": 0.15128548443317413, + "depth": 0.0013391552492976189, + "SDE": 5.1152540651708565, + "chi2_min": 1278.6733893042583 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0052889143116772175, + "SDE": 14.59080489843393, + "chi2_min": 1395.672670674071 + }, + { + "injected": false, + "period": 9.978133201599121, + "T0": 3.989828930425375, + "duration": 0.23240043222904205, + "depth": 0.001154838944785297, + "SDE": 6.614594176237315, + "chi2_min": 1311.4876419213956 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0054702446796, + "SDE": 13.901584845266553, + "chi2_min": 1303.8109990226483 + }, + { + "injected": false, + "period": 1.227888584136963, + "T0": 0.24339220282239182, + "duration": 0.06381379067897797, + "depth": 0.0007502195076085627, + "SDE": 6.699391970724505, + "chi2_min": 1254.5677523085624 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0049850489012897015, + "SDE": 14.549175594439973, + "chi2_min": 1355.400272127356 + }, + { + "injected": false, + "period": 2.2177605628967285, + "T0": 1.0043958503840145, + "duration": 0.06375162303447723, + "depth": 0.0008952956413850188, + "SDE": 5.1197930532229785, + "chi2_min": 1339.4699305699805 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.308795390041496, + "duration": 0.19306959211826324, + "depth": 0.004980760160833597, + "SDE": 14.525513225084268, + "chi2_min": 1379.7126956069637 + }, + { + "injected": false, + "period": 3.958618640899658, + "T0": 1.5509364717303242, + "duration": 0.0574585385620594, + "depth": 0.0012683860259130597, + "SDE": 4.945356782284237, + "chi2_min": 1282.2177474202194 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005414358805865049, + "SDE": 15.565120663517906, + "chi2_min": 1338.7585082855128 + }, + { + "injected": false, + "period": 12.286243438720703, + "T0": 0.6104409236496622, + "duration": 0.08381339907646179, + "depth": 0.0018242186633870006, + "SDE": 6.411775304695559, + "chi2_min": 1332.7316477155935 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005437161773443222, + "SDE": 15.014119449786673, + "chi2_min": 1358.6656671348069 + }, + { + "injected": false, + "period": 6.318080902099609, + "T0": 0.0469745786837219, + "duration": 0.06714829802513123, + "depth": 0.0013039918849244714, + "SDE": 4.801564872948044, + "chi2_min": 1247.8763909035313 + } + ] + } + } + }, + "k2": { + "config": { + "ndata": 4320, + "baseline": 90.0, + "cadence": 0.020833333333333332, + "noise": 0.0008, + "inject_period": 12.4, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 45.0, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 9672, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.32082032412290573, + "times_s": [ + 0.32082032412290573 + ], + "ms_per_lc": 6.416406482458115, + "lc_per_s": 155.85047529858204, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 25.520731137419894, + "median_sde_noise": 6.570467023158963, + "per_lc": [ + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004404039587825537, + "SDE": 26.17749702404464, + "chi2_min": 4387.543069049461 + }, + { + "injected": false, + "period": 1.3685027360916138, + "T0": 0.9995278422767413, + "duration": 0.044523872435092926, + "depth": 0.0003865754115395248, + "SDE": 6.356818574676183, + "chi2_min": 4326.839595866841 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21533998847007751, + "depth": 0.004162405151873827, + "SDE": 25.62356163529343, + "chi2_min": 4456.472117348283 + }, + { + "injected": false, + "period": 1.567154884338379, + "T0": 0.6710433548500703, + "duration": 0.046581678092479706, + "depth": 0.0004660347185563296, + "SDE": 9.811689077322027, + "chi2_min": 4249.244214398832 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004389334935694933, + "SDE": 25.122103240809416, + "chi2_min": 4387.086710296348 + }, + { + "injected": false, + "period": 17.946809768676758, + "T0": 11.771186328499653, + "duration": 0.19984866678714752, + "depth": 0.000686309882439673, + "SDE": 6.69523511064994, + "chi2_min": 4314.305852264322 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004312425851821899, + "SDE": 25.631966697906847, + "chi2_min": 4341.908664728141 + }, + { + "injected": false, + "period": 7.888498306274414, + "T0": 7.531212754637977, + "duration": 0.08388378471136093, + "depth": 0.0007113477913662791, + "SDE": 6.570467023158963, + "chi2_min": 4085.738149098137 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.204982191324234, + "depth": 0.004326025955379009, + "SDE": 26.14699977131388, + "chi2_min": 4431.564516407139 + }, + { + "injected": false, + "period": 1.7248963117599487, + "T0": 0.09257843054204251, + "duration": 0.14293743669986725, + "depth": 0.00026448414428159595, + "SDE": 8.812640088645056, + "chi2_min": 4256.353800798309 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.0044168573804199696, + "SDE": 24.49782119878163, + "chi2_min": 4472.887610904101 + }, + { + "injected": false, + "period": 40.31962203979492, + "T0": 12.79962811146072, + "duration": 0.1850813776254654, + "depth": 0.001074486062861979, + "SDE": 5.6723058332231195, + "chi2_min": 4326.760088810315 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004235788248479366, + "SDE": 24.954817922084917, + "chi2_min": 4373.691853085323 + }, + { + "injected": false, + "period": 13.618805885314941, + "T0": 12.353813122829024, + "duration": 0.11110421270132065, + "depth": 0.000906570116057992, + "SDE": 6.260135732254157, + "chi2_min": 4297.932157246619 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004246351309120655, + "SDE": 24.37666606073983, + "chi2_min": 4262.907247088052 + }, + { + "injected": false, + "period": 8.089868545532227, + "T0": 4.885035835718213, + "duration": 0.0888851210474968, + "depth": 0.0007501470972783864, + "SDE": 6.33623962858502, + "chi2_min": 4085.4030426856834 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.00426411023363471, + "SDE": 24.126784931778023, + "chi2_min": 4317.331950603972 + }, + { + "injected": false, + "period": 15.671825408935547, + "T0": 11.628149511437869, + "duration": 0.363585501909256, + "depth": 0.0004864199145231396, + "SDE": 5.553832261704813, + "chi2_min": 4251.70786210115 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21533998847007751, + "depth": 0.004226861987262964, + "SDE": 25.56299869559361, + "chi2_min": 4253.178250383264 + }, + { + "injected": false, + "period": 2.754897117614746, + "T0": 2.5983700068006783, + "duration": 0.08354046940803528, + "depth": 0.0003899047151207924, + "SDE": 6.612445740904377, + "chi2_min": 4304.2806226586845 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004189441911876202, + "SDE": 25.342519829150696, + "chi2_min": 4441.637445074186 + }, + { + "injected": false, + "period": 7.901713848114014, + "T0": 5.554735825406851, + "duration": 0.10750557482242584, + "depth": 0.0006463858881033957, + "SDE": 6.610492871472095, + "chi2_min": 4282.332309697514 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004312783945351839, + "SDE": 25.520731137419894, + "chi2_min": 4257.315695769827 + }, + { + "injected": false, + "period": 2.0137128829956055, + "T0": 1.3472117866492113, + "duration": 0.05591322481632233, + "depth": 0.000497570086736232, + "SDE": 10.58791051735093, + "chi2_min": 4280.768136245288 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7128754423422947, + "duration": 0.2263185828924179, + "depth": 0.004149800632148981, + "SDE": 25.51077839891597, + "chi2_min": 4445.6471137767185 + }, + { + "injected": false, + "period": 14.453306198120117, + "T0": 7.792941111104028, + "duration": 0.23816171288490295, + "depth": 0.0005341128562577069, + "SDE": 5.291739270813659, + "chi2_min": 4100.002558732529 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0041998703964054585, + "SDE": 26.349105186230148, + "chi2_min": 4507.700801853651 + }, + { + "injected": false, + "period": 10.265218734741211, + "T0": 3.1706764167317942, + "duration": 0.09158045053482056, + "depth": 0.0007960263756103814, + "SDE": 8.393836638594017, + "chi2_min": 4252.540272747918 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004519070964306593, + "SDE": 24.893549081872777, + "chi2_min": 4459.222228646795 + }, + { + "injected": false, + "period": 30.56487464904785, + "T0": 7.08230056832997, + "duration": 0.12538689374923706, + "depth": 0.0011783387744799256, + "SDE": 6.166500957414006, + "chi2_min": 4209.062413090763 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004347286187112331, + "SDE": 25.645459164761437, + "chi2_min": 4367.650080260724 + }, + { + "injected": false, + "period": 36.307926177978516, + "T0": 19.91623931560639, + "duration": 0.29323291778564453, + "depth": 0.0009722203249111772, + "SDE": 5.96117267411373, + "chi2_min": 4425.980193094114 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0043581826612353325, + "SDE": 25.620072088535927, + "chi2_min": 4418.55047630803 + }, + { + "injected": false, + "period": 3.8394787311553955, + "T0": 3.5170847570018395, + "duration": 0.08451777696609497, + "depth": 0.0004938149941153824, + "SDE": 7.28631654633567, + "chi2_min": 4251.628013641141 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7056692181227504, + "duration": 0.2263185828924179, + "depth": 0.003965914715081453, + "SDE": 26.83157991532146, + "chi2_min": 4355.02648468021 + }, + { + "injected": false, + "period": 1.1502342224121094, + "T0": 0.1958829591745257, + "duration": 0.04874681308865547, + "depth": 0.0003573001886252314, + "SDE": 6.803735442144992, + "chi2_min": 4534.605633556768 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.004336000885814428, + "SDE": 26.782616254306795, + "chi2_min": 4509.665909110085 + }, + { + "injected": false, + "period": 5.259787082672119, + "T0": 1.6857420942828583, + "duration": 0.08501768857240677, + "depth": 0.0005589709035120904, + "SDE": 6.523270829232497, + "chi2_min": 4316.4516532573625 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.204982191324234, + "depth": 0.0044480483047664165, + "SDE": 25.55277626833395, + "chi2_min": 4414.776178403963 + }, + { + "injected": false, + "period": 14.582316398620605, + "T0": 8.464501963214104, + "duration": 0.08873925358057022, + "depth": 0.001034076907671988, + "SDE": 6.74956661106862, + "chi2_min": 4359.509288619014 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004289723001420498, + "SDE": 25.876474721506405, + "chi2_min": 4506.705052521887 + }, + { + "injected": false, + "period": 12.45097541809082, + "T0": 9.849355446693153, + "duration": 0.13812200725078583, + "depth": 0.0008029531454667449, + "SDE": 7.4191729318083, + "chi2_min": 4300.465232424514 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.7385776951641674, + "duration": 0.21533998847007751, + "depth": 0.004041068255901337, + "SDE": 25.209039859208932, + "chi2_min": 4325.576950239149 + }, + { + "injected": false, + "period": 2.9074718952178955, + "T0": 1.8813054051288134, + "duration": 0.07331513613462448, + "depth": 0.00043522383202798665, + "SDE": 5.477008307447279, + "chi2_min": 4107.6394164915755 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004308898001909256, + "SDE": 25.054476969400678, + "chi2_min": 4555.486684237196 + }, + { + "injected": false, + "period": 3.8546807765960693, + "T0": 3.842780059564987, + "duration": 0.20632818341255188, + "depth": 0.0003478149010334164, + "SDE": 5.267235115169817, + "chi2_min": 4368.599291771086 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.732052489198992, + "duration": 0.21533998847007751, + "depth": 0.004108169581741095, + "SDE": 24.629152760009696, + "chi2_min": 4430.720853986382 + }, + { + "injected": false, + "period": 5.158578872680664, + "T0": 1.2311123437995661, + "duration": 0.10296858102083206, + "depth": 0.0004856034356635064, + "SDE": 7.478173375535206, + "chi2_min": 4369.114303804741 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.204982191324234, + "depth": 0.004322155378758907, + "SDE": 25.028972433845162, + "chi2_min": 4547.568737473479 + }, + { + "injected": false, + "period": 1.5529592037200928, + "T0": 1.0412210721013935, + "duration": 0.09288113564252853, + "depth": 0.00030092347878962755, + "SDE": 5.9643763694597824, + "chi2_min": 4323.484146598777 + } + ] + } + } + }, + "tess-2min": { + "config": { + "ndata": 19710, + "baseline": 27.4, + "cadence": 0.001388888888888889, + "noise": 0.002, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 2497, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.2545866258442402, + "times_s": [ + 0.2545866258442402 + ], + "ms_per_lc": 5.091732516884804, + "lc_per_s": 196.39680534746836, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.74571155601114, + "median_sde_noise": 5.684664549869927, + "per_lc": [ + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005485764238983393, + "SDE": 14.639897802515064, + "chi2_min": 19986.645856211093 + }, + { + "injected": false, + "period": 13.100994110107422, + "T0": 5.437705039673006, + "duration": 0.10437987744808197, + "depth": 0.00097375811310485, + "SDE": 6.510298948327327, + "chi2_min": 19991.359227174664 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005453883670270443, + "SDE": 14.019519939898716, + "chi2_min": 20026.577512524516 + }, + { + "injected": false, + "period": 0.727395236492157, + "T0": 0.3019376498355886, + "duration": 0.0418415404856205, + "depth": 0.0003676938358694315, + "SDE": 5.684664549869927, + "chi2_min": 19321.845612071113 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.00540515873581171, + "SDE": 14.726376734717029, + "chi2_min": 19669.763819709846 + }, + { + "injected": false, + "period": 2.640021324157715, + "T0": 0.4917686880570642, + "duration": 0.07459784299135208, + "depth": 0.0004884963855147362, + "SDE": 6.6571320754623295, + "chi2_min": 19593.49303085074 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005405776668339968, + "SDE": 14.77705988995394, + "chi2_min": 19828.85640417305 + }, + { + "injected": false, + "period": 9.231582641601562, + "T0": 1.668764258751935, + "duration": 0.07619598507881165, + "depth": 0.0008226633653976023, + "SDE": 4.722179100207669, + "chi2_min": 20048.86317008199 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005362111143767834, + "SDE": 14.354143855240103, + "chi2_min": 19451.406444908673 + }, + { + "injected": false, + "period": 1.5595471858978271, + "T0": 0.281584903400649, + "duration": 0.06259265542030334, + "depth": 0.00037873227847740054, + "SDE": 5.943673920026695, + "chi2_min": 19451.819498490713 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005432446021586657, + "SDE": 14.968046725580562, + "chi2_min": 19835.65647686846 + }, + { + "injected": false, + "period": 3.7597527503967285, + "T0": 3.551468894538374, + "duration": 0.09266479313373566, + "depth": 0.0005465572467073798, + "SDE": 4.939380341066058, + "chi2_min": 19562.21720545303 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005425633862614632, + "SDE": 14.533890983898827, + "chi2_min": 19661.441555852696 + }, + { + "injected": false, + "period": 5.938205242156982, + "T0": 1.8585079530804478, + "duration": 0.0977407768368721, + "depth": 0.0006803313735872507, + "SDE": 5.2725448233277605, + "chi2_min": 19645.717433759728 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005327630788087845, + "SDE": 14.84010647678329, + "chi2_min": 19854.839841139597 + }, + { + "injected": false, + "period": 0.6239511370658875, + "T0": 0.1721244509631692, + "duration": 0.03426845371723175, + "depth": 0.0003540570323821157, + "SDE": 6.39481275782302, + "chi2_min": 19814.719961456234 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005372271407395601, + "SDE": 14.723770628688742, + "chi2_min": 19759.874102802845 + }, + { + "injected": false, + "period": 1.7957980632781982, + "T0": 0.0708701585838174, + "duration": 0.048745013773441315, + "depth": 0.00045976031105965376, + "SDE": 6.211765862225475, + "chi2_min": 19886.988311534038 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005604728125035763, + "SDE": 14.90068397890773, + "chi2_min": 19847.307987816148 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 0.7042438654324634, + "duration": 0.04801042377948761, + "depth": 0.0004377198056317866, + "SDE": 4.988946333383417, + "chi2_min": 19686.281949983317 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005634222645312548, + "SDE": 15.213471317734784, + "chi2_min": 20082.62622246992 + }, + { + "injected": false, + "period": 1.944218635559082, + "T0": 1.0748527127935859, + "duration": 0.04533376544713974, + "depth": 0.0005116008687764406, + "SDE": 5.99529671767753, + "chi2_min": 19728.37795666568 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005626666825264692, + "SDE": 14.850588950668095, + "chi2_min": 19887.365185557555 + }, + { + "injected": false, + "period": 5.338284969329834, + "T0": 1.2329951883274148, + "duration": 0.2083013355731964, + "depth": 0.0004553081525955349, + "SDE": 5.298519016483409, + "chi2_min": 19457.346986131895 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005390736740082502, + "SDE": 14.700393260006301, + "chi2_min": 19622.466311638673 + }, + { + "injected": false, + "period": 1.9001942873001099, + "T0": 1.6714522132299692, + "duration": 0.16299022734165192, + "depth": 0.0003048948128707707, + "SDE": 6.3310102071026275, + "chi2_min": 19683.71679549537 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0052563706412911415, + "SDE": 14.640912928082045, + "chi2_min": 19651.33208119841 + }, + { + "injected": false, + "period": 2.132042646408081, + "T0": 1.8807319745508266, + "duration": 0.051615022122859955, + "depth": 0.0005292857531458139, + "SDE": 5.194727590653593, + "chi2_min": 19577.79985606737 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005402364302426577, + "SDE": 14.456168742904701, + "chi2_min": 19436.729586657333 + }, + { + "injected": false, + "period": 2.430079221725464, + "T0": 2.2736688394607825, + "duration": 0.11330521106719971, + "depth": 0.0004198209207970649, + "SDE": 6.671596344814872, + "chi2_min": 19901.711850304608 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005152017343789339, + "SDE": 14.038241987954455, + "chi2_min": 20221.619944051825 + }, + { + "injected": false, + "period": 9.231582641601562, + "T0": 3.019791075150806, + "duration": 0.07619598507881165, + "depth": 0.0008931891061365604, + "SDE": 5.57414069454813, + "chi2_min": 19451.45299624408 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005317769479006529, + "SDE": 15.080195122904549, + "chi2_min": 19643.599153703726 + }, + { + "injected": false, + "period": 8.733040809631348, + "T0": 1.2241375973440398, + "duration": 0.07479896396398544, + "depth": 0.0009044561302289367, + "SDE": 5.237870576812923, + "chi2_min": 19883.518882539494 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005483759101480246, + "SDE": 14.865137839560045, + "chi2_min": 19639.472178093947 + }, + { + "injected": false, + "period": 1.0538432598114014, + "T0": 0.844823486495514, + "duration": 0.05492657795548439, + "depth": 0.0003412514925003052, + "SDE": 7.41383958895593, + "chi2_min": 19181.17701679314 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005476575344800949, + "SDE": 14.959633809165028, + "chi2_min": 19795.391763729953 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 1.437746418930118, + "duration": 0.07496435195207596, + "depth": 0.00039721315260976553, + "SDE": 7.340804439157507, + "chi2_min": 19453.765267702773 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0053500221110880375, + "SDE": 14.900270670406965, + "chi2_min": 20215.64466906669 + }, + { + "injected": false, + "period": 9.213768005371094, + "T0": 3.8128516220795063, + "duration": 0.07614698261022568, + "depth": 0.0008834492764435709, + "SDE": 5.369271475741715, + "chi2_min": 19582.097059867356 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005436438601464033, + "SDE": 15.008994918326797, + "chi2_min": 19810.99344150698 + }, + { + "injected": false, + "period": 1.186469316482544, + "T0": 0.4394331011325221, + "duration": 0.06308804452419281, + "depth": 0.0003383326402399689, + "SDE": 4.907890099401215, + "chi2_min": 19782.61215164576 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005494052078574896, + "SDE": 14.948855768455966, + "chi2_min": 19692.064174238843 + }, + { + "injected": false, + "period": 1.7699131965637207, + "T0": 1.468044076797554, + "duration": 0.04850965365767479, + "depth": 0.0005157210398465395, + "SDE": 6.3993927850147, + "chi2_min": 19890.123616665805 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005343742202967405, + "SDE": 14.74571155601114, + "chi2_min": 19796.72417997518 + }, + { + "injected": false, + "period": 1.6712085008621216, + "T0": 0.23874408222442334, + "duration": 0.05521129071712494, + "depth": 0.00045216272701509297, + "SDE": 6.5008015934644146, + "chi2_min": 19370.162059362396 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005551040638238192, + "SDE": 14.713967185414138, + "chi2_min": 19685.802620606155 + }, + { + "injected": false, + "period": 0.9706023335456848, + "T0": 0.9548758508475785, + "duration": 0.07941237837076187, + "depth": 0.000291008735075593, + "SDE": 5.375330550042362, + "chi2_min": 20024.64889769606 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005364943295717239, + "SDE": 14.32465841986806, + "chi2_min": 19449.195780767288 + }, + { + "injected": false, + "period": 3.3878562450408936, + "T0": 2.2401032092012088, + "duration": 0.06023088097572327, + "depth": 0.0006338467355817556, + "SDE": 5.240739427079039, + "chi2_min": 19773.893695076826 + } + ] + } + } + }, + "tess-yr": { + "config": { + "ndata": 16850, + "baseline": 351.0, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 21.7, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 175.0, + "nlc": 20 + }, + "nlc": 20, + "nperiods": 42001, + "impls": { + "new": { + "nlc": 20, + "total_s": 0.5467865355312824, + "times_s": [ + 0.5467865355312824 + ], + "ms_per_lc": 27.33932677656412, + "lc_per_s": 36.57734545450557, + "n_injected": 10, + "n_recovered": 10, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 52.45753681285535, + "median_sde_noise": 8.462351870001159, + "per_lc": [ + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.00428521353751421, + "SDE": 51.74706353724402, + "chi2_min": 17045.304161600427 + }, + { + "injected": false, + "period": 71.1637191772461, + "T0": 34.1104773348261, + "duration": 0.19279845058918, + "depth": 0.0008943733409978449, + "SDE": 8.028486663164024, + "chi2_min": 16855.451608264993 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004315289203077555, + "SDE": 53.579226489536495, + "chi2_min": 17079.369669986347 + }, + { + "injected": false, + "period": 3.215691566467285, + "T0": 2.7022411507869606, + "duration": 0.05919283628463745, + "depth": 0.00034801653237082064, + "SDE": 6.9967664551079904, + "chi2_min": 16563.562434798696 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004378140438348055, + "SDE": 51.417002515500876, + "chi2_min": 16690.807726532905 + }, + { + "injected": false, + "period": 1.5765894651412964, + "T0": 1.1244638781501877, + "duration": 0.046674944460392, + "depth": 0.00028764992021024227, + "SDE": 9.104482343494766, + "chi2_min": 16943.34078928957 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004271374084055424, + "SDE": 53.34992635726429, + "chi2_min": 16960.625747997896 + }, + { + "injected": false, + "period": 13.20435905456543, + "T0": 1.339143794434733, + "duration": 0.2093072384595871, + "depth": 0.00039136491250246763, + "SDE": 8.470054973388613, + "chi2_min": 16618.206479002816 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004417836666107178, + "SDE": 52.79030615750608, + "chi2_min": 17073.47447652764 + }, + { + "injected": false, + "period": 1.0810301303863525, + "T0": 0.8323784887314218, + "duration": 0.03727799281477928, + "depth": 0.00030446742312051356, + "SDE": 12.367127339046329, + "chi2_min": 16803.589990291133 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.0041633895598351955, + "SDE": 51.3055595838171, + "chi2_min": 17136.81803806736 + }, + { + "injected": false, + "period": 35.858131408691406, + "T0": 3.57559711400188, + "duration": 0.30683863162994385, + "depth": 0.0005235545104369521, + "SDE": 7.264360780878907, + "chi2_min": 16644.461869314277 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.0043487101793289185, + "SDE": 53.18143485280267, + "chi2_min": 17383.538593224726 + }, + { + "injected": false, + "period": 1.4476821422576904, + "T0": 1.3089501248304032, + "duration": 0.04536651819944382, + "depth": 0.00027384364511817694, + "SDE": 8.454648766613705, + "chi2_min": 17168.875156750462 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.0043377201072871685, + "SDE": 52.124767468204624, + "chi2_min": 17070.551816905958 + }, + { + "injected": false, + "period": 158.73745727539062, + "T0": 94.37186024718721, + "duration": 0.3933356702327728, + "depth": 0.0010018610628321767, + "SDE": 7.243525185957916, + "chi2_min": 16733.71459318848 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004398010671138763, + "SDE": 52.852382712238594, + "chi2_min": 17030.781448475514 + }, + { + "injected": false, + "period": 143.65647888183594, + "T0": 105.68818444369845, + "duration": 0.23189450800418854, + "depth": 0.0014297961024567485, + "SDE": 9.032557748466932, + "chi2_min": 16928.81373207052 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.2470066249370575, + "depth": 0.004301433917135, + "SDE": 50.82005769699972, + "chi2_min": 16781.757778621162 + }, + { + "injected": false, + "period": 1.0684576034545898, + "T0": 0.9389372063940868, + "duration": 0.04099807143211365, + "depth": 0.000271445867838338, + "SDE": 9.543209397347338, + "chi2_min": 16826.68702423067 + } + ] + } + } + }, + "kepler-4yr": { + "config": { + "ndata": 65440, + "baseline": 1363.0, + "cadence": 0.020833333333333332, + "noise": 0.0006, + "inject_period": 41.3, + "inject_depth": 0.003, + "period_min": 0.6, + "period_max": 500.0, + "nlc": 10 + }, + "nlc": 10, + "nperiods": 171688, + "impls": { + "new": { + "nlc": 10, + "total_s": 1.9777560979127884, + "times_s": [ + 1.9777560979127884 + ], + "ms_per_lc": 197.77560979127884, + "lc_per_s": 5.056235200363398, + "n_injected": 5, + "n_recovered": 5, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 98.29821322567172, + "median_sde_noise": 8.72215844392861, + "per_lc": [ + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003353167325258255, + "SDE": 98.45695607941019, + "chi2_min": 65888.8686123984 + }, + { + "injected": false, + "period": 55.92778396606445, + "T0": 50.19175134709553, + "duration": 0.16932636499404907, + "depth": 0.0003138199681416154, + "SDE": 8.72215844392861, + "chi2_min": 65560.97205817862 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003339726710692048, + "SDE": 100.12524747056952, + "chi2_min": 66294.66735087954 + }, + { + "injected": false, + "period": 131.69247436523438, + "T0": 60.8605908603713, + "duration": 0.20403198897838593, + "depth": 0.0004167061997577548, + "SDE": 8.661975668401452, + "chi2_min": 65907.71342974457 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0033489372581243515, + "SDE": 97.03154083286489, + "chi2_min": 66182.91527186599 + }, + { + "injected": false, + "period": 96.05792236328125, + "T0": 50.60741073171448, + "duration": 0.2130729854106903, + "depth": 0.00037875358248129487, + "SDE": 11.123223521227903, + "chi2_min": 65325.414736351566 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003390850266441703, + "SDE": 98.01942805850948, + "chi2_min": 66280.838278687 + }, + { + "injected": false, + "period": 164.4737091064453, + "T0": 46.597844866974356, + "duration": 0.23087593913078308, + "depth": 0.0004218894464429468, + "SDE": 7.961574427646926, + "chi2_min": 65834.8010161592 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0033445751760154963, + "SDE": 98.29821322567172, + "chi2_min": 66655.12926137832 + }, + { + "injected": false, + "period": 2.184530735015869, + "T0": 1.3664060570654044, + "duration": 0.10407035797834396, + "depth": 7.64523065299727e-05, + "SDE": 9.621357122108616, + "chi2_min": 65125.09877587645 + } + ] + } + } + } + }, + "env": { + "python": "3.11.10", + "numpy": "2.4.6", + "hostname": "299dee83312d", + "cpu_count": 40, + "cuvarbase": "1.0.0", + "pycuda": "2026.1", + "cuda_driver_version": 12060, + "gpu": "NVIDIA RTX 4000 Ada Generation", + "compute_capability": "8.9", + "nvcc": "Cuda compilation tools, release 12.4, V12.4.131" + } +} \ No newline at end of file diff --git a/benchmarks/results/tls_survey_jul2026/tls_survey_v100.json b/benchmarks/results/tls_survey_jul2026/tls_survey_v100.json new file mode 100644 index 0000000..f6b70d2 --- /dev/null +++ b/benchmarks/results/tls_survey_jul2026/tls_survey_v100.json @@ -0,0 +1,2269 @@ +{ + "script": "benchmark_tls_survey.py", + "timestamp": "2026-07-06T18:19:15", + "args": { + "regimes": "tess-ffi,k2,tess-2min,tess-yr,kepler-4yr", + "nlc": null, + "impls": "new", + "ref_nlc": 1, + "old_nlc": 5, + "n_iter": 1, + "output": "tls_survey_v100.json", + "quick": false, + "ref_all": false + }, + "regimes": { + "tess-ffi": { + "config": { + "ndata": 1310, + "baseline": 27.4, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 100 + }, + "nlc": 100, + "nperiods": 2486, + "impls": { + "new": { + "nlc": 100, + "total_s": 0.14208540692925453, + "times_s": [ + 0.14208540692925453 + ], + "ms_per_lc": 1.4208540692925453, + "lc_per_s": 703.8020452711995, + "n_injected": 50, + "n_recovered": 50, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.057206183863471, + "median_sde_noise": 5.4799220937731, + "per_lc": [ + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004994536284357309, + "SDE": 13.824748211231611, + "chi2_min": 1343.3166566172576 + }, + { + "injected": false, + "period": 0.6225233674049377, + "T0": 0.542300333010111, + "duration": 0.037806544452905655, + "depth": 0.0005099073168821633, + "SDE": 4.320592343053708, + "chi2_min": 1315.8272696594818 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004595326259732246, + "SDE": 14.151275870863943, + "chi2_min": 1388.0540178442573 + }, + { + "injected": false, + "period": 2.0671958923339844, + "T0": 1.240600977982922, + "duration": 0.04627010226249695, + "depth": 0.0010054809972643852, + "SDE": 5.439224604982493, + "chi2_min": 1312.4691889973124 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.0052549876272678375, + "SDE": 13.546964383908636, + "chi2_min": 1225.6534163995327 + }, + { + "injected": false, + "period": 6.83004903793335, + "T0": 3.3232153407358425, + "duration": 0.14482606947422028, + "depth": 0.0009646486141718924, + "SDE": 5.193850131189129, + "chi2_min": 1239.76099682099 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3137119733402045, + "duration": 0.15838181972503662, + "depth": 0.005142259411513805, + "SDE": 14.291874487780534, + "chi2_min": 1270.7130377876167 + }, + { + "injected": false, + "period": 1.3832758665084839, + "T0": 1.2075118932858473, + "duration": 0.0404709056019783, + "depth": 0.0009033045498654246, + "SDE": 6.169803249922114, + "chi2_min": 1265.5680106485863 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0049972208216786385, + "SDE": 14.095804734228182, + "chi2_min": 1245.5418060909765 + }, + { + "injected": false, + "period": 9.259944915771484, + "T0": 5.364545925151106, + "duration": 0.12514053285121918, + "depth": 0.001239022589288652, + "SDE": 4.375034616567076, + "chi2_min": 1306.7850229998053 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.316541905906817, + "duration": 0.15838171541690826, + "depth": 0.00510247191414237, + "SDE": 12.857307581446387, + "chi2_min": 1379.8338405984605 + }, + { + "injected": false, + "period": 4.670420169830322, + "T0": 1.2254929261115564, + "duration": 0.06071445345878601, + "depth": 0.0012486246414482594, + "SDE": 5.22776518169518, + "chi2_min": 1277.774256301244 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.005526317749172449, + "SDE": 14.447433380924723, + "chi2_min": 1311.2708009539028 + }, + { + "injected": false, + "period": 0.694443941116333, + "T0": 0.5399806443484323, + "duration": 0.039209768176078796, + "depth": 0.000674063281621784, + "SDE": 5.792146913416813, + "chi2_min": 1375.2101674319447 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0049413591623306274, + "SDE": 14.09302631161843, + "chi2_min": 1403.870365483287 + }, + { + "injected": false, + "period": 0.9697667956352234, + "T0": 0.6234215032222714, + "duration": 0.04170956835150719, + "depth": 0.0007125452975742519, + "SDE": 6.053405113355577, + "chi2_min": 1344.0566340195523 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3137119733402045, + "duration": 0.15838181972503662, + "depth": 0.004847251810133457, + "SDE": 14.767257239752563, + "chi2_min": 1377.8351044304827 + }, + { + "injected": false, + "period": 7.037842750549316, + "T0": 1.9062626516681007, + "duration": 0.10343571752309799, + "depth": 0.001075836131349206, + "SDE": 5.550556064393794, + "chi2_min": 1407.0896937434245 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005022849887609482, + "SDE": 13.957691254586013, + "chi2_min": 1322.2117583682011 + }, + { + "injected": false, + "period": 6.641885280609131, + "T0": 1.800826401364219, + "duration": 0.08745435625314713, + "depth": 0.0012787478044629097, + "SDE": 6.51261874832097, + "chi2_min": 1293.3742648347863 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005061832722276449, + "SDE": 14.154451528325065, + "chi2_min": 1238.058633257935 + }, + { + "injected": false, + "period": 3.6306135654449463, + "T0": 1.9492430389519342, + "duration": 0.07513566315174103, + "depth": 0.0009633752051740885, + "SDE": 4.791418499333642, + "chi2_min": 1307.175808758614 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004871276672929525, + "SDE": 13.861185994893118, + "chi2_min": 1375.5374651029479 + }, + { + "injected": false, + "period": 1.812753677368164, + "T0": 1.3972279533641085, + "duration": 0.044288016855716705, + "depth": 0.0008740366320125759, + "SDE": 7.094830302098393, + "chi2_min": 1327.0709532199096 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004978665150702, + "SDE": 14.008937983018118, + "chi2_min": 1321.4317776885973 + }, + { + "injected": false, + "period": 13.635416984558105, + "T0": 0.11372829471468915, + "duration": 0.23358194530010223, + "depth": 0.0010562270181253552, + "SDE": 4.921974250260108, + "chi2_min": 1330.9486482442412 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004547102842479944, + "SDE": 14.403645846421027, + "chi2_min": 1272.013336329653 + }, + { + "injected": false, + "period": 5.856029033660889, + "T0": 3.841079401770827, + "duration": 0.09728776663541794, + "depth": 0.0009948982624337077, + "SDE": 6.013786949469513, + "chi2_min": 1271.3009490778647 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005151343997567892, + "SDE": 15.248616738183886, + "chi2_min": 1373.4479058239203 + }, + { + "injected": false, + "period": 9.743551254272461, + "T0": 5.753852141378843, + "duration": 0.08565451949834824, + "depth": 0.0014769668923690915, + "SDE": 5.346068202165282, + "chi2_min": 1212.8167884247855 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0048437039367854595, + "SDE": 14.55587314234365, + "chi2_min": 1276.2413443851192 + }, + { + "injected": false, + "period": 2.9746532440185547, + "T0": 1.2400459967608413, + "duration": 0.20895150303840637, + "depth": 0.0005317212198860943, + "SDE": 4.615297616613198, + "chi2_min": 1313.5703095084912 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004542770329862833, + "SDE": 14.504544196348368, + "chi2_min": 1375.4844324690948 + }, + { + "injected": false, + "period": 12.579136848449707, + "T0": 4.226824228778526, + "duration": 0.1194644570350647, + "depth": 0.001611640676856041, + "SDE": 5.3635396403655635, + "chi2_min": 1262.274012928982 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.302647882457407, + "duration": 0.19306959211826324, + "depth": 0.004228070843964815, + "SDE": 13.759452625846706, + "chi2_min": 1377.3826188324651 + }, + { + "injected": false, + "period": 2.6725428104400635, + "T0": 0.14387011199786048, + "duration": 0.05565265938639641, + "depth": 0.0009991604601964355, + "SDE": 6.7267399117552555, + "chi2_min": 1288.3582232156282 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00502576120197773, + "SDE": 14.021386056108513, + "chi2_min": 1430.8644369249123 + }, + { + "injected": false, + "period": 0.6743714809417725, + "T0": 0.46929139088928196, + "duration": 0.03346893936395645, + "depth": 0.0006973492563702166, + "SDE": 6.370018960348677, + "chi2_min": 1295.8512727122784 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.00511719798669219, + "SDE": 14.77100328882694, + "chi2_min": 1337.5560828564667 + }, + { + "injected": false, + "period": 12.07891845703125, + "T0": 3.4772668477962725, + "duration": 0.08333925157785416, + "depth": 0.001632323837839067, + "SDE": 5.126722349181245, + "chi2_min": 1315.9131548805497 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0051468717865645885, + "SDE": 13.857145590801544, + "chi2_min": 1246.5068408102534 + }, + { + "injected": false, + "period": 10.641645431518555, + "T0": 0.5814596127869152, + "duration": 0.07989320904016495, + "depth": 0.0015373544301837683, + "SDE": 4.655717986306318, + "chi2_min": 1302.4757446433 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004942566622048616, + "SDE": 14.40184200098637, + "chi2_min": 1306.720259772929 + }, + { + "injected": false, + "period": 1.0845619440078735, + "T0": 0.4145796007128588, + "duration": 0.039212726056575775, + "depth": 0.000743131386116147, + "SDE": 6.100442711846552, + "chi2_min": 1303.0670491921926 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004929995629936457, + "SDE": 13.311797451577288, + "chi2_min": 1336.85502704256 + }, + { + "injected": false, + "period": 3.751904010772705, + "T0": 1.6228371537112594, + "duration": 0.05644047260284424, + "depth": 0.0011902560945600271, + "SDE": 5.192978874403368, + "chi2_min": 1326.865363732231 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004629185423254967, + "SDE": 13.060570324782994, + "chi2_min": 1239.7685167948307 + }, + { + "injected": false, + "period": 5.536378383636475, + "T0": 2.694534725723358, + "duration": 0.06751765310764313, + "depth": 0.0012676733313128352, + "SDE": 4.190803883585782, + "chi2_min": 1266.8976156958374 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3137119733402045, + "duration": 0.15838181972503662, + "depth": 0.004855481907725334, + "SDE": 14.221075719241638, + "chi2_min": 1315.0935318110098 + }, + { + "injected": false, + "period": 9.36865520477295, + "T0": 9.22744536080586, + "duration": 0.1387048214673996, + "depth": 0.0015655959723517299, + "SDE": 6.573981845759308, + "chi2_min": 1252.7033290656664 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005103591829538345, + "SDE": 13.910988186928549, + "chi2_min": 1324.800854724087 + }, + { + "injected": false, + "period": 0.8091202974319458, + "T0": 0.4505333795458242, + "duration": 0.04125908017158508, + "depth": 0.0006217345944605768, + "SDE": 5.649665856513129, + "chi2_min": 1371.453913303614 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004763154312968254, + "SDE": 13.796502676638335, + "chi2_min": 1279.762928250731 + }, + { + "injected": false, + "period": 0.8660217523574829, + "T0": 0.2381559870601997, + "duration": 0.06924363225698471, + "depth": 0.0005305147496983409, + "SDE": 7.01535326577632, + "chi2_min": 1301.5783518420176 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004730703309178352, + "SDE": 14.60839151498775, + "chi2_min": 1307.0834198059863 + }, + { + "injected": false, + "period": 1.0024302005767822, + "T0": 0.27075475157906936, + "duration": 0.11352045834064484, + "depth": 0.0003724353446159512, + "SDE": 5.520619582563706, + "chi2_min": 1208.8534825681932 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004943359177559614, + "SDE": 13.40104079345133, + "chi2_min": 1389.8745100263664 + }, + { + "injected": false, + "period": 0.620072603225708, + "T0": 0.2626584416756259, + "duration": 0.041687000542879105, + "depth": 0.0005490335170179605, + "SDE": 5.357486698310393, + "chi2_min": 1290.547582586565 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.004910163581371307, + "SDE": 14.315323817402604, + "chi2_min": 1313.1217309836666 + }, + { + "injected": false, + "period": 1.4328784942626953, + "T0": 1.2479081935049408, + "duration": 0.04094899445772171, + "depth": 0.0008829569560475647, + "SDE": 5.731119660990421, + "chi2_min": 1376.3152012276637 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004564641043543816, + "SDE": 13.656022960128118, + "chi2_min": 1420.0044340134655 + }, + { + "injected": false, + "period": 2.585336923599243, + "T0": 0.923334637585171, + "duration": 0.13419054448604584, + "depth": 0.000590515963267535, + "SDE": 5.171262197650821, + "chi2_min": 1296.9976288289213 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004706313833594322, + "SDE": 14.58284643855217, + "chi2_min": 1330.1767404106772 + }, + { + "injected": false, + "period": 5.164575099945068, + "T0": 0.5105161548749209, + "duration": 0.19606541097164154, + "depth": 0.0006265363190323114, + "SDE": 5.017631694776126, + "chi2_min": 1278.853556208874 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004956825636327267, + "SDE": 13.465916954927257, + "chi2_min": 1308.3404156230947 + }, + { + "injected": false, + "period": 2.7281653881073, + "T0": 1.4655267958866602, + "duration": 0.05603610351681709, + "depth": 0.0008929938194341958, + "SDE": 5.563565738371344, + "chi2_min": 1255.0453176588635 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.005156008061021566, + "SDE": 13.469434386162332, + "chi2_min": 1283.244246956347 + }, + { + "injected": false, + "period": 4.76552152633667, + "T0": 1.076287644484374, + "duration": 0.06422623991966248, + "depth": 0.0012868511257693172, + "SDE": 5.748693712562537, + "chi2_min": 1269.2160331464397 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004850361030548811, + "SDE": 13.615283922648205, + "chi2_min": 1458.0778547256632 + }, + { + "injected": false, + "period": 5.954711437225342, + "T0": 4.000462559284301, + "duration": 0.06583552062511444, + "depth": 0.001470325980335474, + "SDE": 4.281483931070663, + "chi2_min": 1338.0946768454855 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3166635296710325, + "duration": 0.17486760020256042, + "depth": 0.004960371181368828, + "SDE": 13.76555275711953, + "chi2_min": 1231.1977440257347 + }, + { + "injected": false, + "period": 2.147103786468506, + "T0": 1.9348414355212356, + "duration": 0.0768798366189003, + "depth": 0.0006785376463085413, + "SDE": 5.68102410812672, + "chi2_min": 1183.5185653415458 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.004848890006542206, + "SDE": 13.769896752821715, + "chi2_min": 1365.883146663318 + }, + { + "injected": false, + "period": 2.8218910694122314, + "T0": 0.3113438265453894, + "duration": 0.06908220052719116, + "depth": 0.0010092707816511393, + "SDE": 6.640720602015786, + "chi2_min": 1299.333389310017 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004796753637492657, + "SDE": 14.298441701791154, + "chi2_min": 1280.7008868362532 + }, + { + "injected": false, + "period": 7.533771991729736, + "T0": 2.411801624352279, + "duration": 0.07481919229030609, + "depth": 0.0013799527660012245, + "SDE": 5.8061339876057625, + "chi2_min": 1291.9232631751825 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004743710160255432, + "SDE": 13.22648309749417, + "chi2_min": 1329.3352469008983 + }, + { + "injected": false, + "period": 2.043039560317993, + "T0": 0.20179237219836565, + "duration": 0.046089161187410355, + "depth": 0.0008912621415220201, + "SDE": 5.4377927880629615, + "chi2_min": 1391.1062161528182 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.302647882457407, + "duration": 0.19306959211826324, + "depth": 0.004489851649850607, + "SDE": 13.125461259691887, + "chi2_min": 1341.6502881792726 + }, + { + "injected": false, + "period": 3.65653133392334, + "T0": 0.8910219764560026, + "duration": 0.06178278848528862, + "depth": 0.0010194219648838043, + "SDE": 5.065225246362491, + "chi2_min": 1227.876746646248 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3137119733402045, + "duration": 0.15838181972503662, + "depth": 0.00521567277610302, + "SDE": 15.189097276543496, + "chi2_min": 1232.31205538533 + }, + { + "injected": false, + "period": 4.433368682861328, + "T0": 3.8581784102236725, + "duration": 0.059669382870197296, + "depth": 0.0012552980333566666, + "SDE": 5.399391539559226, + "chi2_min": 1283.402894008224 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005188316572457552, + "SDE": 13.25389997547865, + "chi2_min": 1389.190251579791 + }, + { + "injected": false, + "period": 10.13871955871582, + "T0": 1.3899856289888248, + "duration": 0.2579602003097534, + "depth": 0.0008953508804552257, + "SDE": 5.796619746385799, + "chi2_min": 1317.9714885779986 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.005047174636274576, + "SDE": 14.183686802971941, + "chi2_min": 1302.187000072226 + }, + { + "injected": false, + "period": 13.635416984558105, + "T0": 4.660816806064474, + "duration": 0.1823594868183136, + "depth": 0.0012732133036479354, + "SDE": 5.8839950242694625, + "chi2_min": 1202.6427141749414 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004888550844043493, + "SDE": 13.425439410650005, + "chi2_min": 1331.2404926821323 + }, + { + "injected": false, + "period": 12.15611457824707, + "T0": 4.850591000732152, + "duration": 0.13702285289764404, + "depth": 0.0013082606019452214, + "SDE": 4.783097993755491, + "chi2_min": 1277.6464594489116 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.0047805835492908955, + "SDE": 14.1119977020878, + "chi2_min": 1400.117006611571 + }, + { + "injected": false, + "period": 9.978133201599121, + "T0": 3.989828930425375, + "duration": 0.23240043222904205, + "depth": 0.0010685203596949577, + "SDE": 7.026146879570354, + "chi2_min": 1309.7399345544522 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004954393021762371, + "SDE": 13.472370335921822, + "chi2_min": 1305.724756346867 + }, + { + "injected": false, + "period": 1.227888584136963, + "T0": 0.23685326980719879, + "duration": 0.047413453459739685, + "depth": 0.0007900178316049278, + "SDE": 6.718391751540111, + "chi2_min": 1253.6789144942313 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3166635296710325, + "duration": 0.17486760020256042, + "depth": 0.0045309956185519695, + "SDE": 14.40717291491021, + "chi2_min": 1352.9125401937622 + }, + { + "injected": false, + "period": 2.2177605628967285, + "T0": 1.0043958503840145, + "duration": 0.06375162303447723, + "depth": 0.0008319917833432555, + "SDE": 5.2101067543342205, + "chi2_min": 1338.2337130712012 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.309144427296914, + "duration": 0.15838181972503662, + "depth": 0.005017078015953302, + "SDE": 14.55090687210087, + "chi2_min": 1368.1301150405575 + }, + { + "injected": false, + "period": 3.958618640899658, + "T0": 1.5509364717303242, + "duration": 0.0574585385620594, + "depth": 0.001214984804391861, + "SDE": 4.823721460791652, + "chi2_min": 1282.229853361992 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004880807362496853, + "SDE": 15.11338851387768, + "chi2_min": 1347.1430297698878 + }, + { + "injected": false, + "period": 12.312492370605469, + "T0": 0.625243753194809, + "duration": 0.15193158388137817, + "depth": 0.0011966126039624214, + "SDE": 6.127166003183497, + "chi2_min": 1332.4424726820241 + }, + { + "injected": true, + "period": 7.70003080368042, + "T0": 2.3110956855367704, + "duration": 0.17486760020256042, + "depth": 0.004942622501403093, + "SDE": 14.79142907107735, + "chi2_min": 1355.3571954551194 + }, + { + "injected": false, + "period": 6.318080902099609, + "T0": 0.02583512814802269, + "duration": 0.09978199750185013, + "depth": 0.0009755137725733221, + "SDE": 4.651989778407932, + "chi2_min": 1247.792465656339 + } + ] + } + } + }, + "k2": { + "config": { + "ndata": 4320, + "baseline": 90.0, + "cadence": 0.020833333333333332, + "noise": 0.0008, + "inject_period": 12.4, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 45.0, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 9672, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.19335215166211128, + "times_s": [ + 0.19335215166211128 + ], + "ms_per_lc": 3.8670430332422256, + "lc_per_s": 258.5955189543301, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 25.201212996804028, + "median_sde_noise": 6.574796123467906, + "per_lc": [ + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.18565721809864044, + "depth": 0.004096044693142176, + "SDE": 25.886694644591515, + "chi2_min": 4383.846413776024 + }, + { + "injected": false, + "period": 1.3685027360916138, + "T0": 0.9995278422767413, + "duration": 0.044523872435092926, + "depth": 0.0003543264465406537, + "SDE": 6.372890809448275, + "chi2_min": 4326.31600577895 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.00405429070815444, + "SDE": 25.35949977734318, + "chi2_min": 4391.303049965471 + }, + { + "injected": false, + "period": 1.567154884338379, + "T0": 0.6710433548500703, + "duration": 0.04433155059814453, + "depth": 0.0004464688536245376, + "SDE": 10.254838885848923, + "chi2_min": 4247.198331220121 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004084067884832621, + "SDE": 24.924516382187875, + "chi2_min": 4380.199625335411 + }, + { + "injected": false, + "period": 17.946809768676758, + "T0": 11.771186328499653, + "duration": 0.19019503891468048, + "depth": 0.0006513726548291743, + "SDE": 6.598806967575485, + "chi2_min": 4313.265534728922 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.18565721809864044, + "depth": 0.004031355958431959, + "SDE": 25.96822627870716, + "chi2_min": 4326.187839532829 + }, + { + "injected": false, + "period": 7.355057716369629, + "T0": 1.9938212273293345, + "duration": 0.09989657998085022, + "depth": 0.0005613853572867811, + "SDE": 6.574796123467906, + "chi2_min": 4085.3548769271165 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.0040359050035476685, + "SDE": 26.060700847892555, + "chi2_min": 4419.476137500889 + }, + { + "injected": false, + "period": 1.7254761457443237, + "T0": 0.08025470415425229, + "duration": 0.11726968735456467, + "depth": 0.0002659271704033017, + "SDE": 8.915781523713441, + "chi2_min": 4256.026837373626 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.0040281713008880615, + "SDE": 24.336495976466068, + "chi2_min": 4453.034095279101 + }, + { + "injected": false, + "period": 44.27975845336914, + "T0": 8.941723780283041, + "duration": 0.3819054961204529, + "depth": 0.0006938476581126451, + "SDE": 5.626467295929954, + "chi2_min": 4325.792513737073 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.18565721809864044, + "depth": 0.003936475608497858, + "SDE": 24.49283173357621, + "chi2_min": 4373.98567632751 + }, + { + "injected": false, + "period": 13.618805885314941, + "T0": 12.356419633092571, + "duration": 0.10573731362819672, + "depth": 0.0008530879858881235, + "SDE": 6.705846573149672, + "chi2_min": 4296.93668910697 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.003946550656110048, + "SDE": 24.398364317727268, + "chi2_min": 4258.882833025552 + }, + { + "injected": false, + "period": 8.089868545532227, + "T0": 4.885035835718213, + "duration": 0.0888851210474968, + "depth": 0.0007029274711385369, + "SDE": 6.53334233889305, + "chi2_min": 4083.083096396621 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.003970298916101456, + "SDE": 23.86850258430663, + "chi2_min": 4308.172160564909 + }, + { + "injected": false, + "period": 11.277223587036133, + "T0": 2.307841625154026, + "duration": 0.15503697097301483, + "depth": 0.0005774652818217874, + "SDE": 5.756786244378622, + "chi2_min": 4249.9400512796165 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004039672203361988, + "SDE": 25.41096291795666, + "chi2_min": 4242.778836320764 + }, + { + "injected": false, + "period": 2.755979537963867, + "T0": 2.585970627129086, + "duration": 0.056225892156362534, + "depth": 0.00044577528024092317, + "SDE": 6.9204886710698545, + "chi2_min": 4302.643274483636 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.18565721809864044, + "depth": 0.0038935276679694653, + "SDE": 24.831492887384506, + "chi2_min": 4440.216790777311 + }, + { + "injected": false, + "period": 7.901713848114014, + "T0": 5.555892549455166, + "duration": 0.09737034142017365, + "depth": 0.0006193366716615856, + "SDE": 6.68314073340143, + "chi2_min": 4281.997379277592 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7084950072158165, + "duration": 0.18565721809864044, + "depth": 0.004017242230474949, + "SDE": 25.201212996804028, + "chi2_min": 4254.131613738577 + }, + { + "injected": false, + "period": 2.0137128829956055, + "T0": 1.3468567478449245, + "duration": 0.05064193159341812, + "depth": 0.00047726265620440245, + "SDE": 10.683345388634434, + "chi2_min": 4280.127469283618 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004109283909201622, + "SDE": 25.27781326050563, + "chi2_min": 4399.7225532298435 + }, + { + "injected": false, + "period": 2.7014899253845215, + "T0": 1.6762021150968565, + "duration": 0.07898787409067154, + "depth": 0.0003673879546113312, + "SDE": 5.476729717225788, + "chi2_min": 4098.632586503525 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.0039016627706587315, + "SDE": 26.22208666482348, + "chi2_min": 4505.634883884901 + }, + { + "injected": false, + "period": 10.265218734741211, + "T0": 3.1679013496663515, + "duration": 0.08715666830539703, + "depth": 0.000743471086025238, + "SDE": 8.06459904646771, + "chi2_min": 4252.67821601574 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.18565721809864044, + "depth": 0.004194461274892092, + "SDE": 24.543167263490176, + "chi2_min": 4461.4037472014825 + }, + { + "injected": false, + "period": 30.56487464904785, + "T0": 7.08230056832997, + "duration": 0.12538689374923706, + "depth": 0.0011026031570509076, + "SDE": 6.298550328825552, + "chi2_min": 4207.851691121158 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.0040408591739833355, + "SDE": 25.53118224669925, + "chi2_min": 4363.073542174787 + }, + { + "injected": false, + "period": 36.307926177978516, + "T0": 19.91623931560639, + "duration": 0.29323291778564453, + "depth": 0.0008949660114012659, + "SDE": 6.136413000415641, + "chi2_min": 4425.171017602781 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.0040709576569497585, + "SDE": 25.385079949472022, + "chi2_min": 4399.550720448655 + }, + { + "injected": false, + "period": 3.1908771991729736, + "T0": 0.5407807732185823, + "duration": 0.06849425286054611, + "depth": 0.00045074979425407946, + "SDE": 6.943886854996949, + "chi2_min": 4250.652292281888 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.00394544517621398, + "SDE": 27.331136154734367, + "chi2_min": 4300.823115539585 + }, + { + "injected": false, + "period": 1.1502342224121094, + "T0": 0.197360137900489, + "duration": 0.04639210179448128, + "depth": 0.00033418359817005694, + "SDE": 6.551303311164812, + "chi2_min": 4534.204394161627 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.004040546249598265, + "SDE": 26.440717679789405, + "chi2_min": 4497.820694266335 + }, + { + "injected": false, + "period": 5.21898889541626, + "T0": 1.9571208357810974, + "duration": 0.20673802495002747, + "depth": 0.00033009087201207876, + "SDE": 6.867265125261597, + "chi2_min": 4315.49914623832 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.0041288407519459724, + "SDE": 25.179250185027833, + "chi2_min": 4414.150812193026 + }, + { + "injected": false, + "period": 14.592304229736328, + "T0": 8.439254865465955, + "duration": 0.0932646244764328, + "depth": 0.0008997920085676014, + "SDE": 6.432134855816056, + "chi2_min": 4359.845241377803 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.0038928925059735775, + "SDE": 25.628381969887695, + "chi2_min": 4501.120579865637 + }, + { + "injected": false, + "period": 12.45097541809082, + "T0": 9.853753344433017, + "duration": 0.13812200725078583, + "depth": 0.0007385652279481292, + "SDE": 7.190493045627132, + "chi2_min": 4299.71282535054 + }, + { + "injected": true, + "period": 12.39454174041748, + "T0": 3.7385776951641674, + "duration": 0.20493797957897186, + "depth": 0.0037482825573533773, + "SDE": 24.781014409494773, + "chi2_min": 4330.148849653211 + }, + { + "injected": false, + "period": 6.057334899902344, + "T0": 3.0855804639822964, + "duration": 0.07310348004102707, + "depth": 0.0005771773867309093, + "SDE": 5.39455810967937, + "chi2_min": 4106.679127490111 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.18565721809864044, + "depth": 0.004012775141745806, + "SDE": 24.565142946018387, + "chi2_min": 4548.415883455946 + }, + { + "injected": false, + "period": 1.9488736391067505, + "T0": 1.2610359251262224, + "duration": 0.16437040269374847, + "depth": 0.0002546783653087914, + "SDE": 5.335259812758131, + "chi2_min": 4367.933386772307 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.71311754722862, + "duration": 0.19508059322834015, + "depth": 0.003971559461206198, + "SDE": 24.320701609068305, + "chi2_min": 4388.407499494195 + }, + { + "injected": false, + "period": 20.636655807495117, + "T0": 1.2264171468407028, + "duration": 0.09962917864322662, + "depth": 0.0008535315282642841, + "SDE": 7.449187261085652, + "chi2_min": 4368.517917848931 + }, + { + "injected": true, + "period": 12.402583122253418, + "T0": 3.7072057524929107, + "duration": 0.19508059322834015, + "depth": 0.004027797374874353, + "SDE": 24.936090114669174, + "chi2_min": 4538.718273606291 + }, + { + "injected": false, + "period": 1.4916346073150635, + "T0": 0.13033700851491936, + "duration": 0.04582108184695244, + "depth": 0.00038103508995845914, + "SDE": 6.140997664777387, + "chi2_min": 4323.307037834129 + } + ] + } + } + }, + "tess-2min": { + "config": { + "ndata": 19710, + "baseline": 27.4, + "cadence": 0.001388888888888889, + "noise": 0.002, + "inject_period": 7.7, + "inject_depth": 0.005, + "period_min": 0.6, + "period_max": 13.7, + "nlc": 50 + }, + "nlc": 50, + "nperiods": 2497, + "impls": { + "new": { + "nlc": 50, + "total_s": 0.15442728251218796, + "times_s": [ + 0.15442728251218796 + ], + "ms_per_lc": 3.088545650243759, + "lc_per_s": 323.776985430368, + "n_injected": 25, + "n_recovered": 25, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 14.631184017496109, + "median_sde_noise": 5.8276992197673065, + "per_lc": [ + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0049664671532809734, + "SDE": 14.511805973437204, + "chi2_min": 19987.608990976718 + }, + { + "injected": false, + "period": 13.100994110107422, + "T0": 5.437705039673006, + "duration": 0.0945393294095993, + "depth": 0.0009462718735449016, + "SDE": 6.218545663851509, + "chi2_min": 19990.264594072247 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004929786082357168, + "SDE": 13.82952668342654, + "chi2_min": 20035.94592072764 + }, + { + "injected": false, + "period": 13.100994110107422, + "T0": 0.15181724491853288, + "duration": 0.37815743684768677, + "depth": 0.00040304477442987263, + "SDE": 5.412653048702645, + "chi2_min": 19320.924473307685 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004904397297650576, + "SDE": 14.709911967208317, + "chi2_min": 19659.042140022346 + }, + { + "injected": false, + "period": 2.640021324157715, + "T0": 0.49285268476407396, + "duration": 0.07459786534309387, + "depth": 0.00045013066846877337, + "SDE": 6.37912014455607, + "chi2_min": 19592.601715390534 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0048844218254089355, + "SDE": 14.631184017496109, + "chi2_min": 19840.06441198555 + }, + { + "injected": false, + "period": 9.71132755279541, + "T0": 1.684317547055585, + "duration": 0.07749367505311966, + "depth": 0.0007628701278008521, + "SDE": 4.643142154034844, + "chi2_min": 20047.546487266438 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.3101788923334254, + "duration": 0.17484456300735474, + "depth": 0.004847844131290913, + "SDE": 14.170468105466723, + "chi2_min": 19459.394237877423 + }, + { + "injected": false, + "period": 1.5595471858978271, + "T0": 0.281584903400649, + "duration": 0.06259265542030334, + "depth": 0.00035733776167035103, + "SDE": 6.133569932837978, + "chi2_min": 19450.04878659191 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.00494115287438035, + "SDE": 14.934345918656119, + "chi2_min": 19811.90842999346 + }, + { + "injected": false, + "period": 3.7597527503967285, + "T0": 3.5487965172173404, + "duration": 0.08818858861923218, + "depth": 0.0005031783948652446, + "SDE": 4.845939359134309, + "chi2_min": 19562.621872353666 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0048937126994132996, + "SDE": 14.346971868723235, + "chi2_min": 19681.977688665196 + }, + { + "injected": false, + "period": 5.938205242156982, + "T0": 1.8585079530804478, + "duration": 0.0977407768368721, + "depth": 0.0006222114316187799, + "SDE": 5.532715469680132, + "chi2_min": 19645.132535764733 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004818635061383247, + "SDE": 14.640026979696545, + "chi2_min": 19860.658444655222 + }, + { + "injected": false, + "period": 0.6239511370658875, + "T0": 0.1721244509631692, + "duration": 0.03426845371723175, + "depth": 0.00033038845867849886, + "SDE": 7.0807451820863845, + "chi2_min": 19813.219665817196 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.0048765623942017555, + "SDE": 14.628734025382231, + "chi2_min": 19747.14876100597 + }, + { + "injected": false, + "period": 4.556577205657959, + "T0": 3.351539513559203, + "duration": 0.09879637509584427, + "depth": 0.00046747003216296434, + "SDE": 5.8276992197673065, + "chi2_min": 19887.865550761333 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.00508846715092659, + "SDE": 14.84249018474726, + "chi2_min": 19832.461552269273 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 0.7026321212040472, + "duration": 0.04569127783179283, + "depth": 0.0004081173974554986, + "SDE": 4.770439070682372, + "chi2_min": 19686.11071394511 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005101200193166733, + "SDE": 15.04480815336938, + "chi2_min": 20083.27710137617 + }, + { + "injected": false, + "period": 1.944218635559082, + "T0": 1.0748527127935859, + "duration": 0.04533376544713974, + "depth": 0.0004777288413606584, + "SDE": 6.197533313519624, + "chi2_min": 19727.02577046268 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005103249568492174, + "SDE": 14.776355510397183, + "chi2_min": 19878.123486338805 + }, + { + "injected": false, + "period": 5.355508804321289, + "T0": 1.199133642505899, + "duration": 0.09922617673873901, + "depth": 0.0006026808405295014, + "SDE": 5.162988651305213, + "chi2_min": 19456.848130541075 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004868575371801853, + "SDE": 14.538692492650142, + "chi2_min": 19636.010256951173 + }, + { + "injected": false, + "period": 0.8417149186134338, + "T0": 0.668898175097727, + "duration": 0.04615739732980728, + "depth": 0.0003474026161711663, + "SDE": 6.254640973283194, + "chi2_min": 19683.038635781624 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004774271510541439, + "SDE": 14.513693414179459, + "chi2_min": 19636.113575339034 + }, + { + "injected": false, + "period": 2.132042646408081, + "T0": 1.8807319745508266, + "duration": 0.051615022122859955, + "depth": 0.0004840661713387817, + "SDE": 5.2538700697855205, + "chi2_min": 19577.356464267443 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004903943277895451, + "SDE": 14.392462830205792, + "chi2_min": 19423.792086657333 + }, + { + "injected": false, + "period": 2.433090925216675, + "T0": 2.2493649780494422, + "duration": 0.11910539865493774, + "depth": 0.00037549060652963817, + "SDE": 6.827777238599113, + "chi2_min": 19900.991971154217 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004682204220443964, + "SDE": 14.010258965609918, + "chi2_min": 20204.227365926825 + }, + { + "injected": false, + "period": 9.213768005371094, + "T0": 3.037360770377745, + "duration": 0.0840730294585228, + "depth": 0.0007720250287093222, + "SDE": 5.165652201639094, + "chi2_min": 19451.32359980548 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004808785393834114, + "SDE": 14.815688328967624, + "chi2_min": 19650.37283534435 + }, + { + "injected": false, + "period": 8.733040809631348, + "T0": 1.2241375973440398, + "duration": 0.07479896396398544, + "depth": 0.0008323907968588173, + "SDE": 5.432235623151964, + "chi2_min": 19882.748090532048 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004958319012075663, + "SDE": 14.562404975680403, + "chi2_min": 19647.290293328322 + }, + { + "injected": false, + "period": 0.6907184720039368, + "T0": 0.5337274346762655, + "duration": 0.03544960170984268, + "depth": 0.0003173367877025157, + "SDE": 7.927425804810807, + "chi2_min": 19180.72897106255 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004934984724968672, + "SDE": 14.786869629495767, + "chi2_min": 19821.351968808078 + }, + { + "injected": false, + "period": 1.7158273458480835, + "T0": 1.436677274974798, + "duration": 0.07134322077035904, + "depth": 0.00036875662044622004, + "SDE": 7.296099190818101, + "chi2_min": 19453.75529417677 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004847510252147913, + "SDE": 14.78850613901599, + "chi2_min": 20212.399796019814 + }, + { + "injected": false, + "period": 4.487700462341309, + "T0": 1.568976793495409, + "duration": 0.06614834815263748, + "depth": 0.0006190845742821693, + "SDE": 5.294980863061793, + "chi2_min": 19581.16770233867 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004917678888887167, + "SDE": 14.863112032456096, + "chi2_min": 19816.37478916323 + }, + { + "injected": false, + "period": 0.8550297021865845, + "T0": 0.39220515779566156, + "duration": 0.038063161075115204, + "depth": 0.0003480055893305689, + "SDE": 4.981027191056973, + "chi2_min": 19780.845065616708 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004977596458047628, + "SDE": 14.722472150541336, + "chi2_min": 19689.09371525447 + }, + { + "injected": false, + "period": 1.7699131965637207, + "T0": 1.466499523809091, + "duration": 0.04850965365767479, + "depth": 0.0004770091036334634, + "SDE": 6.181892254357935, + "chi2_min": 19889.050656765903 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004852476995438337, + "SDE": 14.651176792889993, + "chi2_min": 19782.216123334554 + }, + { + "injected": false, + "period": 1.6712085008621216, + "T0": 0.23874408222442334, + "duration": 0.05254431068897247, + "depth": 0.00042131132795475423, + "SDE": 6.297867326815318, + "chi2_min": 19369.96945911215 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.005023227538913488, + "SDE": 14.57819501444377, + "chi2_min": 19689.35071630928 + }, + { + "injected": false, + "period": 0.9706023335456848, + "T0": 0.952585647599431, + "duration": 0.07192564755678177, + "depth": 0.00028235651552677155, + "SDE": 5.854673361596672, + "chi2_min": 20023.695400763077 + }, + { + "injected": true, + "period": 7.696986198425293, + "T0": 2.315746140649111, + "duration": 0.17484456300735474, + "depth": 0.004857673309743404, + "SDE": 14.150109315731994, + "chi2_min": 19449.491679204788 + }, + { + "injected": false, + "period": 8.042945861816406, + "T0": 3.4846562324514707, + "duration": 0.07646815478801727, + "depth": 0.0008291943813674152, + "SDE": 5.126222758428061, + "chi2_min": 19774.037059029462 + } + ] + } + } + }, + "tess-yr": { + "config": { + "ndata": 16850, + "baseline": 351.0, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 21.7, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 175.0, + "nlc": 20 + }, + "nlc": 20, + "nperiods": 42001, + "impls": { + "new": { + "nlc": 20, + "total_s": 0.34016153216362, + "times_s": [ + 0.34016153216362 + ], + "ms_per_lc": 17.008076608181, + "lc_per_s": 58.795595941694735, + "n_injected": 10, + "n_recovered": 10, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 53.27684286723869, + "median_sde_noise": 8.475549940896691, + "per_lc": [ + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.003985285758972168, + "SDE": 52.52794267307139, + "chi2_min": 17032.804161600427 + }, + { + "injected": false, + "period": 77.67776489257812, + "T0": 7.999921420467899, + "duration": 0.2542693614959717, + "depth": 0.0007119004148989916, + "SDE": 7.88728480150725, + "chi2_min": 16854.908607090067 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.00401003472507, + "SDE": 54.354682131569625, + "chi2_min": 17070.743693423847 + }, + { + "injected": false, + "period": 55.14839553833008, + "T0": 52.78218937309475, + "duration": 0.2268328219652176, + "depth": 0.0006788634927943349, + "SDE": 6.475408867465852, + "chi2_min": 16564.201440459707 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004078013356775045, + "SDE": 52.35457710087205, + "chi2_min": 16669.709582001655 + }, + { + "injected": false, + "period": 115.44019317626953, + "T0": 40.610479753671825, + "duration": 0.19526782631874084, + "depth": 0.0010937515180557966, + "SDE": 9.289476895165315, + "chi2_min": 16943.432229490376 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.003973984159529209, + "SDE": 54.03703866575641, + "chi2_min": 16946.253189404146 + }, + { + "injected": false, + "period": 6.136112213134766, + "T0": 3.792819025809422, + "duration": 0.06987259536981583, + "depth": 0.0004311114316806197, + "SDE": 8.584658625062868, + "chi2_min": 16616.814742018563 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004104473162442446, + "SDE": 53.55619974408649, + "chi2_min": 17065.533314418266 + }, + { + "injected": false, + "period": 1.0810301303863525, + "T0": 0.8323784887314218, + "duration": 0.03727799281477928, + "depth": 0.0002662955957930535, + "SDE": 11.62929676085854, + "chi2_min": 16806.166494044795 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.0038824535440653563, + "SDE": 52.557725691364844, + "chi2_min": 17112.30436619236 + }, + { + "injected": false, + "period": 35.86662292480469, + "T0": 3.5118320590913754, + "duration": 0.17800024151802063, + "depth": 0.0006325138383544981, + "SDE": 7.3888615817912315, + "chi2_min": 16643.5253821164 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004052670206874609, + "SDE": 53.70233039426787, + "chi2_min": 17360.095233849726 + }, + { + "injected": false, + "period": 105.29816436767578, + "T0": 18.38559195387313, + "duration": 0.18937286734580994, + "depth": 0.0009571330738253891, + "SDE": 8.366441256730516, + "chi2_min": 17168.121176113866 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004038219340145588, + "SDE": 52.99748599039089, + "chi2_min": 17052.555723155958 + }, + { + "injected": false, + "period": 158.6757354736328, + "T0": 94.3506197494853, + "duration": 0.4794192910194397, + "depth": 0.0008418660727329552, + "SDE": 7.713874966221854, + "chi2_min": 16732.353856341557 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004093342926353216, + "SDE": 53.73570147653968, + "chi2_min": 17013.570022694264 + }, + { + "injected": false, + "period": 143.65647888183594, + "T0": 105.67623106332121, + "duration": 0.1902312934398651, + "depth": 0.001477456884458661, + "SDE": 8.895018055638662, + "chi2_min": 16926.715160293177 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.0040013594552874565, + "SDE": 52.06098131238311, + "chi2_min": 16767.923550105537 + }, + { + "injected": false, + "period": 1.0684576034545898, + "T0": 0.9397277924298919, + "duration": 0.04099808633327484, + "depth": 0.0002517042157705873, + "SDE": 10.026241554881262, + "chi2_min": 16825.25249206514 + } + ] + } + } + }, + "kepler-4yr": { + "config": { + "ndata": 65440, + "baseline": 1363.0, + "cadence": 0.020833333333333332, + "noise": 0.0006, + "inject_period": 41.3, + "inject_depth": 0.003, + "period_min": 0.6, + "period_max": 500.0, + "nlc": 10 + }, + "nlc": 10, + "nperiods": 171688, + "impls": { + "new": { + "nlc": 10, + "total_s": 1.4650629945099354, + "times_s": [ + 1.4650629945099354 + ], + "ms_per_lc": 146.50629945099354, + "lc_per_s": 6.82564506609834, + "n_injected": 5, + "n_recovered": 5, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 102.31244744721329, + "median_sde_noise": 8.48757577323759, + "per_lc": [ + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0030894107185304165, + "SDE": 102.40051303208178, + "chi2_min": 65543.0815030234 + }, + { + "injected": false, + "period": 55.92778396606445, + "T0": 50.18960453722707, + "duration": 0.16932646930217743, + "depth": 0.00028693469357676804, + "SDE": 8.48757577323759, + "chi2_min": 65560.16814887687 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003081277944147587, + "SDE": 104.30749291660592, + "chi2_min": 65923.97301494204 + }, + { + "injected": false, + "period": 253.2958526611328, + "T0": 196.6530496462574, + "duration": 0.3961925506591797, + "depth": 0.0003831548965536058, + "SDE": 8.290345844723038, + "chi2_min": 65907.63152056489 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003086133161559701, + "SDE": 100.93630112578315, + "chi2_min": 65833.95628749099 + }, + { + "injected": false, + "period": 96.05792236328125, + "T0": 50.60741073171448, + "duration": 0.2027805745601654, + "depth": 0.0003557393793016672, + "SDE": 11.321606242738845, + "chi2_min": 65324.45621736963 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0031173741444945335, + "SDE": 101.93700097570971, + "chi2_min": 65971.8080052495 + }, + { + "injected": false, + "period": 164.4737091064453, + "T0": 46.61144217862193, + "duration": 0.19900889694690704, + "depth": 0.00041615593363530934, + "SDE": 7.8630042377815, + "chi2_min": 65834.0297949983 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0030800634995102882, + "SDE": 102.31244744721329, + "chi2_min": 66320.43199575332 + }, + { + "injected": false, + "period": 2.184530735015869, + "T0": 1.369407355578943, + "duration": 0.10407035797834396, + "depth": 6.97811265126802e-05, + "SDE": 9.845653250598026, + "chi2_min": 65124.53212548583 + } + ] + } + } + } + }, + "env": { + "python": "3.11.10", + "numpy": "2.4.6", + "hostname": "2c893c3df85f", + "cpu_count": 80, + "cuvarbase": "1.0.0", + "pycuda": "2026.1", + "cuda_driver_version": 12040, + "gpu": "Tesla V100-SXM2-16GB", + "compute_capability": "7.0", + "nvcc": "Cuda compilation tools, release 12.4, V12.4.131" + } +} \ No newline at end of file diff --git a/benchmarks/results/tls_survey_jul2026/tls_survey_v100b.json b/benchmarks/results/tls_survey_jul2026/tls_survey_v100b.json new file mode 100644 index 0000000..78d752b --- /dev/null +++ b/benchmarks/results/tls_survey_jul2026/tls_survey_v100b.json @@ -0,0 +1,367 @@ +{ + "script": "benchmark_tls_survey.py", + "timestamp": "2026-07-06T18:20:57", + "args": { + "regimes": "kepler-4yr,tess-yr", + "nlc": null, + "impls": "new", + "ref_nlc": 1, + "old_nlc": 5, + "n_iter": 1, + "output": "tls_survey_v100b.json", + "quick": false, + "ref_all": false + }, + "regimes": { + "kepler-4yr": { + "config": { + "ndata": 65440, + "baseline": 1363.0, + "cadence": 0.020833333333333332, + "noise": 0.0006, + "inject_period": 41.3, + "inject_depth": 0.003, + "period_min": 0.6, + "period_max": 500.0, + "nlc": 10 + }, + "nlc": 10, + "nperiods": 171688, + "impls": { + "new": { + "nlc": 10, + "total_s": 1.4562256298959255, + "times_s": [ + 1.4562256298959255 + ], + "ms_per_lc": 145.62256298959255, + "lc_per_s": 6.86706770894747, + "n_injected": 5, + "n_recovered": 5, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 102.31244754205247, + "median_sde_noise": 8.48757579072466, + "per_lc": [ + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0030894107185304165, + "SDE": 102.4005130679425, + "chi2_min": 65543.0815030234 + }, + { + "injected": false, + "period": 55.92778396606445, + "T0": 50.18960453722707, + "duration": 0.16932646930217743, + "depth": 0.00028693469357676804, + "SDE": 8.48757579072466, + "chi2_min": 65560.16814887687 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003081277944147587, + "SDE": 104.30749300460378, + "chi2_min": 65923.97301494204 + }, + { + "injected": false, + "period": 253.2958526611328, + "T0": 196.6530496462574, + "duration": 0.3961925506591797, + "depth": 0.0003831548965536058, + "SDE": 8.290345864402228, + "chi2_min": 65907.63152056489 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.003086133161559701, + "SDE": 100.93630122077218, + "chi2_min": 65833.95628749099 + }, + { + "injected": false, + "period": 96.05792236328125, + "T0": 50.60741073171448, + "duration": 0.2027805745601654, + "depth": 0.0003557393793016672, + "SDE": 11.321606292828855, + "chi2_min": 65324.45621736963 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0031173741444945335, + "SDE": 101.93700022710857, + "chi2_min": 65971.8080052495 + }, + { + "injected": false, + "period": 164.4737091064453, + "T0": 46.61144217862193, + "duration": 0.19900889694690704, + "depth": 0.00041615593363530934, + "SDE": 7.863004249934092, + "chi2_min": 65834.0297949983 + }, + { + "injected": true, + "period": 41.30036926269531, + "T0": 12.382114458711385, + "duration": 0.2772420048713684, + "depth": 0.0030800634995102882, + "SDE": 102.31244754205247, + "chi2_min": 66320.43199575332 + }, + { + "injected": false, + "period": 2.184530735015869, + "T0": 1.369407355578943, + "duration": 0.10407035797834396, + "depth": 6.97811265126802e-05, + "SDE": 9.845653212100956, + "chi2_min": 65124.53212548583 + } + ] + } + } + }, + "tess-yr": { + "config": { + "ndata": 16850, + "baseline": 351.0, + "cadence": 0.020833333333333332, + "noise": 0.001, + "inject_period": 21.7, + "inject_depth": 0.004, + "period_min": 0.6, + "period_max": 175.0, + "nlc": 20 + }, + "nlc": 20, + "nperiods": 42001, + "impls": { + "new": { + "nlc": 20, + "total_s": 0.3298470973968506, + "times_s": [ + 0.3298470973968506 + ], + "ms_per_lc": 16.49235486984253, + "lc_per_s": 60.63415490947098, + "n_injected": 10, + "n_recovered": 10, + "n_alias": 0, + "recovery_frac": 1.0, + "median_sde_injected": 53.27684294213596, + "median_sde_noise": 8.475549912415742, + "per_lc": [ + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.003985285758972168, + "SDE": 52.52794280298104, + "chi2_min": 17032.804161600427 + }, + { + "injected": false, + "period": 77.67776489257812, + "T0": 7.999921420467899, + "duration": 0.2542693614959717, + "depth": 0.0007119004148989916, + "SDE": 7.887282337040116, + "chi2_min": 16854.908607090067 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.00401003472507, + "SDE": 54.35468226435136, + "chi2_min": 17070.743693423847 + }, + { + "injected": false, + "period": 55.14839553833008, + "T0": 52.78218937309475, + "duration": 0.2268328219652176, + "depth": 0.0006788634927943349, + "SDE": 6.4754088282884315, + "chi2_min": 16564.201440459707 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004078013356775045, + "SDE": 52.354577062494386, + "chi2_min": 16669.709582001655 + }, + { + "injected": false, + "period": 115.44019317626953, + "T0": 40.610479753671825, + "duration": 0.19526782631874084, + "depth": 0.0010937515180557966, + "SDE": 9.289476498694379, + "chi2_min": 16943.432229490376 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.003973984159529209, + "SDE": 54.03703867483079, + "chi2_min": 16946.253189404146 + }, + { + "injected": false, + "period": 6.136112213134766, + "T0": 3.792819025809422, + "duration": 0.06987259536981583, + "depth": 0.0004311114316806197, + "SDE": 8.584658587227791, + "chi2_min": 16616.814742018563 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004104473162442446, + "SDE": 53.556199795622724, + "chi2_min": 17065.533314418266 + }, + { + "injected": false, + "period": 1.0810301303863525, + "T0": 0.8323784887314218, + "duration": 0.03727799281477928, + "depth": 0.0002662955957930535, + "SDE": 11.62930028802867, + "chi2_min": 16806.166494044795 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.0038824535440653563, + "SDE": 52.557725468536425, + "chi2_min": 17112.30436619236 + }, + { + "injected": false, + "period": 35.86662292480469, + "T0": 3.5118320590913754, + "duration": 0.17800024151802063, + "depth": 0.0006325138383544981, + "SDE": 7.388861608999988, + "chi2_min": 16643.5253821164 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004052670206874609, + "SDE": 53.70233035497617, + "chi2_min": 17360.095233849726 + }, + { + "injected": false, + "period": 105.29816436767578, + "T0": 18.38559195387313, + "duration": 0.18937286734580994, + "depth": 0.0009571330738253891, + "SDE": 8.366441237603693, + "chi2_min": 17168.121176113866 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004038219340145588, + "SDE": 52.99748608864919, + "chi2_min": 17052.555723155958 + }, + { + "injected": false, + "period": 158.6757354736328, + "T0": 94.3506197494853, + "duration": 0.4794192910194397, + "depth": 0.0008418660727329552, + "SDE": 7.713874971628795, + "chi2_min": 16732.353856341557 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.004093342926353216, + "SDE": 53.735701438061646, + "chi2_min": 17013.570022694264 + }, + { + "injected": false, + "period": 143.65647888183594, + "T0": 105.67623106332121, + "duration": 0.1902312934398651, + "depth": 0.001477456884458661, + "SDE": 8.895017997469333, + "chi2_min": 16926.715160293177 + }, + { + "injected": true, + "period": 21.701496124267578, + "T0": 6.495490946717155, + "duration": 0.235074982047081, + "depth": 0.0040013594552874565, + "SDE": 52.06098135221865, + "chi2_min": 16767.923550105537 + }, + { + "injected": false, + "period": 1.0684576034545898, + "T0": 0.9397277924298919, + "duration": 0.04099808633327484, + "depth": 0.0002517042157705873, + "SDE": 10.02623970261992, + "chi2_min": 16825.25249206514 + } + ] + } + } + } + }, + "env": { + "python": "3.11.10", + "numpy": "2.4.6", + "hostname": "2c893c3df85f", + "cpu_count": 80, + "cuvarbase": "1.0.0", + "pycuda": "2026.1", + "cuda_driver_version": 12040, + "gpu": "Tesla V100-SXM2-16GB", + "compute_capability": "7.0", + "nvcc": "Cuda compilation tools, release 12.4, V12.4.131" + } +} \ No newline at end of file diff --git a/cuvarbase/kernels/tls_fast.cu b/cuvarbase/kernels/tls_fast.cu new file mode 100644 index 0000000..26a147e --- /dev/null +++ b/cuvarbase/kernels/tls_fast.cu @@ -0,0 +1,576 @@ +/* + * Fast Transit Least Squares (TLS) GPU kernel — batch-native. + * + * Algorithmic differences from tls.cu (the reference kernel): + * + * 1. Closed-form chi2. For the weighted least-squares transit fit with + * template T and depth d, chi2(d) = chi2_0 - 2 d num + d^2 den with + * num = sum_i (1 - y_i) T_i / sigma_i^2 + * den = sum_i T_i^2 / sigma_i^2 + * chi2_0 = sum_i (y_i - 1)^2 / sigma_i^2 (per-lightcurve constant) + * At the optimal depth d* = num/den, chi2 = chi2_0 - num^2/den, so a + * single accumulation pass yields both the depth and the chi2 — the + * reference kernel's second full-data chi2 pass is redundant. + * Minimizing chi2 over trials is exactly maximizing num^2/den, so the + * per-period argmin never suffers cancellation against chi2_0. + * + * 2. Phase-binned evaluation. Each block folds its lightcurve at its + * period ONCE into NBINS phase bins (A_k = sum (1-y)/sigma^2, + * B_k = sum 1/sigma^2), then every (duration, t0) trial integrates + * only the ~q*NBINS bins inside the transit window instead of + * scanning all ndata points. This removes both the O(ndata) factor + * from the trial loop and the shared-memory cap on ndata (raw data + * stay in global memory and are read exactly once per period). + * + * 3. Integrated template tables. S1(x) = int_{-1}^{x} T dx and + * S2(x) = int_{-1}^{x} T^2 dx are precomputed on the CPU + * (tls_models.generate_template_integrals). The bin-averaged + * template over a bin's transit-coordinate span [c0, c1] is + * (S1(c1)-S1(c0))/(c1-c0): area sampling rather than point + * sampling, so coarse bins (few bins per duration) remain accurate. + * + * 4. Batch-native. Grid is (nperiods, nlc); per-lightcurve data are + * concatenated with offset/length arrays. A whole survey chunk is a + * single kernel launch sharing one period grid and one template. + * + * The (duration, t0) trial grid is IDENTICAL to tls.cu: n_durations + * log-spaced durations in [qmin, qmax], t0 = j/n_t0 with + * n_t0 = clamp(ceil(T0_OVERSAMPLE/q), MIN_N_T0, MAX_N_T0), and the + * same validity gate 0 < depth < 0.5. + * + * References: + * [1] Hippke & Heller (2019), A&A 623, A39 + * [2] Kovacs et al. (2002), A&A 391, 369 + */ + +#include + +//{CPP_DEFS} + +#ifndef BLOCK_SIZE +#define BLOCK_SIZE 128 +#endif + +/* Number of phase bins; must be a power of two (wrap uses a mask). */ +#ifndef NBINS +#define NBINS 2048 +#endif + +/* Number of intervals in the integrated template tables (tables have + * NTEMPLATE+1 entries). Must match the Python-side table length. */ +#ifndef NTEMPLATE +#define NTEMPLATE 1024 +#endif + +/* Maximum n_durations supported by the per-duration shared staging. */ +#ifndef MAX_DURATIONS +#define MAX_DURATIONS 64 +#endif + +/* Number of local durations scanned by the refinement kernel (odd). */ +#ifndef REFINE_ND +#define REFINE_ND 3 +#endif + +#ifndef T0_OVERSAMPLE +#define T0_OVERSAMPLE 3.0f +#endif +#ifndef MIN_N_T0 +#define MIN_N_T0 30 +#endif +#ifndef MAX_N_T0 +#define MAX_N_T0 20000 +#endif + +#define WARP_SIZE 32 + +__device__ inline float mod1f(float x) { + return x - floorf(x); +} + +__device__ inline int t0_grid_size(float duration_phase) { + int n_t0 = (int)ceilf(T0_OVERSAMPLE / duration_phase); + if (n_t0 < MIN_N_T0) n_t0 = MIN_N_T0; + if (n_t0 > MAX_N_T0) n_t0 = MAX_N_T0; + return n_t0; +} + +/* + * Evaluate an integrated table S (NTEMPLATE+1 entries spanning + * x in [-1, 1]) at x, with linear interpolation. Outside [-1, 1] the + * template is zero, so S saturates at its endpoint values. + */ +__device__ inline float lookup_integral(const float* __restrict__ S, float x) +{ + float idx_f = (x + 1.0f) * (0.5f * (float)NTEMPLATE); + idx_f = fminf(fmaxf(idx_f, 0.0f), (float)NTEMPLATE); + int i0 = (int)idx_f; + if (i0 >= NTEMPLATE) i0 = NTEMPLATE - 1; + float frac = idx_f - (float)i0; + return S[i0] + (S[i0 + 1] - S[i0]) * frac; +} + +/* + * Fast TLS search kernel (batch-native, Keplerian duration constraints). + * + * Grid: (nperiods, nlc, 1); Block: (BLOCK_SIZE, 1, 1) + * + * Inputs (global memory): + * t_hi_all, t_lo_all, a_all, b_all : concatenated per-point arrays; + * t is stored as an epoch-subtracted float-float pair + * (t = t_hi + t_lo to float64 precision), and for point i, + * a = (1 - y)/sigma^2 and b = 1/sigma^2 + * (sigma^2 includes the +1e-10 regularizer, matching tls.cu) + * lc_off, lc_len : per-lightcurve offset/length into the above + * periods[nperiods_band], qmin[...], qmax[...] : the trial grid FOR + * THIS LAUNCH. The host may split the full grid into bands that + * compile with different NBINS (narrow durations need finer + * bins; the scan cost is proportional to NBINS, so coarse bands + * should not pay the finest band's price). + * period_map[nperiods_band] : global period index of each band entry + * (identity when the grid is not banded) + * S1, S2 : integrated template tables (NTEMPLATE+1 entries each) + * + * Outputs, laid out as [lc * nperiods_total + period_map[band idx]]: + * score_out (num^2/den = chi2_0 - chi2; <= 0 marks a failed period; + * the host reconstructs chi2 in float64), best_t0_out, + * best_duration_out, best_depth_out + * + * Shared memory layout (floats): + * A[NBINS] | B[NBINS] | S1[NTEMPLATE+1] | S2[NTEMPLATE+1] | + * red_score[BLOCK_SIZE] | red_t0[BLOCK_SIZE] | red_dur[BLOCK_SIZE] | + * red_depth[BLOCK_SIZE] | dur_q[MAX_DURATIONS] | dur_cum[MAX_DURATIONS+1] + */ +extern "C" __global__ void tls_fast_search_kernel( + const float* __restrict__ t_hi_all, + const float* __restrict__ t_lo_all, + const float* __restrict__ a_all, + const float* __restrict__ b_all, + const int* __restrict__ lc_off, + const int* __restrict__ lc_len, + const float* __restrict__ periods, + const float* __restrict__ qmin, + const float* __restrict__ qmax, + const int* __restrict__ period_map, + const float* __restrict__ S1_g, + const float* __restrict__ S2_g, + const int nperiods_band, + const int nperiods_total, + const int n_durations, + float* __restrict__ score_out, + float* __restrict__ best_t0_out, + float* __restrict__ best_duration_out, + float* __restrict__ best_depth_out) +{ + extern __shared__ float shared_mem[]; + float* A = shared_mem; + float* B = &A[NBINS]; + float* S1 = &B[NBINS]; + float* S2 = &S1[NTEMPLATE + 1]; + float* red_score = &S2[NTEMPLATE + 1]; + float* red_t0 = &red_score[BLOCK_SIZE]; + float* red_dur = &red_t0[BLOCK_SIZE]; + float* red_depth = &red_dur[BLOCK_SIZE]; + float* dur_q = &red_depth[BLOCK_SIZE]; + /* trial-index prefix sums per duration (stored as float-cast ints + * would lose precision above 2^24; keep a separate int view) */ + int* dur_cum = (int*)&dur_q[MAX_DURATIONS]; + + const int period_idx = blockIdx.x; + const int lc_idx = blockIdx.y; + if (period_idx >= nperiods_band) return; + + const int off = lc_off[lc_idx]; + const int nd = lc_len[lc_idx]; + const float period = periods[period_idx]; + + /* --- Stage integrated template tables and zero the bins --- */ + for (int i = threadIdx.x; i < NTEMPLATE + 1; i += blockDim.x) { + S1[i] = S1_g[i]; + S2[i] = S2_g[i]; + } + for (int i = threadIdx.x; i < NBINS; i += blockDim.x) { + A[i] = 0.0f; + B[i] = 0.0f; + } + + /* --- Per-duration trial bookkeeping (one thread; tiny) --- */ + if (threadIdx.x == 0) { + float lqmin = logf(qmin[period_idx]); + float lqmax = logf(qmax[period_idx]); + int cum = 0; + for (int d = 0; d < n_durations; d++) { + float lq = (n_durations > 1) + ? lqmin + (lqmax - lqmin) * d / (n_durations - 1) + : lqmin; + float q = expf(lq); + dur_q[d] = q; + dur_cum[d] = cum; + cum += t0_grid_size(q); + } + dur_cum[n_durations] = cum; + } + __syncthreads(); + + /* --- Fold and bin the lightcurve at this period --- + * Float-float ("double-single") fold: at plain float32, t/P for a + * 1,400-day baseline and a short period carries a phase error of + * ~1e-4 — the size of a whole bin. Times are stored as a hi/lo + * float32 pair (t = t_hi + t_lo exactly to float64 precision) and + * the period reciprocal is split the same way, so the fractional + * phase is recovered to ~1e-7 with pure FP32 FMAs. This avoids + * double-precision math, which runs at 1/64 rate on consumer GPUs + * and would otherwise dominate the whole kernel at large ndata. */ + const double inv_period_d = 1.0 / (double)period; + const float inv_hi = (float)inv_period_d; + const float inv_lo = (float)(inv_period_d - (double)inv_hi); + for (int i = threadIdx.x; i < nd; i += blockDim.x) { + const float th = t_hi_all[off + i]; + const float tl = t_lo_all[off + i]; + float u = th * inv_hi; + float e = fmaf(th, inv_hi, -u); /* exact product residual */ + float c = fmaf(th, inv_lo, fmaf(tl, inv_hi, e)); + float phi = (u - floorf(u)) + c; + phi -= floorf(phi); + int k = (int)(phi * (float)NBINS); + k &= (NBINS - 1); + atomicAdd(&A[k], a_all[off + i]); + atomicAdd(&B[k], b_all[off + i]); + } + __syncthreads(); + + const int total_trials = dur_cum[n_durations]; + + /* --- Scan all (duration, t0) trials, flattened across threads --- */ + float best_score = -1.0f; /* score = num^2/den = chi2_0 - chi2 */ + float best_t0 = 0.0f; + float best_dur = 0.0f; + float best_depth = 0.0f; + + int d_idx = 0; + for (int trial = threadIdx.x; trial < total_trials; trial += blockDim.x) { + /* locate the duration bucket (monotonically increasing) */ + while (dur_cum[d_idx + 1] <= trial) d_idx++; + + const float q = dur_q[d_idx]; + const float hd = 0.5f * q; + const float inv_hd = 1.0f / hd; + const int n_t0 = dur_cum[d_idx + 1] - dur_cum[d_idx]; + const int t0_idx = trial - dur_cum[d_idx]; + const float t0 = (float)t0_idx / (float)n_t0; + + /* bins overlapping the window [t0 - hd, t0 + hd] */ + const float invNB = 1.0f / (float)NBINS; + int k0 = (int)floorf((t0 - hd) * (float)NBINS); + int k1 = (int)ceilf((t0 + hd) * (float)NBINS) - 1; + /* q >= 1 is rejected host-side; belt-and-braces so a rogue + * window can never visit a bin twice */ + if (k1 - k0 >= NBINS) k1 = k0 + NBINS - 1; + + /* transit coordinate of bin kk's left edge, and per-bin span */ + const float dc = invNB * inv_hd; + + float num = 0.0f; + float den = 0.0f; + float c0 = ((float)k0 * invNB - t0) * inv_hd; + float s1_prev = lookup_integral(S1, c0); + float s2_prev = lookup_integral(S2, c0); + for (int kk = k0; kk <= k1; kk++) { + int k = kk & (NBINS - 1); + float c1 = c0 + dc; + float s1_next = lookup_integral(S1, c1); + float s2_next = lookup_integral(S2, c1); + num += A[k] * (s1_next - s1_prev); + den += B[k] * (s2_next - s2_prev); + s1_prev = s1_next; + s2_prev = s2_next; + c0 = c1; + } + /* bin-average scale: 1/(c1-c0) = hd*NBINS applied once */ + const float scale = hd * (float)NBINS; + num *= scale; + den *= scale; + + if (den > 1e-10f && num > 0.0f) { + float depth = num / den; + if (depth < 0.5f) { + float score = num * depth; /* num^2/den */ + if (score > best_score) { + best_score = score; + best_t0 = t0; + best_dur = q * period; + best_depth = depth; + } + } + } + } + + /* --- Block reduction (max score) --- */ + red_score[threadIdx.x] = best_score; + red_t0[threadIdx.x] = best_t0; + red_dur[threadIdx.x] = best_dur; + red_depth[threadIdx.x] = best_depth; + __syncthreads(); + + for (int stride = blockDim.x / 2; stride >= WARP_SIZE; stride /= 2) { + if (threadIdx.x < stride) { + if (red_score[threadIdx.x + stride] > red_score[threadIdx.x]) { + red_score[threadIdx.x] = red_score[threadIdx.x + stride]; + red_t0[threadIdx.x] = red_t0[threadIdx.x + stride]; + red_dur[threadIdx.x] = red_dur[threadIdx.x + stride]; + red_depth[threadIdx.x] = red_depth[threadIdx.x + stride]; + } + } + __syncthreads(); + } + + if (threadIdx.x < WARP_SIZE) { + float v_score = red_score[threadIdx.x]; + float v_t0 = red_t0[threadIdx.x]; + float v_dur = red_dur[threadIdx.x]; + float v_dep = red_depth[threadIdx.x]; + + for (int offset = WARP_SIZE / 2; offset > 0; offset /= 2) { + float o_score = __shfl_down_sync(0xffffffff, v_score, offset); + float o_t0 = __shfl_down_sync(0xffffffff, v_t0, offset); + float o_dur = __shfl_down_sync(0xffffffff, v_dur, offset); + float o_dep = __shfl_down_sync(0xffffffff, v_dep, offset); + if (o_score > v_score) { + v_score = o_score; + v_t0 = o_t0; + v_dur = o_dur; + v_dep = o_dep; + } + } + + if (threadIdx.x == 0) { + const size_t out_idx = (size_t)lc_idx * nperiods_total + + period_map[period_idx]; + /* Write the SCORE (delta-chi2 = num^2/den = chi2_0 - chi2), + * not chi2 itself: subtracting from the large per-LC + * constant in float32 would quantize the spectrum by + * ulp(chi2_0) ~ 6e-8 * ndata. The host reconstructs + * chi2 = chi2_0 - score in float64. score <= 0 marks a + * period with no valid trial. */ + if (v_score > 0.0f) { + score_out[out_idx] = v_score; + best_t0_out[out_idx] = v_t0; + best_duration_out[out_idx] = v_dur; + best_depth_out[out_idx] = v_dep; + } else { + score_out[out_idx] = -1.0f; + best_t0_out[out_idx] = 0.0f; + best_duration_out[out_idx] = 0.0f; + best_depth_out[out_idx] = 0.0f; + } + } + } +} + +/* + * Exact refinement kernel. + * + * The binned scan quantizes t0 to the bin grid and smears each point's + * template weight over its bin. This kernel re-evaluates the best + * candidate periods per lightcurve EXACTLY (per-point template lookup, + * no binning) on a fine local (duration, t0) grid centered on the + * coarse solution. + * + * Results go to separate compact per-candidate outputs — the coarse + * per-period spectrum is left untouched. Detection statistics (SDE) + * must be computed from a UNIFORM-fidelity spectrum: a finer trial + * grid digs deeper chi2 minima everywhere (noise included), so mixing + * refined values into the coarse spectrum — or refining everything — + * shifts the SR distribution and deflates the SDE scale that the + * legacy kernel and its calibrated thresholds established. Refinement + * therefore only sharpens the best-fit parameters (period choice among + * the candidates, t0, duration, depth, chi2_min). + * + * Grid: (n_candidates, nlc, 1); Block: (BLOCK_SIZE, 1, 1) + * cand_period_idx[lc * n_candidates + c] gives the period index to + * refine (a value < 0 disables that slot). + * + * Trial layout per candidate: REFINE_ND durations log-spaced within + * [q0/dur_span, q0*dur_span] (bracketing one coarse duration-grid + * step), each with n_t0_local epochs spanning +/- t0_halfwidth around + * the coarse t0 at stride q/refine_oversample. + * + * Each trial is owned by one warp-group slice of the block: trials are + * distributed round-robin over (blockDim/WARP_SIZE) warps; a warp + * accumulates num/den over all points with lane-strided reads and + * reduces with shuffles. Points stream from global memory (coalesced); + * the point template T is staged in shared memory. + * + * Shared memory layout (floats): + * T[NTEMPLATE + 1] | warp_best[4 * (BLOCK_SIZE/WARP_SIZE)] + */ +extern "C" __global__ void tls_refine_kernel( + const float* __restrict__ t_hi_all, + const float* __restrict__ t_lo_all, + const float* __restrict__ a_all, + const float* __restrict__ b_all, + const int* __restrict__ lc_off, + const int* __restrict__ lc_len, + const float* __restrict__ periods, + const int* __restrict__ cand_period_idx, + const float* __restrict__ T_g, + const int nperiods, + const int n_candidates, + const float dur_span, /* e.g. one coarse log-step, ~1.10 */ + const float t0_halfwidth_frac, /* halfwidth in units of duration */ + const float refine_oversample, /* t0 stride = q / refine_oversample */ + const float* __restrict__ coarse_t0_in, + const float* __restrict__ coarse_duration_in, + float* __restrict__ refined_score_out, /* [lc * n_candidates + c] */ + float* __restrict__ refined_t0_out, + float* __restrict__ refined_duration_out, + float* __restrict__ refined_depth_out) +{ + extern __shared__ float shared_mem[]; + float* T_sh = shared_mem; + float* warp_best = &T_sh[NTEMPLATE + 1]; /* 4 floats per warp */ + + const int cand_idx = blockIdx.x; + const int lc_idx = blockIdx.y; + if (cand_idx >= n_candidates) return; + + const size_t slot = (size_t)lc_idx * n_candidates + cand_idx; + const int period_idx = cand_period_idx[slot]; + + /* disabled or sentinel candidates still need a sentinel output */ + float coarse_dur = 0.0f, coarse_t0 = 0.0f, period = 1.0f; + if (period_idx >= 0) { + const size_t in_idx = (size_t)lc_idx * nperiods + period_idx; + coarse_dur = coarse_duration_in[in_idx]; + coarse_t0 = coarse_t0_in[in_idx]; + period = periods[period_idx]; + } + if (period_idx < 0 || coarse_dur <= 0.0f) { + if (threadIdx.x == 0) { + refined_score_out[slot] = -1.0f; + refined_t0_out[slot] = 0.0f; + refined_duration_out[slot] = 0.0f; + refined_depth_out[slot] = 0.0f; + } + return; + } + + const int off = lc_off[lc_idx]; + const int nd = lc_len[lc_idx]; + const double inv_period_d = 1.0 / (double)period; + const float inv_hi = (float)inv_period_d; + const float inv_lo = (float)(inv_period_d - (double)inv_hi); + const float q0 = coarse_dur / period; + + for (int i = threadIdx.x; i < NTEMPLATE + 1; i += blockDim.x) { + T_sh[i] = T_g[i]; + } + __syncthreads(); + + const int warp_id = threadIdx.x / WARP_SIZE; + const int lane = threadIdx.x % WARP_SIZE; + const int n_warps = blockDim.x / WARP_SIZE; + + /* local trial grid */ + const float lq0 = logf(q0); + const float ldspan = logf(dur_span); + const int n_dur_local = REFINE_ND; /* compile-time, odd, e.g. 5 */ + + float w_best_score = -1.0f; + float w_best_t0 = 0.0f, w_best_dur = 0.0f, w_best_depth = 0.0f; + + /* count t0 trials for the central duration to fix the grid size + * (same count reused for all durations so trial indexing is flat) */ + const float t0_hw = t0_halfwidth_frac * q0; + const float dt0 = q0 / refine_oversample; + int n_t0_local = 2 * (int)ceilf(t0_hw / dt0) + 1; + + const int total_trials = n_dur_local * n_t0_local; + + for (int trial = warp_id; trial < total_trials; trial += n_warps) { + const int d_i = trial / n_t0_local; + const int t0_i = trial % n_t0_local; + + const float lq = lq0 + ldspan * (2.0f * d_i / (n_dur_local - 1) - 1.0f); + const float q = expf(lq); + const float hd = 0.5f * q; + const float inv_hd = 1.0f / hd; + float t0 = coarse_t0 + dt0 * (float)(t0_i - n_t0_local / 2); + t0 = t0 - floorf(t0); /* wrap to [0, 1) */ + + float num = 0.0f; + float den = 0.0f; + for (int i = lane; i < nd; i += WARP_SIZE) { + const float th = t_hi_all[off + i]; + const float tl = t_lo_all[off + i]; + float u = th * inv_hi; + float e = fmaf(th, inv_hi, -u); + float cc = fmaf(th, inv_lo, fmaf(tl, inv_hi, e)); + float phi = (u - floorf(u)) + cc; + phi -= floorf(phi); + float rel = phi - t0; + rel -= rintf(rel); /* wrap to [-0.5, 0.5] */ + float c = rel * inv_hd; + if (fabsf(c) < 1.0f) { + /* point template lookup (linear interpolation) */ + float idx_f = (c + 1.0f) * (0.5f * (float)NTEMPLATE); + int i0 = (int)idx_f; + if (i0 >= NTEMPLATE) i0 = NTEMPLATE - 1; + float frac = idx_f - (float)i0; + float Tv = T_sh[i0] + (T_sh[i0 + 1] - T_sh[i0]) * frac; + num += a_all[off + i] * Tv; + den += b_all[off + i] * Tv * Tv; + } + } + /* warp reduction of the two partial sums */ + for (int offset = WARP_SIZE / 2; offset > 0; offset /= 2) { + num += __shfl_down_sync(0xffffffff, num, offset); + den += __shfl_down_sync(0xffffffff, den, offset); + } + + if (lane == 0 && den > 1e-10f && num > 0.0f) { + float depth = num / den; + if (depth < 0.5f) { + float score = num * depth; + if (score > w_best_score) { + w_best_score = score; + w_best_t0 = t0; + w_best_dur = q * period; + w_best_depth = depth; + } + } + } + } + + /* combine warp winners via shared memory (few warps; lane 0 only) */ + if (lane == 0) { + warp_best[4 * warp_id + 0] = w_best_score; + warp_best[4 * warp_id + 1] = w_best_t0; + warp_best[4 * warp_id + 2] = w_best_dur; + warp_best[4 * warp_id + 3] = w_best_depth; + } + __syncthreads(); + + if (threadIdx.x == 0) { + float b_score = -1.0f, b_t0 = 0.0f, b_dur = 0.0f, b_dep = 0.0f; + for (int w = 0; w < n_warps; w++) { + if (warp_best[4 * w] > b_score) { + b_score = warp_best[4 * w]; + b_t0 = warp_best[4 * w + 1]; + b_dur = warp_best[4 * w + 2]; + b_dep = warp_best[4 * w + 3]; + } + } + if (b_score > 0.0f) { + refined_score_out[slot] = b_score; + refined_t0_out[slot] = b_t0; + refined_duration_out[slot] = b_dur; + refined_depth_out[slot] = b_dep; + } else { + refined_score_out[slot] = -1.0f; + refined_t0_out[slot] = 0.0f; + refined_duration_out[slot] = 0.0f; + refined_depth_out[slot] = 0.0f; + } + } +} diff --git a/cuvarbase/tests/test_tls_basic.py b/cuvarbase/tests/test_tls_basic.py index 24ebdc1..52d581a 100644 --- a/cuvarbase/tests/test_tls_basic.py +++ b/cuvarbase/tests/test_tls_basic.py @@ -465,8 +465,9 @@ def test_sde_positive_with_transit(self): class TestSharedMemoryGuard: - """tls_search_gpu must fail loudly (before touching the GPU) when - the shared-memory layout exceeds the 48 KB per-block budget.""" + """The LEGACY kernel (use_fast=False) must fail loudly (before + touching the GPU) when its shared-memory layout exceeds the 48 KB + per-block budget. The default fast path has no such cap.""" def test_large_ndata_raises_value_error(self): from cuvarbase.tls import tls_search_gpu @@ -476,7 +477,8 @@ def test_large_ndata_raises_value_error(self): y = 1 + 0.001 * rand.randn(ndata) dy = 0.001 * np.ones(ndata) with pytest.raises(ValueError, match="shared memory"): - tls_search_gpu(t, y, dy, periods=np.array([1.0, 2.0])) + tls_search_gpu(t, y, dy, periods=np.array([1.0, 2.0]), + use_fast=False) def test_guard_accounts_for_template_size(self): from cuvarbase.tls import tls_search_gpu @@ -489,7 +491,20 @@ def test_guard_accounts_for_template_size(self): dy = 0.001 * np.ones(ndata) with pytest.raises(ValueError, match="shared memory"): tls_search_gpu(t, y, dy, periods=np.array([1.0, 2.0]), - n_template=4000) + n_template=4000, use_fast=False) + + def test_fast_path_has_no_ndata_cap(self): + # regression for the removed cap: the default (fast) path must + # accept TESS-length lightcurves outright + from cuvarbase.tls import tls_search_gpu + rand = np.random.RandomState(3) + ndata = 20000 + t = np.sort(27 * rand.rand(ndata)) + y = 1 + 0.001 * rand.randn(ndata) + dy = 0.001 * np.ones(ndata) + results = tls_search_gpu(t, y, dy, + periods=np.linspace(2.0, 5.0, 50)) + assert np.isfinite(results['chi2_min']) class TestFailedPeriodMasking: @@ -651,8 +666,12 @@ def test_stream_matches_default(self): dy = np.ones(400) * 0.001 periods = np.linspace(5, 15, 10) + # use_fast=False on both sides: this is a regression test for + # the LEGACY kernel's async D2H sequencing (the fast path does + # not take a user stream and would silently fall back to the + # legacy kernel anyway when one is passed) r_default = tls.tls_search_gpu(t, y, dy, periods=periods, - block_size=64) + block_size=64, use_fast=False) ensure_context() r_stream = tls.tls_search_gpu(t, y, dy, periods=periods, block_size=64, diff --git a/cuvarbase/tests/test_tls_fast.py b/cuvarbase/tests/test_tls_fast.py new file mode 100644 index 0000000..05eeed5 --- /dev/null +++ b/cuvarbase/tests/test_tls_fast.py @@ -0,0 +1,187 @@ +"""GPU tests for the fast (batched, phase-binned) TLS path. + +The fast path is the default for tls_search_gpu/tls_transit; these +tests cover what the legacy-oriented suites do not: batch consistency, +the coarse/refined statistics separation, adaptive binning, chunking, +and the removal of the legacy ndata cap. +""" +import numpy as np +import pytest + +try: + import pycuda.driver # noqa: F401 + PYCUDA_AVAILABLE = True +except Exception: + PYCUDA_AVAILABLE = False + +pytestmark = pytest.mark.skipif(not PYCUDA_AVAILABLE, + reason="pycuda unavailable") + + +def make_transit_lc(period, q, depth, ndata=1500, baseline=27.0, + noise=2e-3, seed=42, t0_frac=0.3): + rng = np.random.RandomState(seed) + t = np.sort(rng.uniform(0, baseline, ndata)) + y = 1.0 + rng.randn(ndata) * noise + t0 = t0_frac * period + rel = np.abs(((t - t0 + 0.5 * period) % period) - 0.5 * period) + y[rel < 0.5 * q * period] -= depth + dy = np.full(ndata, noise) + return t, y, dy + + +def shared_grid(baseline=27.0, period_min=1.0, period_max=12.0): + from cuvarbase import tls_grids + t_ref = np.linspace(0, baseline, 500) + return tls_grids.period_grid_ofir( + t_ref, R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=period_min, period_max=period_max) + + +class TestBatchConsistency: + def test_batch_matches_single(self): + from cuvarbase import tls + periods = shared_grid() + lcs = [make_transit_lc(3.3, 0.03, 0.012, seed=1), + make_transit_lc(7.7, 0.02, 0.012, ndata=2500, seed=2)] + batch = tls.tls_search_batch(lcs, periods=periods) + singles = [tls.tls_search_batch([lc], periods=periods)[0] + for lc in lcs] + for b, s in zip(batch, singles): + # atomics make near-tied neighbors non-deterministic; + # a few grid steps of slack + assert abs(b['period'] - s['period']) / s['period'] < 5e-3 + assert abs(b['SDE'] - s['SDE']) < 1.0 + + def test_recovers_injected_periods(self): + from cuvarbase import tls + periods = shared_grid() + p_injs = [3.3, 7.7] + lcs = [make_transit_lc(p, 0.03, 0.012, seed=10 + i) + for i, p in enumerate(p_injs)] + results = tls.tls_search_batch(lcs, periods=periods) + for r, p in zip(results, p_injs): + assert abs(r['period'] - p) / p < 0.01 + assert r['SDE'] > 5 + + def test_noise_lc_scores_below_signal(self): + from cuvarbase import tls + periods = shared_grid() + rng = np.random.RandomState(3) + t = np.sort(rng.uniform(0, 27.0, 1500)) + noise_lc = (t, 1.0 + 2e-3 * rng.randn(1500), + np.full(1500, 2e-3)) + sig_lc = make_transit_lc(3.3, 0.03, 0.012, seed=4) + r_noise, r_sig = tls.tls_search_batch([noise_lc, sig_lc], + periods=periods) + assert r_noise['SDE'] < r_sig['SDE'] + + +class TestStatisticsSeparation: + def test_spectrum_is_coarse_and_uniform(self): + """Refinement must not touch the per-period spectrum: SDE + computed with refine on and off must agree.""" + from cuvarbase import tls + periods = shared_grid() + lc = make_transit_lc(3.3, 0.03, 0.012, seed=5) + r_ref = tls.tls_search_batch([lc], periods=periods, + refine_top_k=200, + return_arrays=True)[0] + r_none = tls.tls_search_batch([lc], periods=periods, + refine_top_k=0, + return_arrays=True)[0] + ok = (np.isfinite(r_ref['chi2']) & np.isfinite(r_none['chi2'])) + np.testing.assert_allclose(r_ref['chi2'][ok], + r_none['chi2'][ok], rtol=1e-2) + assert abs(r_ref['SDE'] - r_none['SDE']) < 0.5 + + def test_refined_chi2_min_not_above_coarse(self): + """The exact refinement searches a finer local grid around the + coarse optimum, so the reported chi2_min should be at or below + the coarse spectrum minimum (up to float noise).""" + from cuvarbase import tls + periods = shared_grid() + lc = make_transit_lc(3.3, 0.03, 0.012, seed=6) + r = tls.tls_search_batch([lc], periods=periods, + return_arrays=True)[0] + coarse_min = np.nanmin(r['chi2']) + assert r['chi2_min'] <= coarse_min * (1 + 1e-3) + + +class TestScalability: + def test_ndata_beyond_legacy_cap(self): + from cuvarbase import tls + periods = shared_grid() + lc = make_transit_lc(4.56, 0.025, 0.008, ndata=20000, seed=7) + r = tls.tls_search_batch([lc], periods=periods)[0] + assert abs(r['period'] - 4.56) / 4.56 < 0.01 + + def test_bjd_scale_times(self): + from cuvarbase import tls + periods = shared_grid() + t, y, dy = make_transit_lc(4.56, 0.025, 0.008, ndata=5000, + seed=8) + r = tls.tls_search_batch([(t + 2457000.0, y, dy)], + periods=periods)[0] + assert abs(r['period'] - 4.56) / 4.56 < 0.01 + # T0 reported near the (shifted) epoch + assert r['T0'] >= 2457000.0 + assert r['T0'] <= 2457000.0 + 27.0 + r['period'] + + def test_chunking_many_small_lcs(self): + """Force multiple chunks via the LC-count ceiling and check + every LC still gets a result.""" + from cuvarbase import tls + periods = shared_grid() + old = tls._TLS_FAST_MAX_OUT_FLOATS + tls._TLS_FAST_MAX_OUT_FLOATS = 3 * len(periods) # 3 LCs/chunk + try: + lcs = [make_transit_lc(3.3, 0.03, 0.012, ndata=400, + seed=20 + i) for i in range(8)] + results = tls.tls_search_batch(lcs, periods=periods) + finally: + tls._TLS_FAST_MAX_OUT_FLOATS = old + assert len(results) == 8 + for r in results: + assert 'error' not in r + assert abs(r['period'] - 3.3) / 3.3 < 0.02 + + def test_mixed_lengths_offsets(self): + from cuvarbase import tls + periods = shared_grid() + lcs = [make_transit_lc(3.3, 0.03, 0.015, ndata=n, seed=30 + i) + for i, n in enumerate((300, 4000, 1100))] + results = tls.tls_search_batch(lcs, periods=periods) + for r in results: + assert abs(r['period'] - 3.3) / 3.3 < 0.02 + + +class TestValidation: + def test_empty_batch(self): + from cuvarbase import tls + assert tls.tls_search_batch([]) == [] + + def test_mismatched_qmin_qmax(self): + from cuvarbase import tls + lc = make_transit_lc(3.3, 0.03, 0.012, ndata=300) + with pytest.raises(ValueError): + tls.tls_search_batch([lc], periods=np.linspace(2, 5, 50), + qmin=np.full(10, 0.01), + qmax=np.full(10, 0.05)) + + def test_qmin_only_rejected(self): + from cuvarbase import tls + lc = make_transit_lc(3.3, 0.03, 0.012, ndata=300) + with pytest.raises(ValueError, match="both qmin and qmax"): + tls.tls_search_batch([lc], periods=np.linspace(2, 5, 50), + qmin=np.full(50, 0.01)) + + def test_bad_n_durations(self): + from cuvarbase import tls + lc = make_transit_lc(3.3, 0.03, 0.012, ndata=300) + with pytest.raises(ValueError, match="n_durations"): + tls.tls_search_batch([lc], n_durations=100) + + +if __name__ == '__main__': + pytest.main([__file__, '-v']) diff --git a/cuvarbase/tls.py b/cuvarbase/tls.py index e50dce6..65b4fc9 100644 --- a/cuvarbase/tls.py +++ b/cuvarbase/tls.py @@ -10,18 +10,19 @@ .. [2] Kovács et al. (2002), "Box Least Squares", A&A 391, 369 """ +import os import sys import threading import warnings from collections import OrderedDict +from concurrent.futures import ThreadPoolExecutor warnings.warn( "cuvarbase.tls is EXPERIMENTAL and not recommended for science use " - "in this release. The epoch (t0) grid is now duration-scaled and " - "failed periods are masked from the statistics, but the rework has " - "not yet been validated against the reference transitleastsquares " - "package. Light curves with more than ~3,500 points exceed the " - "kernel's shared-memory budget (a ValueError is raised). See " + "in this release. The default fast path (use_fast=True) is a " + "phase-binned scan with exact top-K refinement and supports " + "arbitrary ndata; the legacy kernel (use_fast=False) caps light " + "curves at ~3,500 points (a ValueError is raised). See " "analysis/V1_AUDIT_AND_GAMEPLAN.md in the repository. For validated " "transit searches use cuvarbase.bls (eebls_transit).", UserWarning) @@ -438,6 +439,8 @@ def tls_search_gpu(t, y, dy, periods=None, durations=None, block_size=None, t0_oversample=3.0, kernel=None, memory=None, stream=None, transfer_to_device=True, transfer_to_host=True, + use_fast=True, refine_top_k=50, + refine_oversample=33.0, nbins=None, **kwargs): """ Run Transit Least Squares search on GPU. @@ -533,22 +536,103 @@ def tls_search_gpu(t, y, dy, periods=None, durations=None, n_transits_min=n_transits_min ) + # The fast path keeps t in float64 for epoch subtraction; only the + # legacy path (below) downcasts inputs to float32 up front. + periods = np.asarray(periods, dtype=np.float32) + nperiods = len(periods) + + # Determine if using Keplerian mode + use_keplerian = (qmin is not None and qmax is not None) + + # Fast path: phase-binned batch engine with exact top-K refinement. + # Falls through to the legacy per-point kernel when the caller uses + # the low-level plumbing (pre-compiled kernel, external memory or + # stream, or transfer control), which the batch engine does not + # expose. + fast_gate = (kernel is None and memory is None and stream is None + and transfer_to_device and transfer_to_host) + if use_fast and not fast_gate: + warnings.warn( + "use_fast=True is ignored because a pre-compiled kernel, " + "external memory/stream, or transfer control was supplied; " + "falling back to the legacy per-point kernel (which caps " + "ndata at ~3,500 points)") + if use_fast and fast_gate: + if use_keplerian: + qmin_arr = np.asarray(qmin, dtype=np.float64) + qmax_arr = np.asarray(qmax, dtype=np.float64) + if len(qmin_arr) != nperiods or len(qmax_arr) != nperiods: + raise ValueError( + "qmin and qmax must have same length as periods " + "(%d)" % nperiods) + n_durations_eff = n_durations + else: + # match the legacy standard kernel exactly: fixed duration + # range AND its hard-coded 15 durations (the legacy kernel + # ignores n_durations outside Keplerian mode) + qmin_arr = np.full(nperiods, 0.005) + qmax_arr = np.full(nperiods, 0.15) + if n_durations != 15: + warnings.warn( + "n_durations is only honored in Keplerian mode " + "(qmin/qmax provided); the standard TLS duration " + "grid is fixed at 15 log-spaced durations") + n_durations_eff = 15 + + batch_results = tls_search_batch( + [(t, y, dy)], + periods=periods, qmin=qmin_arr, qmax=qmax_arr, + n_durations=n_durations_eff, t0_oversample=t0_oversample, + refine_top_k=refine_top_k, + refine_oversample=refine_oversample, + block_size=block_size, nbins=nbins, + limb_dark=limb_dark, u=u, + R_star=R_star, M_star=M_star, + return_arrays=True, + _warn_failed=True) + r = batch_results[0] + if 'error' in r: + raise RuntimeError(r['error']) + + # legacy result dict ('T0' is the transit phase, as before) + return { + 'periods': periods, + 'chi2': r['chi2'], + 'best_t0_per_period': r['best_t0_per_period'], + 'best_duration_per_period': r['best_duration_per_period'], + 'best_depth_per_period': r['best_depth_per_period'], + 'valid_periods': r['valid_periods'], + 'n_failed_periods': r['n_failed_periods'], + 'period': r['period'], + 'period_uncertainty': r['period_uncertainty'], + 'T0': r['t0_phase'], + 'duration': r['duration'], + 'depth': r['depth'], + 'chi2_min': r['chi2_min'], + 'SDE': r['SDE'], + 'SDE_raw': r['SDE_raw'], + 'SNR': r['SNR'], + 'FAP': r['FAP'], + 'power': r['power'], + 'SR': r['SR'], + 'n_transits': r['n_transits'], + 'R_star': R_star, + 'M_star': M_star, + } + + # ---- Legacy per-point kernel path ---- + # Convert to numpy arrays t = np.asarray(t, dtype=np.float32) y = np.asarray(y, dtype=np.float32) dy = np.asarray(dy, dtype=np.float32) - periods = np.asarray(periods, dtype=np.float32) ndata = len(t) - nperiods = len(periods) # Choose block size if block_size is None: block_size = _choose_block_size(ndata) - # Determine if using Keplerian mode - use_keplerian = (qmin is not None and qmax is not None) - # Shared-memory budget check BEFORE compiling kernels or touching # the GPU. Layout: phases[ndata] + y_sorted[ndata] + # dy_sorted[ndata] + template[n_template] + 4 thread arrays of @@ -865,3 +949,667 @@ def tls_transit(t, y, dy, R_star=1.0, M_star=1.0, R_planet=1.0, ) return results + + +# ===================================================================== +# Fast batch TLS engine (phase-binned scan + exact top-K refinement) +# ===================================================================== +# +# One kernel launch searches a whole batch of lightcurves over a shared +# period grid: grid = (nperiods, n_lightcurves), one block per +# (lightcurve, period). Each block folds its lightcurve once into +# shared-memory phase bins and scans every (duration, t0) trial against +# the bins, so trial cost is independent of ndata and there is no +# shared-memory cap on the lightcurve length. A second, exact kernel +# then re-fits the best `refine_top_k` candidate periods per lightcurve +# with per-point template evaluation on a finer local (duration, t0) +# grid. See kernels/tls_fast.cu for the algorithm notes. + +_TLS_FAST_NTEMPLATE = 1024 +_TLS_FAST_MAX_DURATIONS = 64 +_TLS_FAST_MAX_NBINS = 8192 +_TLS_FAST_DEFAULT_BLOCK = 256 + +# Chunking budgets (per kernel launch) +_TLS_FAST_MAX_OUT_FLOATS = 32 * 1024 * 1024 # per output array +_TLS_FAST_MAX_POINTS = 16 * 1024 * 1024 # concatenated data points +_TLS_FAST_MAX_GRID_Y = 65535 + + +def _next_pow2(n): + p = 1 + while p < n: + p *= 2 + return p + + +def _device_max_shared(): + """Max opt-in dynamic shared memory per block on the current device.""" + ensure_context() + dev = cuda.Context.get_device() + try: + return dev.get_attribute( + cuda.device_attribute.MAX_SHARED_MEMORY_PER_BLOCK_OPTIN) + except Exception: + return dev.get_attribute( + cuda.device_attribute.MAX_SHARED_MEMORY_PER_BLOCK) + + +def _tls_fast_shared_size(block_size, nbins): + """Dynamic shared memory (bytes) for tls_fast_search_kernel.""" + nt = _TLS_FAST_NTEMPLATE + md = _TLS_FAST_MAX_DURATIONS + n_floats = 2 * nbins + 2 * (nt + 1) + 4 * block_size + md + n_ints = md + 1 + return 4 * (n_floats + n_ints) + + +def _tls_refine_shared_size(block_size): + """Dynamic shared memory (bytes) for tls_refine_kernel.""" + return 4 * ((_TLS_FAST_NTEMPLATE + 1) + 4 * (block_size // 32)) + + +def _auto_nbins(qmin_global, t0_oversample, block_size): + """Pick the phase-bin count: bin width <= qmin/t0_oversample, power + of two, bounded by the device's shared-memory limit.""" + need = t0_oversample / max(float(qmin_global), 1e-6) + nbins = _next_pow2(int(np.ceil(need))) + nbins = max(256, min(nbins, _TLS_FAST_MAX_NBINS)) + max_shared = _device_max_shared() + while nbins > 256 and _tls_fast_shared_size(block_size, nbins) > max_shared: + nbins //= 2 + if nbins < need: + warnings.warn( + "TLS fast path: %d phase bins under-resolve the narrowest " + "trial duration (q=%.2e wants %d bins); the coarse scan is " + "smeared there and recovery relies on the exact refinement " + "pass (refine_top_k)." % (nbins, qmin_global, + int(np.ceil(need)))) + return nbins + + +def compile_tls_fast(block_size=_TLS_FAST_DEFAULT_BLOCK, nbins=2048, + t0_oversample=3.0, refine_nd=3): + """ + Compile the fast (batched, phase-binned) TLS kernels. + + Parameters + ---------- + block_size : int + CUDA block size (multiple of 32). + nbins : int + Number of phase bins (power of two). + t0_oversample : float + Epoch oversampling: t0 stride = duration / t0_oversample in the + coarse scan (same convention as the legacy kernels). + refine_nd : int + Number of local durations in the refinement kernel (odd; + default 3 spans one coarse duration-grid step each way). + + Returns + ------- + kernels : dict + {'search': ..., 'refine': ...} PyCUDA functions. + """ + ensure_context() + if block_size < 32 or (block_size & (block_size - 1)): + # the block max-reduction assumes a power-of-two blockDim + raise ValueError("block_size must be a power of two >= 32") + if nbins & (nbins - 1): + raise ValueError("nbins must be a power of two") + if int(refine_nd) != refine_nd or refine_nd < 2: + raise ValueError("refine_nd must be an integer >= 2 " + "(odd recommended so the coarse duration sits " + "on the refinement grid)") + + cppd = dict(BLOCK_SIZE=block_size, + NBINS=nbins, + NTEMPLATE=_TLS_FAST_NTEMPLATE, + MAX_DURATIONS=_TLS_FAST_MAX_DURATIONS, + T0_OVERSAMPLE=float(t0_oversample), + REFINE_ND=refine_nd) + kernel_txt = _module_reader(find_kernel('tls_fast'), cpp_defs=cppd) + module = SourceModule(kernel_txt, options=['--use_fast_math'], + no_extern_c=True) + search = module.get_function('tls_fast_search_kernel') + refine = module.get_function('tls_refine_kernel') + + smem = _tls_fast_shared_size(block_size, nbins) + if smem > _SHARED_MEM_LIMIT: + max_shared = _device_max_shared() + if smem > max_shared: + raise ValueError( + "TLS fast kernel wants %d bytes of shared memory per " + "block but the device caps at %d; reduce nbins (or " + "block_size)" % (smem, max_shared)) + # opt in to >48KB dynamic shared memory (sm_70+) + search.set_attribute( + cuda.function_attribute.MAX_DYNAMIC_SHARED_SIZE_BYTES, smem) + + return {'search': search, 'refine': refine} + + +def _get_cached_fast_kernels(block_size, nbins, t0_oversample, + refine_nd=3): + key = ('fast', block_size, nbins, float(t0_oversample), refine_nd) + with _kernel_cache_lock: + if key in _kernel_cache: + _kernel_cache.move_to_end(key) + return _kernel_cache[key] + compiled = compile_tls_fast(block_size=block_size, nbins=nbins, + t0_oversample=t0_oversample, + refine_nd=refine_nd) + _kernel_cache[key] = compiled + _kernel_cache.move_to_end(key) + if len(_kernel_cache) > _KERNEL_CACHE_MAX_SIZE: + _kernel_cache.popitem(last=False) + return compiled + + +def _preprocess_batch(lightcurves): + """Epoch-subtract, weight, and concatenate lightcurves (float64 + accumulation; times stored as a float-float hi/lo pair so the + kernels can fold at ~float64 precision with pure FP32 math). + + Returns (t_hi, t_lo, a_c, b_c, offs, lens, chi2_0, epochs, spans); + chi2_0 stays float64 for cancellation-free chi2 reconstruction. + """ + n_lc = len(lightcurves) + lens = np.array([len(lc[0]) for lc in lightcurves], dtype=np.int64) + for i, (lc, n) in enumerate(zip(lightcurves, lens)): + if n == 0: + raise ValueError("lightcurve %d is empty" % i) + if len(lc[1]) != n or len(lc[2]) != n: + raise ValueError( + "lightcurve %d: t, y, dy lengths differ (%d, %d, %d)" + % (i, n, len(lc[1]), len(lc[2]))) + # batch-wide offsets in int64 (a large survey can exceed 2^31 + # total points); per-chunk offsets are rebased and cast to int32 + # at upload, where the chunk-size cap keeps them small + offs = np.zeros(n_lc, dtype=np.int64) + if n_lc > 1: + offs[1:] = np.cumsum(lens)[:-1] + total = int(lens.sum()) + + t_hi = np.empty(total, dtype=np.float32) + t_lo = np.empty(total, dtype=np.float32) + a_c = np.empty(total, dtype=np.float32) + b_c = np.empty(total, dtype=np.float32) + chi2_0 = np.empty(n_lc, dtype=np.float64) + epochs = np.empty(n_lc, dtype=np.float64) + spans = np.empty(n_lc, dtype=np.float64) + + for i, (t, y, dy) in enumerate(lightcurves): + t64 = np.asarray(t, dtype=np.float64) + y64 = np.asarray(y, dtype=np.float64) + dy64 = np.asarray(dy, dtype=np.float64) + epoch = np.floor(t64.min()) + # sigma^2 regularizer matches the legacy kernel (float32 dy) + s2 = dy64 * dy64 + 1e-10 + o, n = int(offs[i]), int(lens[i]) + tshift = t64 - epoch + hi = tshift.astype(np.float32) + t_hi[o:o + n] = hi + t_lo[o:o + n] = (tshift - hi.astype(np.float64)).astype(np.float32) + resid = 1.0 - y64 + a_c[o:o + n] = resid / s2 + b_c[o:o + n] = 1.0 / s2 + chi2_0[i] = np.sum(resid * resid / s2) + epochs[i] = epoch + spans[i] = t64.max() - t64.min() + + return t_hi, t_lo, a_c, b_c, offs, lens, chi2_0, epochs, spans + + +def tls_search_batch(lightcurves, R_star=1.0, M_star=1.0, R_planet=1.0, + periods=None, qmin=None, qmax=None, + period_min=None, period_max=None, + n_transits_min=2, oversampling_factor=3, + qmin_fac=0.5, qmax_fac=2.0, n_durations=15, + t0_oversample=3.0, + refine_top_k=50, refine_oversample=33.0, + block_size=None, nbins=None, + limb_dark='quadratic', u=[0.4804, 0.1867], + return_arrays=False, sde_kernel_size=None, + _warn_failed=False): + """ + Survey-scale Transit Least Squares search over a batch of + lightcurves sharing one trial-period grid. + + This is the fast path for N >> 1 lightcurves: a single kernel + launch (per chunk) searches every (lightcurve, period) pair with a + phase-binned scan, then an exact per-point refinement kernel + re-fits the ``refine_top_k`` best candidate periods per lightcurve + on a finer local (duration, t0) grid. There is no cap on ndata. + + Parameters + ---------- + lightcurves : list of (t, y, dy) tuples + Times (days), fluxes (normalized to a baseline of 1.0), and + flux uncertainties. Each lightcurve's epoch floor(min(t)) is + subtracted internally (float64), so BJD-scale times are safe. + R_star, M_star : float + Stellar radius/mass in solar units; set the period grid and the + Keplerian duration window (shared by all lightcurves). + R_planet : float + Fiducial planet radius (Earth radii) for the duration window. + periods, qmin, qmax : array_like, optional + Explicit trial grid: periods (days) and per-period fractional + duration bounds. Auto-generated (Ofir 2014 grid + Keplerian + durations) when omitted. + period_min, period_max : float, optional + Period search range for the auto grid. + n_transits_min, oversampling_factor : optional + Auto period-grid parameters (see tls_grids.period_grid_ofir). + qmin_fac, qmax_fac : float + Keplerian duration window factors (search [qmin_fac*q, + qmax_fac*q] at each period). + n_durations : int + Trial durations per period (log-spaced), max 64. + t0_oversample : float + Coarse epoch oversampling; t0 stride = duration / t0_oversample. + refine_top_k : int + Number of best candidate periods per lightcurve re-fit exactly + (default 50; 0 disables refinement). + refine_oversample : float + Refinement epoch stride = duration / refine_oversample (the + reference transitleastsquares package uses ~100). + block_size : int, optional + CUDA block size override (power of two). By default each + bin-count band picks its own (256, or 512 for bands with 4096+ + bins, shrunk to fit the device's shared-memory cap). + nbins : int, optional + Phase bins (power of two). Auto-sized so a bin is no wider than + the narrowest trial duration / t0_oversample, within the + device's shared-memory limit. + limb_dark, u : optional + Limb-darkening law/coefficients for the transit template. + return_arrays : bool + Also return the per-period chi2/t0/duration/depth arrays and + derived spectra for each lightcurve (adds D2H transfer time). + sde_kernel_size : int, optional + Median-detrend window for the SDE statistic (see tls_stats). + + Returns + ------- + results : list of dict + One dict per lightcurve: + 'period', 'period_uncertainty', 't0_phase', 'T0' (absolute + mid-transit time near the epoch), 'duration', 'depth', + 'chi2_min', 'SDE', 'SDE_raw', 'SNR', 'FAP', 'n_transits', + 'n_failed_periods'; plus the per-period arrays when + ``return_arrays`` is set. A lightcurve whose every trial period + failed gets {'error': message} instead. + + The best-fit parameters (including 'chi2_min') come from the + exact refinement pass, so 'chi2_min' is generally slightly + below the minimum of the returned coarse 'chi2' spectrum; the + SDE/FAP statistics are computed from the uniform coarse + spectrum only, keeping the detection statistic's scale + consistent across periods. + """ + tls_grids.validate_stellar_parameters(R_star, M_star) + tls_models.validate_limb_darkening_coeffs(u, limb_dark) + + if len(lightcurves) == 0: + return [] + if n_durations < 2 or n_durations > _TLS_FAST_MAX_DURATIONS: + raise ValueError("n_durations must be in [2, %d]" % + _TLS_FAST_MAX_DURATIONS) + if refine_top_k is not None and refine_top_k < 0: + raise ValueError("refine_top_k must be >= 0 (got %r)" + % (refine_top_k,)) + if refine_top_k and not refine_oversample > 0: + raise ValueError("refine_oversample must be > 0 (got %r)" + % (refine_oversample,)) + + # ---- Trial grid (shared across the batch) ---- + if periods is None: + # build the grid from the longest lightcurve baseline + spans_probe = [np.max(lc[0]) - np.min(lc[0]) for lc in lightcurves] + t_ref = lightcurves[int(np.argmax(spans_probe))][0] + periods = tls_grids.period_grid_ofir( + t_ref, R_star=R_star, M_star=M_star, + oversampling_factor=oversampling_factor, + period_min=period_min, period_max=period_max, + n_transits_min=n_transits_min) + periods = np.asarray(periods, dtype=np.float32) + nperiods = len(periods) + if nperiods == 0: + raise ValueError("periods must be non-empty") + + if (qmin is None) != (qmax is None): + raise ValueError("provide both qmin and qmax, or neither") + if qmin is None: + # only the q bounds are needed here; skip building the + # (nperiods x n_durations) duration table + q_values = tls_grids.q_transit(periods.astype(np.float64), + R_star=R_star, M_star=M_star, + R_planet=R_planet) + qmin = q_values * qmin_fac + qmax = q_values * qmax_fac + qmin = np.ascontiguousarray(qmin, dtype=np.float32) + qmax = np.ascontiguousarray(qmax, dtype=np.float32) + if len(qmin) != nperiods or len(qmax) != nperiods: + raise ValueError("qmin and qmax must have same length as periods " + "(%d)" % nperiods) + if np.any(qmin <= 0) or np.any(qmax < qmin) or np.any(qmax >= 1): + raise ValueError( + "need 0 < qmin <= qmax < 1 at every period (the transit " + "duration must be shorter than the period; the binned scan " + "would double-count phase bins for q >= 1)") + + # ---- Kernel configuration: band the grid by required bin count. + # The trial-scan cost is proportional to NBINS, while the bin count + # a period actually needs scales with 1/qmin at that period, so + # running the whole grid at the finest band's NBINS overpays by 2x+ + # on long-baseline searches. Each band compiles (and caches) its + # own NBINS variant and scatters results through period_map. ---- + qmin_global = float(np.min(qmin)) + max_dev_shared = _device_max_shared() + ensure_context() + cc_major = cuda.Context.get_device().compute_capability()[0] + + def _band_block_size(nb): + if block_size is not None: + return block_size + # Swept on RTX A5000 (sm_86), RTX 4000 Ada (sm_89) and Tesla + # V100 (sm_70), kepler-4yr config with the float-float fold: + # 256 beats 128 everywhere; 512 wins on the big-bin bands on + # Ampere/Ada from 4096 bins up, while Volta prefers 256 until + # shared memory forces one block per SM (8192 bins). + # On devices with a hard 48KB cap (no opt-in; Pascal and + # earlier) prefer shrinking the block over losing phase bins. + big_bin_threshold = 4096 if cc_major >= 8 else 8192 + bs = 512 if nb >= big_bin_threshold else 256 + while bs > 64 and _tls_fast_shared_size(bs, nb) > max_dev_shared: + bs //= 2 + return bs + + need = t0_oversample / np.maximum(qmin.astype(np.float64), 1e-6) + if nbins is None: + nbins_per = np.power( + 2, np.ceil(np.log2(np.clip(need, 256, None)))).astype(np.int64) + nbins_per = np.minimum(nbins_per, _TLS_FAST_MAX_NBINS) + # shared-memory cap for this device + while _tls_fast_shared_size( + _band_block_size(int(nbins_per.max())), + int(nbins_per.max())) > max_dev_shared: + cap = int(nbins_per.max()) // 2 + nbins_per = np.minimum(nbins_per, cap) + if cap <= 256: + break + short = need > nbins_per + if np.any(short): + warnings.warn( + "TLS fast path: %d of %d trial periods have their " + "narrowest durations under-resolved by the phase bins " + "(device shared-memory cap); their coarse scan is " + "smeared and recovery there relies on the exact " + "refinement pass." % (int(short.sum()), nperiods)) + bands = [(int(nb), np.flatnonzero(nbins_per == nb).astype(np.int32)) + for nb in np.unique(nbins_per)] + smear = float(np.max(need / nbins_per)) + else: + bands = [(int(nbins), np.arange(nperiods, dtype=np.int32))] + smear = float(np.max(need / nbins)) + + # When the coarse bins under-resolve a duration (smear > 1), the + # coarse best duration is biased wide by the bin convolution; + # widen the refinement's duration window accordingly and use more + # local durations so the true value stays inside it. + smear = max(1.0, smear) + refine_nd = 3 if smear <= 1.3 else 5 + + band_launches = [] # (kernels, block_size, smem, n, per_g, qmn_g, qmx_g, map_g) + for nb, idx in bands: + bs = _band_block_size(nb) + kern = _get_cached_fast_kernels(bs, nb, t0_oversample, + refine_nd=refine_nd) + band_launches.append(( + kern, bs, _tls_fast_shared_size(bs, nb), len(idx), + gpuarray.to_gpu(periods[idx]), + gpuarray.to_gpu(qmin[idx]), + gpuarray.to_gpu(qmax[idx]), + gpuarray.to_gpu(idx))) + + # refinement runs at the first band's block size (any variant works) + refine_bs = _band_block_size(bands[0][0]) + refine_kern = band_launches[0][0] + refine_smem = _tls_refine_shared_size(refine_bs) + + # refinement trial-grid shape (see kernels/tls_fast.cu). The t0 + # halfwidth must cover the worst coarse quantization, which lives + # in the FINEST band if the device cap clamped it below its need. + dur_ratio = float(np.median(qmax / qmin)) + dur_span = dur_ratio ** (1.0 / (2.0 * max(n_durations - 1, 1))) + dur_span *= min(smear, 4.0) + nbins_finest = bands[-1][0] + t0_halfwidth = min(3.0, max(0.5, 1.5 / (nbins_finest * qmin_global))) + + # ---- Template tables ---- + T_tab, S1_tab, S2_tab = tls_models.generate_template_tables( + n_table=_TLS_FAST_NTEMPLATE, limb_dark=limb_dark, u=u) + + # ---- Host preprocessing ---- + t_hi_c, t_lo_c, a_c, b_c, offs, lens, chi2_0, epochs, spans = \ + _preprocess_batch(lightcurves) + n_lc = len(lightcurves) + + # ---- Static GPU arrays ---- + periods_g = gpuarray.to_gpu(periods) + T_g = gpuarray.to_gpu(T_tab) + S1_g = gpuarray.to_gpu(S1_tab) + S2_g = gpuarray.to_gpu(S2_tab) + + # ---- Chunk plan: bound output size, data size, and grid.y ---- + max_lcs_by_out = max(1, _TLS_FAST_MAX_OUT_FLOATS // max(nperiods, 1)) + chunks = [] # list of (i0, i1) + i0 = 0 + while i0 < n_lc: + i1 = i0 + 1 + pts = int(lens[i0]) + while (i1 < n_lc + and i1 - i0 < max_lcs_by_out + and i1 - i0 < _TLS_FAST_MAX_GRID_Y + and pts + int(lens[i1]) <= _TLS_FAST_MAX_POINTS): + pts += int(lens[i1]) + i1 += 1 + chunks.append((i0, i1)) + i0 = i1 + + max_chunk_lcs = max(i1 - i0 for i0, i1 in chunks) + max_chunk_pts = max(int(lens[i0:i1].sum()) for i0, i1 in chunks) + + # reusable per-chunk GPU buffers + thi_g = gpuarray.empty(max_chunk_pts, np.float32) + tlo_g = gpuarray.empty(max_chunk_pts, np.float32) + a_g = gpuarray.empty(max_chunk_pts, np.float32) + b_g = gpuarray.empty(max_chunk_pts, np.float32) + off_g = gpuarray.empty(max_chunk_lcs, np.int32) + len_g = gpuarray.empty(max_chunk_lcs, np.int32) + out_n = max_chunk_lcs * nperiods + score_g = gpuarray.empty(out_n, np.float32) + t0_g = gpuarray.empty(out_n, np.float32) + dur_g = gpuarray.empty(out_n, np.float32) + depth_g = gpuarray.empty(out_n, np.float32) + + # Refinement targets the peak region only: capping K at ~10% of the + # grid keeps the SDE background dominated by uniformly-treated + # (coarse) periods, so the refined peak stands out the same way it + # would in a full-fidelity spectrum. + K = int(min(refine_top_k, max(16, nperiods // 10), + nperiods)) if refine_top_k else 0 + if K: + cand_g = gpuarray.empty(max_chunk_lcs * K, np.int32) + # compact refined outputs, one slot per candidate; the coarse + # spectrum is never overwritten (SDE needs uniform fidelity) + rscore_g = gpuarray.empty(max_chunk_lcs * K, np.float32) + rt0_g = gpuarray.empty(max_chunk_lcs * K, np.float32) + rdur_g = gpuarray.empty(max_chunk_lcs * K, np.float32) + rdepth_g = gpuarray.empty(max_chunk_lcs * K, np.float32) + + results = [None] * n_lc + + for (i0, i1) in chunks: + nc = i1 - i0 + p0 = int(offs[i0]) + pts = int(lens[i0:i1].sum()) + + # H2D (chunk-relative offsets are bounded by the points cap, + # so the int32 cast is safe) + thi_g[:pts].set(t_hi_c[p0:p0 + pts]) + tlo_g[:pts].set(t_lo_c[p0:p0 + pts]) + a_g[:pts].set(a_c[p0:p0 + pts]) + b_g[:pts].set(b_c[p0:p0 + pts]) + off_g[:nc].set((offs[i0:i1] - p0).astype(np.int32)) + len_g[:nc].set(lens[i0:i1].astype(np.int32)) + + # coarse binned scan, one launch per bin-count band + for kern, bs, smem, band_n, per_g, qmn_g, qmx_g, map_g \ + in band_launches: + kern['search']( + thi_g, tlo_g, a_g, b_g, off_g, len_g, + per_g, qmn_g, qmx_g, map_g, S1_g, S2_g, + np.int32(band_n), np.int32(nperiods), + np.int32(n_durations), + score_g, t0_g, dur_g, depth_g, + block=(bs, 1, 1), grid=(band_n, nc, 1), + shared=smem) + + # score = chi2_0 - chi2 (cancellation-free); <= 0 marks failure + score_h = score_g[:nc * nperiods].get().reshape(nc, nperiods) + + # exact refinement of the best K candidate periods per LC + # (parameters only; the coarse spectrum feeds the statistics) + rscore_h = rt0_h = rdur_h = rdepth_h = cand = None + if K: + cand = np.empty((nc, K), dtype=np.int32) + for j in range(nc): + if K < nperiods: + # K largest scores = K smallest chi2; failed + # periods (score < 0) sort last automatically + cand[j] = np.argpartition(-score_h[j], K)[:K] + else: + cand[j] = np.arange(nperiods) + cand_g[:nc * K].set(cand.ravel()) + refine_kern['refine']( + thi_g, tlo_g, a_g, b_g, off_g, len_g, + periods_g, cand_g, T_g, + np.int32(nperiods), np.int32(K), + np.float32(dur_span), np.float32(t0_halfwidth), + np.float32(refine_oversample), + t0_g, dur_g, + rscore_g, rt0_g, rdur_g, rdepth_g, + block=(refine_bs, 1, 1), grid=(K, nc, 1), + shared=refine_smem) + rscore_h = rscore_g[:nc * K].get().reshape(nc, K) + rt0_h = rt0_g[:nc * K].get().reshape(nc, K) + rdur_h = rdur_g[:nc * K].get().reshape(nc, K) + rdepth_h = rdepth_g[:nc * K].get().reshape(nc, K) + + if return_arrays or not K: + t0_h = t0_g[:nc * nperiods].get().reshape(nc, nperiods) + dur_h = dur_g[:nc * nperiods].get().reshape(nc, nperiods) + depth_h = depth_g[:nc * nperiods].get().reshape(nc, nperiods) + + # ---- Per-LC statistics (pure CPU; threaded across the chunk, + # scipy/numpy release the GIL in the hot medfilt) ---- + def _finish_lc(j): + lc_idx = i0 + j + srow = score_h[j] + valid = srow > 0.0 + n_failed = int(nperiods - valid.sum()) + if n_failed == nperiods: + return lc_idx, { + 'error': "TLS kernel returned no valid solution for " + "any of the %d trial periods" % nperiods} + if n_failed and _warn_failed: + warnings.warn( + "%d of %d trial periods returned no valid TLS " + "solution (chi2 sentinel); they are excluded from " + "the best-fit search and the SDE/FAP statistics and " + "appear as NaN in the returned arrays" + % (n_failed, nperiods)) + + # chi2 reconstructed in float64 against the float64 chi2_0 + row = chi2_0[lc_idx] - srow.astype(np.float64) + chi2_valid = row[valid] + periods_valid = periods[valid] + + # Best-fit parameters come from the exact refinement pass + # when available; the coarse spectrum (row) is what feeds + # the SDE/FAP statistics either way. + slot = int(np.argmax(rscore_h[j])) if K else 0 + if K and rscore_h[j, slot] > 0.0: + best_idx = int(cand[j, slot]) + best_t0 = float(rt0_h[j, slot]) + best_duration = float(rdur_h[j, slot]) + best_depth = float(rdepth_h[j, slot]) + chi2_min = float(chi2_0[lc_idx] - rscore_h[j, slot]) + best_valid_idx = int(np.searchsorted( + np.flatnonzero(valid), best_idx)) + else: + best_valid_idx = int(np.argmin(chi2_valid)) + best_idx = int(np.flatnonzero(valid)[best_valid_idx]) + chi2_min = float(row[best_idx]) + best_t0 = float(t0_h[j, best_idx]) + best_duration = float(dur_h[j, best_idx]) + best_depth = float(depth_h[j, best_idx]) + + best_period = float(periods[best_idx]) + n_transits = int(spans[lc_idx] / best_period) + + stats = tls_stats.compute_all_statistics( + chi2_valid, periods_valid, best_valid_idx, + best_depth, best_duration, n_transits, + kernel_size=sde_kernel_size) + period_uncertainty = tls_stats.compute_period_uncertainty( + periods_valid, chi2_valid, best_valid_idx) + + res = { + 'period': best_period, + 'period_uncertainty': period_uncertainty, + 't0_phase': best_t0, + 'T0': epochs[lc_idx] + best_t0 * best_period, + 'duration': best_duration, + 'depth': best_depth, + 'chi2_min': chi2_min, + 'SDE': stats['SDE'], + 'SDE_raw': stats['SDE_raw'], + 'SNR': stats['SNR'], + 'FAP': stats['FAP'], + 'n_transits': n_transits, + 'n_failed_periods': n_failed, + } + if return_arrays: + def _expand(values): + full = np.full(nperiods, np.nan) + full[valid] = values + return full + res.update({ + 'periods': periods, + 'chi2': np.where(valid, row, np.nan), + 'best_t0_per_period': t0_h[j].copy(), + 'best_duration_per_period': dur_h[j].copy(), + 'best_depth_per_period': depth_h[j].copy(), + 'valid_periods': valid, + 'power': _expand(stats['power']), + 'SR': _expand(stats['SR']), + }) + return lc_idx, res + + if nc > 1: + n_workers = min(8, os.cpu_count() or 1, nc) + else: + n_workers = 1 + if n_workers > 1: + with ThreadPoolExecutor(max_workers=n_workers) as pool: + for lc_idx, res in pool.map(_finish_lc, range(nc)): + results[lc_idx] = res + else: + for j in range(nc): + lc_idx, res = _finish_lc(j) + results[lc_idx] = res + + return results diff --git a/cuvarbase/tls_grids.py b/cuvarbase/tls_grids.py index 4132851..5a4a7ee 100644 --- a/cuvarbase/tls_grids.py +++ b/cuvarbase/tls_grids.py @@ -339,20 +339,18 @@ def duration_grid_keplerian(periods, R_star=1.0, M_star=1.0, R_planet=1.0, qmin_vals = q_values * qmin_fac qmax_vals = q_values * qmax_fac - durations = [] duration_counts = np.full(len(periods), n_durations, dtype=np.int32) - for period, qmin, qmax in zip(periods, qmin_vals, qmax_vals): - # Logarithmically-spaced durations from qmin to qmax - # (in absolute time, not fractional) - dur_min = qmin * period - dur_max = qmax * period - - # Log-spaced grid - dur = np.logspace(np.log10(dur_min), np.log10(dur_max), - n_durations, dtype=np.float32) - - durations.append(dur) + # Logarithmically-spaced durations from qmin*P to qmax*P per period + # (absolute time, not fractional), vectorized over the whole grid: + # equivalent to np.logspace per period, but one broadcast instead of + # len(periods) Python-level calls (which dominate at ~1e5 periods). + log_min = np.log10(qmin_vals * periods) + log_max = np.log10(qmax_vals * periods) + frac = np.linspace(0.0, 1.0, n_durations) + dur_2d = 10.0 ** (log_min[:, None] + + (log_max - log_min)[:, None] * frac[None, :]) + durations = list(dur_2d.astype(np.float32)) return durations, duration_counts, q_values diff --git a/cuvarbase/tls_models.py b/cuvarbase/tls_models.py index 7a86c64..aad38d3 100644 --- a/cuvarbase/tls_models.py +++ b/cuvarbase/tls_models.py @@ -368,6 +368,62 @@ def generate_transit_template(n_template=1000, limb_dark='quadratic', return _trapezoid_template(n_template) +def generate_template_tables(n_table=1024, limb_dark='quadratic', + u=[0.4804, 0.1867], oversample=8): + """ + Generate the template lookup tables used by the fast TLS kernel. + + The fast kernel evaluates the transit template two ways: + + - The binned scan needs the template's *running integrals* so it can + compute the exact bin-averaged template over any transit-coordinate + interval (area sampling): ``S1(x) = int_{-1}^{x} T dx`` and + ``S2(x) = int_{-1}^{x} T^2 dx``. + - The refinement kernel needs the pointwise template ``T(x)`` itself. + + All three are tabulated on the same uniform grid of ``n_table + 1`` + knots spanning transit_coord in [-1, 1]. The integrals are computed + from a template oversampled by ``oversample`` relative to the knot + grid (trapezoid rule), so S1/S2 are accurate even where T is curved. + + Parameters + ---------- + n_table : int, optional + Number of table intervals; the returned arrays have + ``n_table + 1`` entries (default: 1024). + limb_dark : str, optional + Limb darkening law (default: 'quadratic') + u : list, optional + Limb darkening coefficients (default: [0.4804, 0.1867]) + oversample : int, optional + Oversampling of the integrand relative to the knot grid. + + Returns + ------- + T, S1, S2 : ndarray + Float32 arrays of shape (n_table + 1,). + """ + n_fine = n_table * oversample + fine = generate_transit_template(n_template=n_fine + 1, + limb_dark=limb_dark, u=u) + fine = np.asarray(fine, dtype=np.float64) + dx = 2.0 / n_fine + + def running_integral(values): + # cumulative trapezoid on the fine grid, then subsample to knots + cum = np.concatenate([ + [0.0], np.cumsum(0.5 * (values[1:] + values[:-1]) * dx)]) + return cum[::oversample] + + S1 = running_integral(fine) + S2 = running_integral(fine ** 2) + T = fine[::oversample] + + return (T.astype(np.float32), + S1.astype(np.float32), + S2.astype(np.float32)) + + def _trapezoid_template(n_template=1000, ingress_fraction=0.1): """ Generate a trapezoidal transit template as fallback. diff --git a/cuvarbase/tls_stats.py b/cuvarbase/tls_stats.py index 9dda6b2..3d9b61c 100644 --- a/cuvarbase/tls_stats.py +++ b/cuvarbase/tls_stats.py @@ -49,7 +49,7 @@ def signal_residue(chi2, chi2_null=None): def signal_detection_efficiency(chi2, chi2_null=None, detrend=True, - window_length=None): + kernel_size=None, window_length=None): """ Calculate Signal Detection Efficiency (SDE). @@ -64,8 +64,18 @@ def signal_detection_efficiency(chi2, chi2_null=None, detrend=True, Null hypothesis chi-squared detrend : bool, optional Apply median filter detrending (default: True) + kernel_size : int, optional + Running-median kernel size for detrending. If None (default), + uses ``min(len(SR)//10 forced odd (min 3), 91)``: small period + grids keep the length-proportional window, while large grids + are capped at 91 points -- the fixed-kernel convention of the + reference ``transitleastsquares`` package (oversampling factor + 3 x SDE_MEDIAN_KERNEL_SIZE 30, forced odd). Passing an explicit + value overrides the automatic choice (even values are rounded + up to the next odd integer, as required by the median filter). window_length : int, optional - Window length for median filter (default: len(chi2)//10) + Deprecated alias for ``kernel_size``; ignored when + ``kernel_size`` is given. Returns ------- @@ -82,6 +92,10 @@ def signal_detection_efficiency(chi2, chi2_null=None, detrend=True, SDE = (max(SR) - mean(SR)) / std(SR) Typical threshold: SDE > 7 for 1% false alarm probability + + Following ``transitleastsquares`` (Hippke & Heller 2019), detrending + is skipped entirely when ``len(SR) <= 2 * kernel_size``; in that + case the raw SDE and raw SR are returned unchanged. """ chi2 = np.asarray(chi2) @@ -99,28 +113,45 @@ def signal_detection_efficiency(chi2, chi2_null=None, detrend=True, # Detrend with median filter if requested if detrend: - if window_length is None: - window_length = max(len(SR) // 10, 3) + if kernel_size is None: + kernel_size = window_length # deprecated alias + if kernel_size is None: + kernel_size = max(len(SR) // 10, 3) # Ensure odd window - if window_length % 2 == 0: - window_length += 1 - - # Apply median filter to remove trends - SR_trend = signal.medfilt(SR, kernel_size=window_length) + if kernel_size % 2 == 0: + kernel_size += 1 + # Cap at the fixed 91-point kernel used by the reference + # transitleastsquares implementation; an uncapped len//10 + # window makes medfilt O(n*k) ~ O(n^2/10) and takes minutes + # of CPU at survey-scale period grids (n ~ 1e5). + kernel_size = min(kernel_size, 91) + elif kernel_size % 2 == 0: + # medfilt requires an odd kernel + kernel_size += 1 + + if len(SR) <= 2 * kernel_size: + # Too few points to estimate a trend; follow the reference + # transitleastsquares behavior and skip detrending. + SDE = SDE_raw + power = SR + else: + # Apply median filter to remove trends + SR_trend = signal.medfilt(SR, kernel_size=kernel_size) - # Detrended signal residue - SR_detrended = SR - SR_trend + np.median(SR) + # Detrended signal residue + SR_detrended = SR - SR_trend + np.median(SR) - # Calculate SDE on detrended signal - mean_SR_detrended = np.mean(SR_detrended) - std_SR_detrended = np.std(SR_detrended) + # Calculate SDE on detrended signal + mean_SR_detrended = np.mean(SR_detrended) + std_SR_detrended = np.std(SR_detrended) - if std_SR_detrended < 1e-10: - SDE = 0.0 - else: - SDE = (np.max(SR_detrended) - mean_SR_detrended) / std_SR_detrended + if std_SR_detrended < 1e-10: + SDE = 0.0 + else: + SDE = ((np.max(SR_detrended) - mean_SR_detrended) + / std_SR_detrended) - power = SR_detrended + power = SR_detrended else: SDE = SDE_raw power = SR @@ -279,7 +310,7 @@ def odd_even_mismatch(depths_odd, depths_even): def compute_all_statistics(chi2, periods, best_period_idx, depth, duration, n_transits, - depths_per_transit=None): + depths_per_transit=None, kernel_size=None): """ Compute all TLS statistics for a search result. @@ -299,6 +330,11 @@ def compute_all_statistics(chi2, periods, best_period_idx, Number of transits at best period depths_per_transit : array_like, optional Individual transit depths + kernel_size : int, optional + Running-median kernel for SDE detrending, passed through to + :func:`signal_detection_efficiency`. Default (None) uses + ``min(len(chi2)//10 forced odd, 91)``, following the fixed + 91-point kernel convention of ``transitleastsquares``. Returns ------- @@ -313,7 +349,8 @@ def compute_all_statistics(chi2, periods, best_period_idx, - odd_even_mismatch: Odd/even depth difference (if available) """ # Signal residue and SDE - SDE, SDE_raw, power = signal_detection_efficiency(chi2, detrend=True) + SDE, SDE_raw, power = signal_detection_efficiency( + chi2, detrend=True, kernel_size=kernel_size) SR = signal_residue(chi2) diff --git a/scripts/benchmark_tls_survey.py b/scripts/benchmark_tls_survey.py new file mode 100644 index 0000000..9f3d39f --- /dev/null +++ b/scripts/benchmark_tls_survey.py @@ -0,0 +1,480 @@ +#!/usr/bin/env python +"""Survey-scale TLS throughput benchmark (end-to-end, GPU). + +Measures wall time per lightcurve (grid generation + preprocessing + H2D + +kernel + D2H + statistics) for N lightcurves per survey regime, comparing up +to three implementations (each degrades gracefully if unavailable): + + new cuvarbase.tls.tls_search_batch (batch API) + old cuvarbase.tls.tls_transit looped per LC (ndata <= 3300 only; + capped at --old-nlc LCs, per-LC median extrapolated) + reference CPU transitleastsquares (--ref-nlc LCs; kepler-4yr skipped + unless --ref-all) + +Half the lightcurves carry an injected box transit (Keplerian duration, +Sun-like), half are pure noise; recovery + median SDE reported per half. + +Usage (RunPod pod): + ./scripts/run-remote.sh python scripts/benchmark_tls_survey.py \\ + [--regimes tess-ffi,k2] [--impls new,old,reference] [--quick] + +Output: JSON via --output plus a human-readable summary table. +""" + +import argparse +import json +import multiprocessing +import platform +import subprocess +import sys +import time +import traceback +from collections import OrderedDict +from pathlib import Path + +import numpy as np + +sys.path.insert(0, str(Path(__file__).parent.parent)) + +OLD_NDATA_CAP = 3300 # old per-LC kernel's shared-memory cap on ndata +REF_SKIP_DEFAULT = ('kepler-4yr',) # CPU ref >> 15 min; needs --ref-all + +# ---------------------------------------------------------------------------- +# Survey regimes (period ranges chosen for comparability with the reference +# TLS paper / GTLS 2026 paper). cadence in days; noise/depth fractional flux. +# ---------------------------------------------------------------------------- +MIN30 = 30.0 / (60.0 * 24.0) +MIN2 = 2.0 / (60.0 * 24.0) + +REGIMES = OrderedDict([ + ('tess-ffi', dict(ndata=1310, baseline=27.4, cadence=MIN30, noise=1e-3, + inject_period=7.7, inject_depth=0.005, + period_min=0.6, period_max=13.7, nlc=100)), + ('k2', dict(ndata=4320, baseline=90.0, cadence=MIN30, noise=8e-4, + inject_period=12.4, inject_depth=0.004, + period_min=0.6, period_max=45.0, nlc=50)), + ('tess-2min', dict(ndata=19710, baseline=27.4, cadence=MIN2, noise=2e-3, + inject_period=7.7, inject_depth=0.005, + period_min=0.6, period_max=13.7, nlc=50)), + ('tess-yr', dict(ndata=16850, baseline=351.0, cadence=MIN30, noise=1e-3, + inject_period=21.7, inject_depth=0.004, + period_min=0.6, period_max=175.0, nlc=20)), + ('kepler-4yr', dict(ndata=65440, baseline=1363.0, cadence=MIN30, + noise=6e-4, inject_period=41.3, inject_depth=0.003, + period_min=0.6, period_max=500.0, nlc=10)), +]) + + +# ---------------------------------------------------------------------------- +# Lightcurve generation +# ---------------------------------------------------------------------------- + +def make_lc(cfg, seed, inject): + """Regular-cadence LC, flux ~1.0, optional box transit at t0 = 0.3 * P + with Keplerian duration q = 0.0763 * P^(-2/3) (fraction of period, + Sun-like). A box (not limb-darkened) is fine: recovery is on period.""" + rng = np.random.default_rng(seed) + t = np.arange(cfg['ndata'], dtype=np.float64) * cfg['cadence'] + y = 1.0 + rng.normal(0.0, cfg['noise'], cfg['ndata']) + if inject: + P = cfg['inject_period'] + q = 0.0763 * P ** (-2.0 / 3.0) + t0 = 0.3 * P + in_transit = np.abs(((t - t0 + 0.5 * P) % P) - 0.5 * P) < 0.5 * q * P + y[in_transit] -= cfg['inject_depth'] + dy = np.full(cfg['ndata'], cfg['noise']) + return t, y, dy + + +def make_regime_lcs(key, cfg, nlc): + """~Half injected, half pure noise; seeded per (regime, lc_index).""" + regime_idx = list(REGIMES).index(key) + flags = [i % 2 == 0 for i in range(nlc)] + lcs = [make_lc(cfg, 100000 * (regime_idx + 1) + i, flags[i]) + for i in range(nlc)] + return lcs, flags + + +# ---------------------------------------------------------------------------- +# GPU / environment helpers (imports deferred so --help works anywhere) +# ---------------------------------------------------------------------------- + +def gpu_sync(): + try: + import pycuda.driver as drv + drv.Context.synchronize() + except Exception: + pass + + +def _get_device(): + try: # v1.0 lazy context helper + from cuvarbase.core import ensure_context + return ensure_context().device + except Exception: + import pycuda.autoprimaryctx + return pycuda.autoprimaryctx.device + + +def env_info(): + info = dict(python=platform.python_version(), numpy=np.__version__, + hostname=platform.node(), + cpu_count=multiprocessing.cpu_count()) + try: + import cuvarbase + info['cuvarbase'] = cuvarbase.__version__ + except Exception as e: + info['cuvarbase'] = 'unavailable: %s' % e + try: + import pycuda + import pycuda.driver as drv + dev = _get_device() + info['pycuda'] = getattr(pycuda, 'VERSION_TEXT', 'unknown') + info['cuda_driver_version'] = drv.get_driver_version() + info['gpu'] = dev.name() + info['compute_capability'] = '%d.%d' % dev.compute_capability() + except Exception as e: + info['gpu'] = 'unavailable: %s' % e + try: + out = subprocess.check_output(['nvcc', '--version'], + stderr=subprocess.STDOUT) + info['nvcc'] = out.decode().strip().splitlines()[-2].strip() + except Exception as e: + info['nvcc'] = 'unavailable: %s' % e + return info + + +def probe_nperiods(cfg): + """Number of Ofir-grid periods this regime's search covers.""" + try: + from cuvarbase import tls_grids + t = np.arange(cfg['ndata'], dtype=np.float64) * cfg['cadence'] + periods = tls_grids.period_grid_ofir( + t, R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=cfg['period_min'], period_max=cfg['period_max']) + return int(len(periods)) + except Exception as e: + print(' nperiods probe failed: %r' % e) + return None + + +# ---------------------------------------------------------------------------- +# Recovery / statistics +# ---------------------------------------------------------------------------- + +def _f(v): + try: + return float(v) + except Exception: + return None + + +def eval_recovery(results, flags, inject_period): + """Recovery on the injected half; |P/P_inj - 1| < 0.01 counts as + recovered, 2x / 0.5x aliases (1% relative) counted separately.""" + n_inj = n_rec = n_alias = 0 + sde_inj, sde_noise = [], [] + for res, injected in zip(results, flags): + res = res or {} + sde = _f(res.get('SDE')) + if injected: + n_inj += 1 + if sde is not None: + sde_inj.append(sde) + p = _f(res.get('period')) + if p: + r = p / inject_period + if abs(r - 1.0) < 0.01: + n_rec += 1 + elif abs(r / 2.0 - 1.0) < 0.01 or abs(2.0 * r - 1.0) < 0.01: + n_alias += 1 + elif sde is not None: + sde_noise.append(sde) + return dict( + n_injected=n_inj, n_recovered=n_rec, n_alias=n_alias, + recovery_frac=(n_rec / n_inj) if n_inj else None, + median_sde_injected=float(np.median(sde_inj)) if sde_inj else None, + median_sde_noise=float(np.median(sde_noise)) if sde_noise else None) + + +def compact_per_lc(results, flags): + out = [] + for res, injected in zip(results, flags): + res = res or {} + rec = dict(injected=bool(injected)) + for k in ('period', 'T0', 'duration', 'depth', 'SDE', 'chi2_min'): + rec[k] = _f(res.get(k)) + out.append(rec) + return out + + +# ---------------------------------------------------------------------------- +# Implementations +# ---------------------------------------------------------------------------- + +def _warmup_new(tls_search_batch, cfg): + """A 2-LC batch with the REGIME's own config so every phase-bin + band variant this regime needs is compiled before timing (band + structure depends on the period range).""" + print(' [new] warmup: 2-LC regime batch (absorbs compile)...', + flush=True) + wlcs = [make_lc(cfg, 900 + i, inject=True) for i in range(2)] + tls_search_batch(wlcs, R_star=1.0, M_star=1.0, + period_min=cfg['period_min'], + period_max=cfg['period_max'], + oversampling_factor=3, n_durations=15, + t0_oversample=3.0, + block_size=None, nbins=None, return_arrays=False) + gpu_sync() + + +def run_new(cfg, lcs, flags, args, state): + entry = dict(nlc=len(lcs)) + try: + from cuvarbase.tls import tls_search_batch + except Exception as e: + traceback.print_exc() + entry['error'] = 'import failed: %r' % e + return entry + try: + warm_key = 'new_warmed_%s_%s' % (cfg['period_min'], + cfg['period_max']) + if not state.get(warm_key): + _warmup_new(tls_search_batch, cfg) + state[warm_key] = True + print(' [new] timing %d-LC batch (x%d iter)...' + % (len(lcs), args.n_iter), flush=True) + times, results = [], None + for _ in range(args.n_iter): + gpu_sync() + t0 = time.perf_counter() + results = tls_search_batch( + lcs, R_star=1.0, M_star=1.0, + period_min=cfg['period_min'], period_max=cfg['period_max'], + oversampling_factor=3, n_durations=15, t0_oversample=3.0, + block_size=None, nbins=None, + return_arrays=False) + gpu_sync() + times.append(time.perf_counter() - t0) + total = float(np.median(times)) + entry.update(total_s=total, times_s=times, + ms_per_lc=1000.0 * total / len(lcs), + lc_per_s=len(lcs) / total) + entry.update(eval_recovery(results, flags, cfg['inject_period'])) + entry['per_lc'] = compact_per_lc(results, flags) + except Exception as e: + traceback.print_exc() + entry['error'] = repr(e) + return entry + + +def run_old(cfg, lcs, flags, args): + entry = dict() + if cfg['ndata'] > OLD_NDATA_CAP: + entry['skipped'] = 'ndata cap' + print(' [old] skipped: ndata=%d > %d (shared-memory cap)' + % (cfg['ndata'], OLD_NDATA_CAP)) + return entry + try: + from cuvarbase.tls import tls_transit + except Exception as e: + traceback.print_exc() + entry['error'] = 'import failed: %r' % e + return entry + n_old = min(args.old_nlc, len(lcs)) + sub, subflags = lcs[:n_old], flags[:n_old] + kwargs = dict(R_star=1.0, M_star=1.0, period_min=cfg['period_min'], + period_max=cfg['period_max'], use_fast=False) + try: + print(' [old] warmup (1 LC, absorbs compile)...', flush=True) + tls_transit(*sub[0], **kwargs) + gpu_sync() + print(' [old] timing %d LCs (per-LC loop)...' % n_old, flush=True) + per_call, results = [], [] + for (t, y, dy) in sub: + gpu_sync() + t0 = time.perf_counter() + results.append(tls_transit(t, y, dy, **kwargs)) + gpu_sync() + per_call.append(time.perf_counter() - t0) + med = float(np.median(per_call)) + entry.update(nlc=n_old, total_s=float(np.sum(per_call)), + per_call_s=per_call, ms_per_lc=1000.0 * med, + lc_per_s=1.0 / med, extrapolated=True, + note='per-LC median over %d LCs' % n_old) + entry.update(eval_recovery(results, subflags, cfg['inject_period'])) + entry['per_lc'] = compact_per_lc(results, subflags) + except Exception as e: + traceback.print_exc() + entry['error'] = repr(e) + return entry + + +def run_reference(key, cfg, lcs, flags, args): + entry = dict() + if key in REF_SKIP_DEFAULT and not args.ref_all: + entry['skipped'] = ('expected CPU runtime >~15 min; ' + 'pass --ref-all to run') + print(' [reference] skipped: %s' % entry['skipped']) + return entry + try: + from transitleastsquares import transitleastsquares + except Exception as e: + entry['error'] = 'import failed: %r' % e + print(' [reference] %s' % entry['error']) + return entry + n_ref = min(args.ref_nlc, len(lcs)) + sub, subflags = lcs[:n_ref], flags[:n_ref] + ncpu = multiprocessing.cpu_count() + try: + print(' [reference] timing %d LCs (CPU, %d threads)...' + % (n_ref, ncpu), flush=True) + per_call, results = [], [] + for (t, y, dy) in sub: + t0 = time.perf_counter() + # reference TLS expects flux normalized around 1.0; our + # generator already produces y ~ 1.0 + model = transitleastsquares(t, y, dy) + res = model.power(R_star=1.0, M_star=1.0, + period_min=cfg['period_min'], + period_max=cfg['period_max'], + oversampling_factor=3, use_threads=ncpu, + show_progress_bar=False) + per_call.append(time.perf_counter() - t0) + results.append({k: _f(getattr(res, k, None)) for k in + ('period', 'SDE', 'T0', 'duration', 'depth')}) + med = float(np.median(per_call)) + entry.update(nlc=n_ref, total_s=float(np.sum(per_call)), + per_call_s=per_call, ms_per_lc=1000.0 * med, + lc_per_s=1.0 / med, extrapolated=True, + note='per-LC median over %d LCs' % n_ref) + entry.update(eval_recovery(results, subflags, cfg['inject_period'])) + entry['per_lc'] = compact_per_lc(results, subflags) + except Exception as e: + traceback.print_exc() + entry['error'] = repr(e) + return entry + + +# ---------------------------------------------------------------------------- +# Reporting +# ---------------------------------------------------------------------------- + +def _row(key, impl, nlc, total, mslc, lcs, rec, notes): + print('%-11s %-10s %5s %10s %10s %9s %10s %s' + % (key, impl, nlc, total, mslc, lcs, rec, notes)) + + +def print_summary(out): + print('\n' + '=' * 96 + '\nSUMMARY\n' + '=' * 96) + _row('regime', 'impl', 'nlc', 'total s', 'ms/LC', 'LC/s', 'recovery', + 'notes') + print('-' * 96) + for key, regime in out['regimes'].items(): + for impl, e in regime['impls'].items(): + if 'skipped' in e: + _row(key, impl, *['-'] * 5, 'skipped: %s' % e['skipped']) + elif 'error' in e: + _row(key, impl, *['-'] * 5, + 'error: %s' % str(e['error'])[:40]) + else: + rec = '%d/%d' % (e.get('n_recovered', 0), + e.get('n_injected', 0)) + if e.get('n_alias'): + rec += '+%da' % e['n_alias'] + _row(key, impl, '%d' % e['nlc'], '%.3f' % e['total_s'], + '%.2f' % e['ms_per_lc'], '%.2f' % e['lc_per_s'], + rec, e.get('note', '')) + print('=' * 96) + + +# ---------------------------------------------------------------------------- +# Main +# ---------------------------------------------------------------------------- + +def parse_args(): + p = argparse.ArgumentParser( + description='Survey-scale TLS throughput benchmark (GPU)', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + p.add_argument('--regimes', default=','.join(REGIMES), + help='comma-separated regime keys') + p.add_argument('--nlc', type=int, default=None, + help='override per-regime lightcurve count') + p.add_argument('--impls', default='new,old', + help='comma-separated: new,old,reference') + p.add_argument('--ref-nlc', type=int, default=1, + help='LCs for the CPU reference implementation') + p.add_argument('--old-nlc', type=int, default=5, + help='LCs for the old per-LC GPU path (extrapolated)') + p.add_argument('--n-iter', type=int, default=1, + help='timed iterations per batch (median reported)') + p.add_argument('--output', default='tls_survey_bench_results.json') + p.add_argument('--quick', action='store_true', + help='smoke test: nlc=4 per regime') + p.add_argument('--ref-all', action='store_true', + help='run CPU reference on all regimes incl. kepler-4yr') + return p.parse_args() + + +def main(): + args = parse_args() + + regime_keys = [k.strip() for k in args.regimes.split(',') if k.strip()] + bad = [k for k in regime_keys if k not in REGIMES] + if bad: + sys.exit('unknown regime(s) %s; choose from %s' + % (bad, list(REGIMES))) + impls = [s.strip() for s in args.impls.split(',') if s.strip()] + bad = [s for s in impls if s not in ('new', 'old', 'reference')] + if bad: + sys.exit('unknown impl(s) %s; choose from new,old,reference' % bad) + + out = dict(script='benchmark_tls_survey.py', + timestamp=time.strftime('%Y-%m-%dT%H:%M:%S'), + args=vars(args), regimes=OrderedDict()) + state = {} + + for key in regime_keys: + cfg = dict(REGIMES[key]) + nlc = args.nlc if args.nlc else (4 if args.quick else cfg['nlc']) + print('\n' + '=' * 70) + print('%s: ndata=%d, baseline=%.1fd, P=[%.2g, %.4g]d, nlc=%d' + % (key, cfg['ndata'], cfg['baseline'], cfg['period_min'], + cfg['period_max'], nlc)) + print('=' * 70) + + lcs, flags = make_regime_lcs(key, cfg, nlc) + nperiods = probe_nperiods(cfg) + if nperiods: + print(' Ofir grid: %d periods' % nperiods) + + regime_entry = dict(config=cfg, nlc=nlc, nperiods=nperiods, + impls=OrderedDict()) + for impl in impls: + if impl == 'new': + e = run_new(cfg, lcs, flags, args, state) + elif impl == 'old': + e = run_old(cfg, lcs, flags, args) + else: + e = run_reference(key, cfg, lcs, flags, args) + if 'total_s' in e: + print(' [%s] total %.3f s | %.2f ms/LC | %.2f LC/s | ' + 'recovered %d/%d (+%d alias)' + % (impl, e['total_s'], e['ms_per_lc'], e['lc_per_s'], + e.get('n_recovered', 0), e.get('n_injected', 0), + e.get('n_alias', 0))) + regime_entry['impls'][impl] = e + out['regimes'][key] = regime_entry + + out['env'] = env_info() + print('\n' + json.dumps(out['env'], indent=2)) + + print_summary(out) + + with open(args.output, 'w') as f: + json.dump(out, f, indent=2, default=str) + print('wrote %s' % args.output) + + +if __name__ == '__main__': + main() diff --git a/scripts/setup-remote.sh b/scripts/setup-remote.sh index d2f9319..5cf2208 100755 --- a/scripts/setup-remote.sh +++ b/scripts/setup-remote.sh @@ -30,10 +30,10 @@ echo "Step 1: Syncing code..." echo "" echo "Step 2: Installing cuvarbase in development mode..." -ssh ${SSH_OPTS} ${SSH_HOST} bash << 'ENDSSH' +ssh ${SSH_OPTS} ${SSH_HOST} REMOTE_DIR="${RUNPOD_REMOTE_DIR:-/workspace/cuvarbase}" bash << 'ENDSSH' set -e -cd /workspace/cuvarbase +cd "${REMOTE_DIR}" # Set up CUDA environment (auto-detect version) if [ -d /usr/local/cuda ]; then diff --git a/scripts/tls_fast_smoke.py b/scripts/tls_fast_smoke.py new file mode 100644 index 0000000..9d978e7 --- /dev/null +++ b/scripts/tls_fast_smoke.py @@ -0,0 +1,177 @@ +"""Smoke + parity test for the fast TLS path (run on a GPU pod). + +Checks, in order: +1. The fast kernels compile. +2. Fast path vs legacy kernel on the same explicit trial grid: + chi2 spectra strongly correlated, same best period, similar SDE. +3. Fast path recovers an injected transit (period + SDE), single LC. +4. Batch of mixed lightcurves: per-LC results match single-LC calls. +5. Large-ndata lightcurve (beyond the legacy 3,500-point cap) works. +""" +import sys +import time +import warnings + +import numpy as np + +warnings.filterwarnings('ignore', message='.*EXPERIMENTAL.*') + +from cuvarbase import tls +from cuvarbase import tls_grids + + +def make_lc(ndata, baseline, period, depth, noise, seed, t0_frac=0.3): + rng = np.random.RandomState(seed) + t = np.sort(rng.uniform(0, baseline, ndata)) + y = 1.0 + rng.randn(ndata) * noise + q = 0.0763 * period ** (-2.0 / 3.0) + t0 = t0_frac * period + rel = np.abs(((t - t0 + 0.5 * period) % period) - 0.5 * period) + y[rel < 0.5 * q * period] -= depth + dy = np.full(ndata, noise) + return t, y, dy + + +def check(name, cond, detail=""): + status = "PASS" if cond else "FAIL" + print("[%s] %s %s" % (status, name, detail)) + if not cond: + check.failures += 1 + + +check.failures = 0 + + +def main(): + # ---------------- 1. compile ---------------- + t_start = time.time() + kernels = tls.compile_tls_fast(block_size=128, nbins=1024) + check("compile", set(kernels) == {'search', 'refine'}, + "(%.1fs)" % (time.time() - t_start)) + + # ---------------- 2. parity vs legacy ---------------- + ndata, baseline = 1200, 27.0 + P_inj, depth = 5.123, 0.01 + t, y, dy = make_lc(ndata, baseline, P_inj, depth, 2e-3, seed=42) + + periods = tls_grids.period_grid_ofir( + t, R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=1.0, period_max=12.0).astype(np.float64) + _, _, qv = 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) + qmin, qmax = qv * 0.5, qv * 2.0 + + t0 = time.time() + r_old = tls.tls_search_gpu(t, y, dy, periods=periods, + qmin=qmin, qmax=qmax, n_durations=15, + use_fast=False) + t_old = time.time() - t0 + + t0 = time.time() + r_new = tls.tls_search_gpu(t, y, dy, periods=periods, + qmin=qmin, qmax=qmax, n_durations=15, + use_fast=True) + t_new = time.time() - t0 + + c_old = r_old['chi2'] + c_new = r_new['chi2'] + both = np.isfinite(c_old) & np.isfinite(c_new) + corr = np.corrcoef(c_old[both], c_new[both])[0, 1] + check("parity/chi2-corr", corr > 0.99, "corr=%.5f" % corr) + check("parity/best-period", + abs(r_new['period'] - r_old['period']) / r_old['period'] < 0.01, + "old=%.4f new=%.4f" % (r_old['period'], r_new['period'])) + check("parity/period-hit", + abs(r_new['period'] - P_inj) / P_inj < 0.01, + "P=%.4f (inj %.4f)" % (r_new['period'], P_inj)) + check("parity/SDE", r_new['SDE'] > 0.8 * r_old['SDE'], + "old=%.2f new=%.2f" % (r_old['SDE'], r_new['SDE'])) + check("parity/depth", + abs(r_new['depth'] - depth) / depth < 0.5, + "depth=%.4f" % r_new['depth']) + med_old = np.median(c_old[both]) + med_new = np.median(c_new[both]) + check("parity/chi2-scale", abs(med_new / med_old - 1) < 0.05, + "median old=%.1f new=%.1f" % (med_old, med_new)) + print(" timing: legacy %.3fs, fast %.3fs (%.1fx)" + % (t_old, t_new, t_old / max(t_new, 1e-9))) + + # ---------------- 3. auto-grid recovery ---------------- + res = tls.tls_transit(t, y, dy, R_star=1.0, M_star=1.0, + period_min=1.0, period_max=12.0) + check("auto/period", abs(res['period'] - P_inj) / P_inj < 0.01, + "P=%.4f SDE=%.2f" % (res['period'], res['SDE'])) + check("auto/SDE", res['SDE'] > 5.0, "SDE=%.2f" % res['SDE']) + + # ---------------- 4. batch consistency ---------------- + lcs = [] + P_injs = [3.3, 7.7, 0.0] # third LC = pure noise + for i, P in enumerate(P_injs): + if P > 0: + lcs.append(make_lc(1500 + 400 * i, 27.0, P, 0.012, 2e-3, + seed=100 + i)) + else: + rng = np.random.RandomState(100 + i) + tt = np.sort(rng.uniform(0, 27.0, 1500 + 400 * i)) + lcs.append((tt, 1.0 + rng.randn(len(tt)) * 2e-3, + np.full(len(tt), 2e-3))) + + # shared explicit grid so batch and single calls are comparable + # (auto grids depend on each lightcurve's exact baseline) + tspan_max = max(lc[0].max() - lc[0].min() for lc in lcs) + t_ref = [lc for lc in lcs + if lc[0].max() - lc[0].min() == tspan_max][0][0] + shared_periods = tls_grids.period_grid_ofir( + t_ref, R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=1.0, period_max=12.0) + + batch = tls.tls_search_batch(lcs, R_star=1.0, M_star=1.0, + periods=shared_periods) + singles = [tls.tls_search_batch([lc], R_star=1.0, M_star=1.0, + periods=shared_periods)[0] + for lc in lcs] + for i, (b, s) in enumerate(zip(batch, singles)): + if P_injs[i] > 0: + # non-deterministic atomics can flip near-tied neighboring + # grid points; allow a few grid steps of slack + check("batch/lc%d-period-match" % i, + abs(b['period'] - s['period']) / s['period'] < 5e-3, + "batch=%.5f single=%.5f" % (b['period'], s['period'])) + check("batch/lc%d-recovered" % i, + abs(b['period'] - P_injs[i]) / P_injs[i] < 0.01, + "P=%.4f SDE=%.2f" % (b['period'], b['SDE'])) + else: + check("batch/lc%d-noise-SDE-consistent" % i, + abs(b['SDE'] - s['SDE']) < 1.5, + "batch=%.2f single=%.2f" % (b['SDE'], s['SDE'])) + sde_noise = batch[2]['SDE'] + sde_sig = batch[0]['SDE'] + check("batch/noise-SDE-lower", sde_noise < sde_sig, + "sig=%.2f noise=%.2f" % (sde_sig, sde_noise)) + + # ---------------- 5. large ndata (legacy cap exceeded) ---------------- + t5, y5, dy5 = make_lc(20000, 27.0, 4.56, 0.008, 2e-3, seed=7) + t0 = time.time() + r5 = tls.tls_search_batch([(t5, y5, dy5)], R_star=1.0, M_star=1.0, + period_min=1.0, period_max=12.0)[0] + dt5 = time.time() - t0 + check("large/period", abs(r5['period'] - 4.56) / 4.56 < 0.01, + "P=%.4f SDE=%.2f (%.2fs)" % (r5['period'], r5['SDE'], dt5)) + + # BJD-scale time offsets + r6 = tls.tls_search_batch([(t5 + 2457000.0, y5, dy5)], + R_star=1.0, M_star=1.0, + period_min=1.0, period_max=12.0)[0] + check("large/bjd-offset", abs(r6['period'] - 4.56) / 4.56 < 0.01, + "P=%.4f SDE=%.2f" % (r6['period'], r6['SDE'])) + + print() + if check.failures: + print("%d FAILURES" % check.failures) + sys.exit(1) + print("ALL SMOKE CHECKS PASSED") + + +if __name__ == '__main__': + main() diff --git a/scripts/tls_kernel_sweep.py b/scripts/tls_kernel_sweep.py new file mode 100644 index 0000000..d6aacfd --- /dev/null +++ b/scripts/tls_kernel_sweep.py @@ -0,0 +1,97 @@ +"""Sweep block_size x nbins for the coarse TLS kernel (kepler-4yr-like +config), reporting steady-state kernel-only times.""" +import warnings +import time + +warnings.filterwarnings('ignore') + +import numpy as np +import pycuda.driver as cuda +import pycuda.gpuarray as gpuarray + +from cuvarbase import tls, tls_grids, tls_models +from cuvarbase.base import ensure_context + +ensure_context() + +ndata, nlc = 65440, 4 +cad = 30. / 60 / 24 +lcs = [] +for i in range(nlc): + rng = np.random.RandomState(1234 + i) + t = np.arange(ndata) * cad + y = 1.0 + rng.randn(ndata) * 6e-4 + lcs.append((t, y, np.full(ndata, 6e-4))) + +periods = tls_grids.period_grid_ofir( + lcs[0][0], R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=0.6, period_max=500.) +periods32 = np.asarray(periods, np.float32) +_, _, qv = tls_grids.duration_grid_keplerian( + np.asarray(periods, np.float64), R_star=1.0, M_star=1.0, + R_planet=1.0, qmin_fac=0.5, qmax_fac=2.0, n_durations=15) +qmin = (qv * 0.5).astype(np.float32) +qmax = (qv * 2).astype(np.float32) +nperiods = len(periods32) + +t_hi_c, t_lo_c, a_c, b_c, offs, lens, chi2_0, epochs, spans = \ + tls._preprocess_batch(lcs) +T_tab, S1_tab, S2_tab = tls_models.generate_template_tables() +periods_g = gpuarray.to_gpu(periods32) +qmin_g_ = gpuarray.to_gpu(qmin) +qmax_g_ = gpuarray.to_gpu(qmax) +S1_g = gpuarray.to_gpu(S1_tab) +S2_g = gpuarray.to_gpu(S2_tab) +thi_g = gpuarray.to_gpu(t_hi_c) +tlo_g = gpuarray.to_gpu(t_lo_c) +a_g = gpuarray.to_gpu(a_c) +b_g = gpuarray.to_gpu(b_c) +off_g = gpuarray.to_gpu(offs.astype(np.int32)) +len_g = gpuarray.to_gpu(lens.astype(np.int32)) +outn = nlc * nperiods +chi2_g = gpuarray.empty(outn, np.float32) +t0_g = gpuarray.empty(outn, np.float32) +dur_g = gpuarray.empty(outn, np.float32) +dep_g = gpuarray.empty(outn, np.float32) + + +map_g = gpuarray.to_gpu(np.arange(nperiods, dtype=np.int32)) + + +def launch(k, bs, smem): + k['search'](thi_g, tlo_g, a_g, b_g, off_g, len_g, periods_g, + qmin_g_, qmax_g_, map_g, S1_g, S2_g, + np.int32(nperiods), np.int32(nperiods), np.int32(15), + chi2_g, t0_g, dur_g, dep_g, + block=(bs, 1, 1), grid=(nperiods, nlc, 1), shared=smem) + + +ref_chi2 = None +for bs in (128, 256, 512): + for nb in (4096, 8192): + try: + k = tls._get_cached_fast_kernels(bs, nb, 3.0) + smem = tls._tls_fast_shared_size(bs, nb) + launch(k, bs, smem) + cuda.Context.synchronize() + ts = [] + for _ in range(3): + cuda.Context.synchronize() + s = time.perf_counter() + launch(k, bs, smem) + cuda.Context.synchronize() + ts.append(time.perf_counter() - s) + med = sorted(ts)[1] + c = chi2_g.get()[:nperiods] + if ref_chi2 is None: + ref_chi2 = c + corr = 1.0 + else: + ok = (c > 0) & (ref_chi2 > 0) + corr = np.corrcoef(c[ok], ref_chi2[ok])[0, 1] + print("bs=%d nbins=%d smem=%dKB: %.3f s (%.1f ms/LC) " + "scoremax=%.1f corr_vs_first=%.5f" + % (bs, nb, smem // 1024, med, 1000 * med / nlc, + np.nanmax(c), corr)) + except Exception as e: + print("bs=%d nbins=%d FAIL: %r" % (bs, nb, e)) diff --git a/scripts/tls_profile_stages.py b/scripts/tls_profile_stages.py new file mode 100644 index 0000000..48ad47c --- /dev/null +++ b/scripts/tls_profile_stages.py @@ -0,0 +1,204 @@ +"""Stage-level profiling + tuning sweeps for the fast TLS batch engine. + +Times each stage of tls_search_batch separately (by monkeypatching / +re-implementing its flow), then sweeps block_size and nbins on the +kernel-dominated regimes to pick defaults. + +Usage (on pod): + python scripts/tls_profile_stages.py [--regime kepler-4yr] [--nlc 4] +""" +import argparse +import time +import warnings + +warnings.filterwarnings('ignore') + +import numpy as np + + +def make_lcs(regime, nlc): + cfgs = { + 'tess-ffi': dict(ndata=1310, cadence=30. / 60 / 24, noise=1e-3, + pinj=7.7, depth=0.005, pmin=0.6, pmax=13.7), + 'k2': dict(ndata=4320, cadence=30. / 60 / 24, noise=8e-4, + pinj=12.4, depth=0.004, pmin=0.6, pmax=45.), + 'tess-2min': dict(ndata=19710, cadence=2. / 60 / 24, noise=2e-3, + pinj=7.7, depth=0.005, pmin=0.6, pmax=13.7), + 'tess-yr': dict(ndata=16850, cadence=30. / 60 / 24, noise=1e-3, + pinj=21.7, depth=0.004, pmin=0.6, pmax=175.), + 'kepler-4yr': dict(ndata=65440, cadence=30. / 60 / 24, noise=6e-4, + pinj=41.3, depth=0.003, pmin=0.6, pmax=500.), + } + c = cfgs[regime] + lcs = [] + for i in range(nlc): + rng = np.random.RandomState(1234 + i) + t = np.arange(c['ndata']) * c['cadence'] + y = 1.0 + rng.randn(c['ndata']) * c['noise'] + q = 0.0763 * c['pinj'] ** (-2.0 / 3.0) + t0 = 0.3 * c['pinj'] + rel = np.abs(((t - t0 + 0.5 * c['pinj']) % c['pinj']) + - 0.5 * c['pinj']) + y[rel < 0.5 * q * c['pinj']] -= c['depth'] + lcs.append((t, y, np.full(c['ndata'], c['noise']))) + return lcs, c + + +def profile(regime, nlc, block_size=None, nbins=None, refine_top_k=200, + n_durations=15): + import pycuda.driver as cuda + from cuvarbase import tls, tls_grids, tls_models + + lcs, c = make_lcs(regime, nlc) + + def sync(): + cuda.Context.synchronize() + + T = {} + + t0 = time.perf_counter() + periods = tls_grids.period_grid_ofir( + lcs[0][0], R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=c['pmin'], period_max=c['pmax']) + periods32 = np.asarray(periods, dtype=np.float32) + _, _, qv = tls_grids.duration_grid_keplerian( + np.asarray(periods, np.float64), R_star=1.0, M_star=1.0, + R_planet=1.0, qmin_fac=0.5, qmax_fac=2.0, n_durations=n_durations) + qmin, qmax = (qv * 0.5).astype(np.float32), (qv * 2).astype(np.float32) + T['grid_gen'] = time.perf_counter() - t0 + nperiods = len(periods32) + + bs = block_size or tls._TLS_FAST_DEFAULT_BLOCK + qmin_g = float(qmin.min()) + nb = nbins or tls._auto_nbins(qmin_g, 3.0, bs) + + t0 = time.perf_counter() + kernels = tls._get_cached_fast_kernels(bs, nb, 3.0) + T['compile_or_cache'] = time.perf_counter() - t0 + + t0 = time.perf_counter() + T_tab, S1_tab, S2_tab = tls_models.generate_template_tables() + T['template'] = time.perf_counter() - t0 + + t0 = time.perf_counter() + t_hi_c, t_lo_c, a_c, b_c, offs, lens, chi2_0, epochs, spans = \ + tls._preprocess_batch(lcs) + T['preprocess_cpu'] = time.perf_counter() - t0 + + import pycuda.gpuarray as gpuarray + t0 = time.perf_counter() + periods_gpu = gpuarray.to_gpu(periods32) + qmin_gpu = gpuarray.to_gpu(qmin) + qmax_gpu = gpuarray.to_gpu(qmax) + T_g = gpuarray.to_gpu(T_tab) + S1_g = gpuarray.to_gpu(S1_tab) + S2_g = gpuarray.to_gpu(S2_tab) + thi_g = gpuarray.to_gpu(t_hi_c) + tlo_g = gpuarray.to_gpu(t_lo_c) + a_g = gpuarray.to_gpu(a_c) + b_g = gpuarray.to_gpu(b_c) + off_g = gpuarray.to_gpu(offs.astype(np.int32)) + len_g = gpuarray.to_gpu(lens.astype(np.int32)) + out_n = nlc * nperiods + chi2_g = gpuarray.empty(out_n, np.float32) + t0_g = gpuarray.empty(out_n, np.float32) + dur_g = gpuarray.empty(out_n, np.float32) + depth_g = gpuarray.empty(out_n, np.float32) + sync() + T['h2d_alloc'] = time.perf_counter() - t0 + + smem = tls._tls_fast_shared_size(bs, nb) + map_g = gpuarray.to_gpu(np.arange(nperiods, dtype=np.int32)) + t0 = time.perf_counter() + kernels['search']( + thi_g, tlo_g, a_g, b_g, off_g, len_g, + periods_gpu, qmin_gpu, qmax_gpu, map_g, S1_g, S2_g, + np.int32(nperiods), np.int32(nperiods), np.int32(n_durations), + chi2_g, t0_g, dur_g, depth_g, + block=(bs, 1, 1), grid=(nperiods, nlc, 1), shared=smem) + sync() + T['coarse_kernel'] = time.perf_counter() - t0 + + t0 = time.perf_counter() + chi2_h = chi2_g.get().reshape(nlc, nperiods) + T['d2h_chi2'] = time.perf_counter() - t0 + + K = int(min(refine_top_k, max(16, nperiods // 10), nperiods)) + t0 = time.perf_counter() + cand = np.empty((nlc, K), dtype=np.int32) + for j in range(nlc): + cand[j] = np.argpartition(-chi2_h[j], K)[:K] if K < nperiods \ + else np.arange(nperiods) + cand_g = gpuarray.to_gpu(cand.ravel()) + rchi2_g = gpuarray.empty(nlc * K, np.float32) + rt0_g = gpuarray.empty(nlc * K, np.float32) + rdur_g = gpuarray.empty(nlc * K, np.float32) + rdepth_g = gpuarray.empty(nlc * K, np.float32) + dur_ratio = float(np.median(qmax / qmin)) + dur_span = dur_ratio ** (1.0 / (2.0 * (n_durations - 1))) + t0_hw = min(3.0, max(0.75, 1.5 / (nb * qmin_g))) + kernels['refine']( + thi_g, tlo_g, a_g, b_g, off_g, len_g, + periods_gpu, cand_g, T_g, + np.int32(nperiods), np.int32(K), + np.float32(dur_span), np.float32(t0_hw), np.float32(33.0), + t0_g, dur_g, + rchi2_g, rt0_g, rdur_g, rdepth_g, + block=(bs, 1, 1), grid=(K, nlc, 1), + shared=tls._tls_refine_shared_size(bs)) + sync() + T['refine'] = time.perf_counter() - t0 + + t0 = time.perf_counter() + from cuvarbase import tls_stats + for j in range(nlc): + row = chi2_h[j] + valid = row > 0 + cv = (chi2_0[j] - row[valid].astype(np.float64)) + bi = int(np.argmin(cv)) + tls_stats.compute_all_statistics(cv, periods32[valid], bi, + 0.01, 0.1, 10) + tls_stats.compute_period_uncertainty(periods32[valid], cv, bi) + T['stats_cpu'] = time.perf_counter() - t0 + + total = sum(T.values()) + print("\n%s nlc=%d nperiods=%d ndata=%d bs=%d nbins=%d K=%d" + % (regime, nlc, nperiods, c['ndata'], bs, nb, K)) + for k, v in T.items(): + print(" %-18s %8.3f s (%4.1f%%) %7.2f ms/LC" + % (k, v, 100 * v / total, 1000 * v / nlc)) + print(" %-18s %8.3f s %7.2f ms/LC" + % ('TOTAL', total, 1000 * total / nlc)) + return T + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--regime', default='kepler-4yr') + ap.add_argument('--nlc', type=int, default=4) + ap.add_argument('--block-size', type=int, default=None) + ap.add_argument('--nbins', type=int, default=None) + ap.add_argument('--sweep', action='store_true', + help='sweep block_size x nbins on this regime ' + '(reports steady-state coarse-kernel time)') + args = ap.parse_args() + + if args.sweep: + for bs in (64, 128, 256): + for nb in (None, 2048, 4096): + try: + profile(args.regime, args.nlc, block_size=bs, + nbins=nb) + except Exception as exc: + print("bs=%d nbins=%s FAILED: %r" % (bs, nb, exc)) + return + + # steady-state: run twice (first pays compile), report second + profile(args.regime, args.nlc, block_size=args.block_size, + nbins=args.nbins) + profile(args.regime, args.nlc, block_size=args.block_size, + nbins=args.nbins) + + +if __name__ == '__main__': + main() From c4d10ff25e063680edbd95608024bb75f482b07f Mon Sep 17 00:00:00 2001 From: John Hoffman Date: Mon, 6 Jul 2026 14:53:48 -0500 Subject: [PATCH 2/6] =?UTF-8?q?TLS:=20measured=20fidelity=20=E2=80=94=20SD?= =?UTF-8?q?E=20parity=20with=20reference,=20matched-fidelity=20timing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an apples-to-apples fidelity experiment answering whether the coarse-epoch-grid + refinement fast path sacrifices detectability. scripts/tls_fidelity_experiment.py runs identical injected light curves through cuvarbase (t0_oversample=3 default AND 33 reference-matched) and the reference transitleastsquares package on one shared Ofir grid, and recomputes SDE with the IDENTICAL statistic on both methods' chi2 spectra (their SR->SDE normalizations differ, so the statistic is held fixed and only spectrum fidelity varies). Result (RTX A5000): the default coarse grid is within 1-3% of the reference SDE (0.97-0.99x) with 100% recovery, including a marginal near-threshold depth and a narrow transit; matched t0=33 closes it to within 1% (1.01-1.03x). SDE is a period-space contrast that is largely insensitive to epoch-grid density, so the coarse grid trades reported t0/parameter precision (restored by refinement) for speed, not detectability. scripts/tls_matched_timing.py measures the matched- fidelity cost: 6-15x over default (Kepler-4yr 8.4x -> 1.48 s/LC). Rewrites analysis/TLS_COST_ANALYSIS.md honestly: throughput is the market-independent invariant (thousands x vs CPU; ~22-190x vs the GTLS CuPy GPU-TLS, arXiv:2607.00348); the earlier dollar ratios over-pinned the multiplier by comparing a spot-price GPU against an AWS on-demand CPU. Raw fidelity numbers in benchmarks/results/tls_survey_jul2026/. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01WEGJJrPcrvkJGeMAEryRPG --- CHANGELOG.rst | 2 +- analysis/TLS_COST_ANALYSIS.md | 240 ++++++++++-------- .../tls_survey_jul2026/fidelity_raw_a5000.txt | 22 ++ scripts/tls_fidelity_experiment.py | 196 ++++++++++++++ scripts/tls_matched_timing.py | 78 ++++++ 5 files changed, 429 insertions(+), 109 deletions(-) create mode 100644 benchmarks/results/tls_survey_jul2026/fidelity_raw_a5000.txt create mode 100644 scripts/tls_fidelity_experiment.py create mode 100644 scripts/tls_matched_timing.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index db2f09d..2e6c5e7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -48,7 +48,7 @@ What's new in cuvarbase * CE is now in **maintenance mode**: it keeps working, but no new development is planned — for an actively developed GPU CE/AOV search see `periodfind `_ * **Experimental** (UserWarning on import; not recommended for science use yet) * GPU Transit Least Squares (``cuvarbase.tls``) with Ofir (2014) period grids - * **TLS rewritten for survey-scale throughput (Jul 2026):** a new batch-native fast path (``tls_fast.cu`` + ``tls_search_batch()``) is now the default for ``tls_search``/``tls_search_gpu``/``tls_transit`` (opt out with ``use_fast=False``). Each (lightcurve, period) block folds once into shared-memory phase bins and scans every (duration, t0) trial against bin-averaged integrated-template tables with a closed-form chi2 (``chi2 = chi2_0 - num^2/den``), so trial cost is independent of ndata — the legacy kernel's two full O(ndata) passes per trial and its ~3,500-point shared-memory cap are both gone (Kepler-length and 2-min-cadence TESS lightcurves run natively). The period grid is split into bin-count bands so long-period searches don't pay the finest band's cost; folding uses an exact float-float decomposition (~1e-8 phase error at 4-year baselines, no 1/64-rate double math); the kernel outputs the cancellation-free delta-chi2 and the host reconstructs chi2 in float64. A second exact kernel re-fits the top-K candidate periods per lightcurve on a finer local (duration, t0) grid (``refine_top_k``, default 50; ``refine_oversample`` default 33, near the reference package's t0 stepping) — refinement sharpens the reported parameters while the SDE/FAP statistics come from the uniform coarse spectrum, keeping the detection statistic's scale consistent with the legacy kernel (chi2 correlation 0.998 measured). SDE detrending now uses the reference ``transitleastsquares`` 91-point median window instead of a pathological ``nperiods/10`` window (minutes -> ~0.1 s at 190k periods), ``duration_grid_keplerian`` is vectorized (1.1 s -> 40 ms at 190k periods), and per-lightcurve statistics run on a thread pool. Measured end-to-end on an RTX A5000 (``scripts/benchmark_tls_survey.py``, 100% injected-transit recovery in every regime): TESS-FFI sector 1.2 ms/lightcurve (~800 LC/s; reference CPU package: 14.7 s), K2 90-d 3.1 ms, TESS 2-min 2.8 ms, 1-yr/30-min 18 ms, Kepler 4-yr/65k-pt/188k-period 0.17 s vs ~522 s for reference TLS on a 16-core CPU (~3,000x) and 33 s/LC reported by the concurrent GTLS CuPy implementation (arXiv:2607.00348) on a faster RTX 4090. Batch API validation: empty/mismatched inputs, ``qmax < 1``, power-of-two ``block_size``, and non-negative ``refine_top_k`` are enforced with clear errors; offsets are 64-bit so >2^31-point batches chunk correctly + * **TLS rewritten for survey-scale throughput (Jul 2026):** a new batch-native fast path (``tls_fast.cu`` + ``tls_search_batch()``) is now the default for ``tls_search``/``tls_search_gpu``/``tls_transit`` (opt out with ``use_fast=False``). Each (lightcurve, period) block folds once into shared-memory phase bins and scans every (duration, t0) trial against bin-averaged integrated-template tables with a closed-form chi2 (``chi2 = chi2_0 - num^2/den``), so trial cost is independent of ndata — the legacy kernel's two full O(ndata) passes per trial and its ~3,500-point shared-memory cap are both gone (Kepler-length and 2-min-cadence TESS lightcurves run natively). The period grid is split into bin-count bands so long-period searches don't pay the finest band's cost; folding uses an exact float-float decomposition (~1e-8 phase error at 4-year baselines, no 1/64-rate double math); the kernel outputs the cancellation-free delta-chi2 and the host reconstructs chi2 in float64. A second exact kernel re-fits the top-K candidate periods per lightcurve on a finer local (duration, t0) grid (``refine_top_k``, default 50; ``refine_oversample`` default 33, near the reference package's t0 stepping) — refinement sharpens the reported parameters while the SDE/FAP statistics come from the uniform coarse spectrum, keeping the detection statistic's scale consistent with the legacy kernel (chi2 correlation 0.998 measured). SDE detrending now uses the reference ``transitleastsquares`` 91-point median window instead of a pathological ``nperiods/10`` window (minutes -> ~0.1 s at 190k periods), ``duration_grid_keplerian`` is vectorized (1.1 s -> 40 ms at 190k periods), and per-lightcurve statistics run on a thread pool. Measured end-to-end on an RTX A5000 (``scripts/benchmark_tls_survey.py``, 100% injected-transit recovery in every regime): TESS-FFI sector 1.2 ms/lightcurve (~800 LC/s), K2 90-d 3.1 ms, TESS 2-min 2.8 ms, 1-yr/30-min 18 ms, Kepler 4-yr/65k-pt/172k-period 0.17 s/LC. **Fidelity is not sacrificed for detection:** on the identical SDE statistic (recomputed on each method's chi2 spectrum), the default coarse-epoch grid gives SDE within 1-3% of the reference ``transitleastsquares`` package (0.97-0.99x) with 100% recovery including marginal-depth and narrow transits, because SDE is a period-space contrast largely insensitive to epoch-grid density; a reference-matched epoch grid (``t0_oversample=33``) closes it to within 1% (1.01-1.03x) at a measured 6-15x cost, and the exact refinement restores per-transit t0/parameter precision regardless. Apples-to-apples on the same machine (same light curves, same grid, single GPU vs all CPU cores), cuvarbase is ~1,000-3,000x faster than the reference at matched SDE fidelity; on a Kepler-class configuration it is ~22x (matched) to ~190x (default) faster than the concurrent GTLS CuPy GPU-TLS (arXiv:2607.00348, 33-138 s/LC on a faster RTX 4090). See ``analysis/TLS_COST_ANALYSIS.md``. Batch API validation: empty/mismatched inputs, ``qmax < 1``, power-of-two ``block_size``, and non-negative ``refine_top_k`` are enforced with clear errors; offsets are 64-bit so >2^31-point batches chunk correctly * TLS epoch (t0) grid is now duration-scaled (stride = duration / oversample, floor 30, cap 20,000 epochs): the previous fixed 30-epoch grid missed transits narrower than ~1/30 of the period entirely, which broke Keplerian-mode searches for most periods > ~3.5 d. The oversample factor is caller-tunable via ``t0_oversample`` on ``tls_search``/``tls_search_gpu``/``compile_tls`` (default 3.0, favoring speed; the reference ``transitleastsquares`` steps ~33x finer — raise it for sensitivity-critical searches). Mirrored in ``tls_grids.t0_grid_size()`` * Removed the TLS kernels' bitonic phase sort: it was incomplete for non-power-of-2 sizes and its output order was never consumed — pure wasted per-period work; results are unchanged * Added golden accuracy tests against the reference ``transitleastsquares`` package (``test_tls_golden.py``) diff --git a/analysis/TLS_COST_ANALYSIS.md b/analysis/TLS_COST_ANALYSIS.md index 3ce6bd1..e7ba957 100644 --- a/analysis/TLS_COST_ANALYSIS.md +++ b/analysis/TLS_COST_ANALYSIS.md @@ -1,111 +1,135 @@ -# TLS cost analysis: GPU vs CPU, and vs the only other GPU TLS - -**Question.** With the survey-scale fast TLS path (`tls_search_batch`), is it -cheaper to run a Transit Least Squares search on a rented GPU than on a CPU? -And is cuvarbase now not just the fastest but the *cheapest* TLS available? - -**Short answer.** Yes on both counts, by a wide margin. At its default -fidelity, cuvarbase GPU TLS costs **$0.06–$7.45 per million light curves** -depending on regime, versus **$11,000–$99,000 per million** for the reference -CPU `transitleastsquares` package — a **13,000× to 200,000×** cost reduction. -It is also ~600× cheaper than GTLS, the only other GPU TLS. Read the fidelity -caveat at the end before quoting the largest ratios. - -## Method - -Throughput is the measured end-to-end survey wall time (grid generation + -preprocessing + host↔device transfer + kernels + per-LC statistics), -`scripts/benchmark_tls_survey.py`, 100% injected-transit recovery in every -regime on every GPU. Raw JSON in `benchmarks/results/tls_survey_jul2026/`. - -Cost per million light curves is `(ms_per_lc × 1000 / 3600) × $/hr`. - -- **GPU $/hr** are the prices actually paid on RunPod this session: A5000 - **$0.16**, RTX 4000 Ada **$0.20**, Tesla V100 **$0.23**. -- **CPU $/hr** uses AWS on-demand as a defensible public anchor: 64 vCPU = - `c6i.16xlarge` **$2.72/hr** (the reference runs used `use_threads=cpu_count()` - = 64 on the pod); the compute-bound Kepler row uses 16 vCPU `c6i.4xlarge` - **$0.68/hr** to match the published 522 s / 16-core Kepler baseline. -- **Reference CPU** = the `transitleastsquares` package (Hippke & Heller 2019), - same forced Ofir period grid, `oversampling_factor=3`, all cores. Measured on - the pod for four regimes; the 4-year Kepler point (>15 min/LC) uses the - published 522 s figure (Ryzen 9 7950X, 16 cores; GTLS paper arXiv:2607.00348). - -## Cost per million light curves - -| Regime (ndata, periods) | A5000 | Ada | V100 | Cheapest GPU | Reference CPU | GPU savings | -|---|---:|---:|---:|---|---:|---:| -| TESS FFI (1.3K, 2.5K) | **$0.056** | $0.170 | $0.091 | A5000 $0.056 | $11,110 | ~199,000× | -| K2 90-d (4.3K, 9.7K) | **$0.138** | $0.356 | $0.247 | A5000 $0.138 | $16,226 | ~118,000× | -| TESS 2-min (19.7K, 2.5K)| **$0.125** | $0.283 | $0.197 | A5000 $0.125 | $15,643 | ~125,000× | -| TESS 1-yr (17K, 42K) | **$0.819** | $1.519 | $1.054 | A5000 $0.819 | $83,930 | ~102,000× | -| Kepler 4-yr (65K, 172K) | **$7.45** | $10.99 | $9.30 | A5000 $7.45 | $98,600 | ~13,000× | - -Two robust conclusions: - -1. **GPU TLS is dramatically cheaper than CPU TLS.** The ratio is the - throughput advantage (~3,000–12,000×) multiplied by the hourly-cost - advantage (a $0.16/hr GPU beats a multi-core CPU box), so it holds under any - reasonable CPU price — even pricing the CPU at the GPU's $0.16/hr leaves the - throughput gap intact. - -2. **The RTX A5000 is the cost sweet spot.** It is not always the fastest - (the V100 edges it on the biggest regime), but at $0.16/hr it is the - cheapest to operate in every regime. Fastest-per-dollar ≠ fastest. - -At A5000 rates, a full **TESS FFI sector–scale run of ~1 million light curves -costs about 6 cents** of GPU time; a **Kepler-depth 4-year, 65K-point, 172K-period -search of a million targets costs about $7.45** — versus roughly $100,000 for -the same million on the reference CPU pipeline. - -## Versus the only other GPU TLS (GTLS) - -GTLS (arXiv:2607.00348, submitted 1 Jul 2026; CuPy) is the sole other GPU TLS. -On a ~1500-day / 67K-point / 190K-period Kepler-class light curve it reports -**33.3 s/LC on an RTX 4090** (15.7× over reference TLS). cuvarbase does the -comparable Kepler-4yr configuration in **168 ms/LC on an A5000**. - -| | Time/LC | GPU $/hr | $/million LC | -|---|---:|---:|---:| -| GTLS, RTX 4090 | 33.3 s | ~$0.50 | ~$4,625 | -| cuvarbase, A5000 | 0.168 s | $0.16 | **$7.45** | - -≈ **620× cheaper** than GTLS, on a cheaper GPU. cuvarbase wins on hardware-hours -(hand-written kernels + phase-binned scan vs CuPy per-point) and on hardware -price (A5000 < 4090). - -## The honest caveat: fidelity - -The largest ratios are partly a fidelity trade, and the comparison is only fair -if that is stated: - -- **Default epoch grid.** cuvarbase's default coarse scan steps the transit - epoch at `t0_oversample=3` (~3 positions per transit duration), then runs an - **exact per-point refinement** at `refine_oversample=33` on the top candidate - periods. The reference package steps ~100× finer (`T0_FIT_MARGIN=0.01`) - *uniformly*. So cuvarbase evaluates far fewer coarse trials, which is a large - part of why it is faster — not implementation efficiency alone. -- **What we verified.** 100% injected-transit recovery in all five regimes on - all three GPUs; agreement with the reference package on the golden configs - (period error <1%, both packages flag the detection significant); coarse - chi² spectrum correlated 0.998 with the legacy per-point cuvarbase kernel. - This is strong evidence the default fidelity is science-useful, but it is not - a bit-for-bit statistical match to the reference's ~100× epoch grid, and no - full injection–recovery completeness campaign has been run yet (that is the - open D3 validation item; the module remains flagged EXPERIMENTAL). -- **Matched fidelity is still a win.** Raising `t0_oversample` toward the - reference grid costs roughly linearly in the coarse scan (~70% of Kepler-4yr - time). A reference-matched run is an estimated ~5–10× slower — order - **1–1.7 s/LC on an A5000, ~$40–75/million** — still **>20× faster/cheaper - than GTLS** and **>300× cheaper than the reference CPU**. (Estimate from the - stage profile; not yet measured end-to-end.) +# TLS fidelity, throughput, and cost: cuvarbase vs CPU vs GTLS + +Three questions, answered with measurements (RTX A5000, `scripts/tls_fidelity_experiment.py`, +`scripts/tls_matched_timing.py`, `scripts/benchmark_tls_survey.py`; raw in +`benchmarks/results/tls_survey_jul2026/`): + +1. Is the coarse-epoch-grid + refinement fast path **lossy** — does it sacrifice SNR/SDE? +2. How much **faster** is it, apples-to-apples (same light curves, same grid, same detectability)? +3. Is it **cheaper**, and is it the cheapest TLS available? + +## 0. What the reference "CPU pipeline" is + +The `transitleastsquares` package (Hippke & Heller 2019), pip-installed, called as a +user would: `transitleastsquares(t, y, dy).power(R_star=1, M_star=1, period_min, period_max, +oversampling_factor=3, use_threads=cpu_count())`. It runs on *all* CPU cores. All CPU +timings below are that package on the same machine as the GPU (a RunPod pod), except the +4-year Kepler row (>15 min/LC) which uses the published 522 s figure (16-core Ryzen 9 +7950X, GTLS paper). + +## 1. Fidelity: it is NOT lossy in detectability (measured) + +The detection statistic is the SDE, built from the whole χ²(period) spectrum. cuvarbase's +default fast path scans a **coarse epoch grid** (`t0_oversample=3`, ~3 epochs per transit +duration) plus an exact refinement of the top candidate periods; the reference steps t0 +~100× finer *everywhere*. Does that cost detectability? + +To compare cleanly, the *statistic* is held fixed: cuvarbase and the reference normalize +SR→SDE differently, so SDE is recomputed with `cuvarbase.tls_stats` on **both** methods' +χ² spectra. Only spectrum fidelity then varies. Identical injected light curves, one +shared Ofir period grid. + +| Signal | cuvarbase t0=3 (default) | cuvarbase t0=33 (matched) | reference | recovery | +|---|---:|---:|---:|:--:| +| tess-ffi, depth 0.005 (strong) | SDE 14.5 (**0.99×**) | 15.0 (**1.03×**) | 14.61 | 12/12 all | +| tess-ffi, depth 0.002 (marginal) | 12.01 (**0.97×**) | 12.47 (**1.01×**) | 12.37 | 10/10 all | +| k2, depth 0.004 (narrow, q≈0.014) | 25.23 (**0.98×**) | 26.11 (**1.01×**) | 25.82 | 6/6 all | + +**The default fast path is within 1–3% of the reference SDE, and matched (t0=33) is within +1%.** 100% recovery in every case, including a marginal near-threshold depth and a narrow +transit — the two regimes where any loss would show. + +Why the coarse epoch grid barely moves the SDE: **SDE is a period-space contrast, +`(peak − mean)/std` of the spectrum.** A coarser t0 grid lowers the best-fit quality at +*every* trial period by roughly the same amount, so the normalized contrast between the +true-period peak and the background is preserved. The finer reference grid raises all fits, +again roughly uniformly. The epoch grid mostly sets *reported t0/parameter precision* — and +that is exactly what the exact refinement pass restores. The duration-scaled t0 grid also +guarantees at least one tested epoch overlaps the transit, so even narrow transits don't +fall through. + +The small residual (1–3% at default) is in the **safe direction**: cuvarbase slightly +*under*-reports significance, never over-reports. Refinement is deliberately excluded from +the SDE (it feeds parameters only) precisely so the statistic stays on a uniform-fidelity +spectrum — sharpening only the peak would *inflate* SDE and manufacture false positives. + +Earlier internal notes cited a "~5–15% SDE loss." That was a *cuvarbase-fast-vs-cuvarbase-legacy* +artifact (two of our own kernels), **not** a loss versus the reference. Against the actual +reference package it is parity. + +## 2. Throughput, apples-to-apples + +Matched fidelity (t0=33, SDE parity confirmed above) costs 6–15× over the default coarse +grid: tess-ffi ~6×, k2 ~12×, TESS-yr 14.6×, Kepler-4yr 8.4× (176.8 → 1479 ms/LC on A5000). + +Same light curves, same period grid, single A5000 GPU vs all CPU cores of the same pod: + +| Regime | cuvarbase default | cuvarbase matched (SDE parity) | reference CPU | speedup (matched / default) | +|---|---:|---:|---:|---:| +| tess-ffi (marginal) | 2.6 ms/LC | 13.8 ms/LC | 46,222 ms/LC | 3,300× / 17,500× | +| k2 (narrow) | 5.3 ms/LC | 63.1 ms/LC | 61,445 ms/LC | 970× / 11,600× | + +So **even at genuine SDE parity (matched t0=33), cuvarbase is ~1,000–3,000× faster than the +reference TLS on the same machine**; at the default grid (already SDE-parity for detection) +it is ~11,000–17,000×. Caveat: this pod's reference is unusually slow (46–61 s/LC — a +slower CPU and 96-thread oversubscription on a small problem); a faster CPU narrows the raw +speedup. **Throughput ratio is the market-independent invariant; the exact multiplier is +CPU-dependent.** The robust claim is "thousands of times faster." + +## 3. Cost + +Cost = throughput × ($/hr). The throughput advantage above is measured and market-independent; +the dollar multiplier depends entirely on how you price the two markets, and an earlier +version of this note over-pinned it by comparing a lucky **$0.16/hr spot GPU against a +$2.72/hr AWS on-demand CPU** — two different markets. Corrected inputs: + +- **GPU**: RunPod A5000 list price is **$0.27/hr** (I paid $0.16 on some spot pods and $0.27 + on others — it fluctuates). Use $0.27. +- **CPU**: RunPod does not publish CPU-pod pricing; AWS on-demand 16-vCPU `c6i.4xlarge` is + **$0.68/hr**, 64-vCPU `c6i.16xlarge` is $2.72/hr. Cross-market, so treat as indicative only. + +Cost per million light curves at genuine full fidelity (matched t0=33, A5000 $0.27/hr): + +| Regime | cuvarbase matched | reference CPU | note | +|---|---:|---:|---| +| Kepler-4yr | ~$111/M | ~$98,600/M (16-core, published 522 s) | ~890× cheaper | +| TESS-yr | ~$24/M | (not measured) | — | + +At the default grid (already detection-parity): Kepler ~$13/M, TESS-FFI a few cents/M. But +the honest headline is the **throughput invariant (thousands×)**, not a single dollar ratio; +the ~890× above already uses the *most* CPU-favorable pairing (cheap 16-vCPU CPU, list-price +GPU, full-fidelity GPU). Under any reasonable pricing, GPU TLS is hundreds-to-thousands of +times cheaper. + +## 4. Versus GTLS (the only other GPU TLS) + +[GTLS](https://arxiv.org/abs/2607.00348) (arXiv:2607.00348, Hu, Ge, Jin, Willis, 1 Jul 2026; +CuPy, RTX 4090) reports a 3000-day light curve in **138 s** (single GPU) / 79 s (dual) vs +**3289 s** for CPU TLS → 24× / 42×, at TLS-equivalent detection (matched precision/recall). +A 1500-day case is ~33 s. cuvarbase does the comparable Kepler-4yr configuration in **177 ms/LC +at the default grid** (SDE-parity) or **1.48 s/LC at matched t0=33** on an A5000 (< a 4090): + +| | fidelity | time/LC | vs GTLS 1500-day | +|---|---|---:|---:| +| GTLS (RTX 4090) | TLS-matched | ~33 s | 1× | +| cuvarbase matched (A5000) | SDE parity, matched t0 | 1.48 s | ~22× faster | +| cuvarbase default (A5000) | SDE parity for detection | 0.177 s | ~190× faster | + +cuvarbase wins on hardware-hours (hand-written kernels + phase-binned scan vs CuPy per-point) +and on hardware price (A5000 < 4090), on a fidelity basis GTLS's own detection metric would +call equivalent. ## Bottom line -At default fidelity cuvarbase is, on the evidence here, both the fastest and the -cheapest TLS available — thousands of times cheaper than CPU TLS and ~600× -cheaper than the only other GPU TLS. Even conservatively adjusted to the -reference's finer epoch grid, it remains the cheapest by a large margin. The one -thing still owed before dropping the EXPERIMENTAL flag is a full -injection–recovery validation at matched fidelity (item D3), not a speed or cost -result. +- **Not lossy.** Detection SDE is at parity with the reference (0.97–1.03×) with 100% + recovery, including marginal and narrow transits. The coarse grid trades *epoch/parameter + precision* for speed, and the refinement restores that. Apples-to-apples (matched t0=33) is + within 1% of the reference SDE. +- **Fastest.** ~1,000–3,000× faster than reference CPU TLS at genuine SDE parity on the same + machine; ~22–190× faster than GTLS on cheaper hardware. +- **Cheapest.** Hundreds-to-thousands of times cheaper per light curve than CPU TLS under any + reasonable pricing, and cheaper than GTLS. The exact dollar multiplier is pricing-dependent; + the throughput invariant is not. +- Still **EXPERIMENTAL** pending a full injection–recovery *completeness* campaign across a + (period, depth, ndata) grid (item D3). Three-regime SDE parity is strong evidence, not a + completeness proof. diff --git a/benchmarks/results/tls_survey_jul2026/fidelity_raw_a5000.txt b/benchmarks/results/tls_survey_jul2026/fidelity_raw_a5000.txt new file mode 100644 index 0000000..424ccc0 --- /dev/null +++ b/benchmarks/results/tls_survey_jul2026/fidelity_raw_a5000.txt @@ -0,0 +1,22 @@ +# TLS fidelity experiment raw output (A5000, 2026-07-06) +## strong signal (tess-ffi depth=0.005) + reference transitleastsquares SDE(identical)= 14.61 SDE(native)= 17.71 depthSNR= 0.03 recov=12/12 39077.1 ms/LC + cuvarbase t0os=3 (default) SDE ratio=0.99 depthSNR ratio=1000.85 speedup=17351x + cuvarbase t0os=33 (matched) SDE ratio=1.03 depthSNR ratio=1002.64 speedup=2914x +## marginal + narrow stress +########## MARGINAL depth=0.002 (near threshold), tess-ffi ########## +=== tess-ffi: ndata=1310 nperiods=2486 P_inj=7.70d depth=0.0020 (10 inj LCs) === + SDE(identical) = cuvarbase SDE recomputed on each method's chi2 spectrum; + cuvarbase t0os=3 (default) SDE(identical)= 12.01 SDE(native)= 12.01 depthSNR= 9.66 recov=10/10 2.6 ms/LC + cuvarbase t0os=33 (matched) SDE(identical)= 12.47 SDE(native)= 12.47 depthSNR= 9.75 recov=10/10 13.8 ms/LC + reference transitleastsquares SDE(identical)= 12.37 SDE(native)= 12.91 depthSNR= 0.01 recov=10/10 46222.2 ms/LC + cuvarbase t0os=3 (default) SDE ratio=0.97 depthSNR ratio=996.36 speedup=17475x + cuvarbase t0os=33 (matched) SDE ratio=1.01 depthSNR ratio=1006.34 speedup=3349x +########## NARROW transit, k2 (q~0.014) ########## +=== k2: ndata=4320 nperiods=9672 P_inj=12.40d depth=0.0040 (6 inj LCs) === + SDE(identical) = cuvarbase SDE recomputed on each method's chi2 spectrum; + cuvarbase t0os=3 (default) SDE(identical)= 25.23 SDE(native)= 25.23 depthSNR=37.74 recov=6/6 5.3 ms/LC + cuvarbase t0os=33 (matched) SDE(identical)= 26.11 SDE(native)= 26.11 depthSNR=37.92 recov=6/6 63.1 ms/LC + reference transitleastsquares SDE(identical)= 25.82 SDE(native)= 29.97 depthSNR= 0.03 recov=6/6 61444.9 ms/LC + cuvarbase t0os=3 (default) SDE ratio=0.98 depthSNR ratio=1236.37 speedup=11578x + cuvarbase t0os=33 (matched) SDE ratio=1.01 depthSNR ratio=1242.21 speedup=974x diff --git a/scripts/tls_fidelity_experiment.py b/scripts/tls_fidelity_experiment.py new file mode 100644 index 0000000..f7c5806 --- /dev/null +++ b/scripts/tls_fidelity_experiment.py @@ -0,0 +1,196 @@ +"""Apples-to-apples fidelity + timing: cuvarbase fast TLS vs reference +transitleastsquares on the SAME light curves and SAME period grid. + +The question is the detection statistic, not just recovery. cuvarbase's +fast path evaluates a COARSE epoch (t0) grid (t0_oversample=3 by +default) plus an exact refinement of the top candidate periods; the +reference steps t0 ~100x finer everywhere. Does coarsening cost SNR? + +To compare cleanly we hold the *statistic* fixed: cuvarbase and the +reference define the SR->SDE transform differently, so we recompute SDE +with cuvarbase.tls_stats on BOTH methods' chi2(period) spectra. The +only thing that then varies is the fidelity of the chi2 spectrum. We +also report a definition-free signal strength, the depth SNR at the +recovered period, sqrt(chi2_null - chi2_min). + +Runs, on identical injected light curves + one shared Ofir grid: + - cuvarbase fast, t0_oversample=3 (default) + - cuvarbase fast, t0_oversample=33 (reference-matched epoch grid) + - reference transitleastsquares + +Usage (GPU pod, batman + transitleastsquares installed): + python scripts/tls_fidelity_experiment.py [--regime tess-ffi] [--nlc 12] +""" +import argparse +import contextlib +import os +import sys +import time +import warnings +from multiprocessing import cpu_count + +warnings.filterwarnings('ignore') + +import numpy as np + +try: # keep our report lines from being clobbered by the C-ext stdout + sys.stdout.reconfigure(line_buffering=True) +except Exception: + pass + +REGIMES = { + 'tess-ffi': dict(ndata=1310, cadence=30. / 60 / 24, noise=1e-3, + pinj=7.7, depth=0.005, pmin=0.6, pmax=13.7), + 'k2': dict(ndata=4320, cadence=30. / 60 / 24, noise=8e-4, + pinj=12.4, depth=0.004, pmin=0.6, pmax=45.), +} + + +def make_lc(c, seed, inject=True): + rng = np.random.RandomState(seed) + t = np.arange(c['ndata']) * c['cadence'] + y = 1.0 + rng.randn(c['ndata']) * c['noise'] + if inject: + q = 0.0763 * c['pinj'] ** (-2.0 / 3.0) + t0 = 0.3 * c['pinj'] + rel = np.abs(((t - t0 + 0.5 * c['pinj']) % c['pinj']) + - 0.5 * c['pinj']) + y[rel < 0.5 * q * c['pinj']] -= c['depth'] + return t, y, np.full(c['ndata'], c['noise']) + + +def recovered(p_found, p_inj, tol=0.01): + for k in (1.0, 2.0, 0.5, 3.0, 1 / 3.0): + if abs(p_found - k * p_inj) / (k * p_inj) < tol: + return True + return False + + +def sde_identical(chi2, periods): + """cuvarbase's SDE, applied to any chi2(period) spectrum, so both + methods are scored by the identical statistic.""" + from cuvarbase import tls_stats + chi2 = np.asarray(chi2, dtype=float) + ok = np.isfinite(chi2) & (chi2 < 1e29) + c = chi2[ok] + best = int(np.argmin(c)) + stats = tls_stats.compute_all_statistics( + c, np.asarray(periods)[ok], best, 0.01, 0.1, 10) + return float(stats['SDE']) + + +def run_cuvarbase(lcs, periods, t0_oversample): + import pycuda.driver as cuda + from cuvarbase.tls import tls_search_batch + from cuvarbase.base import ensure_context + ensure_context() + cuda.Context.synchronize() + t0 = time.perf_counter() + res = tls_search_batch( + lcs, R_star=1.0, M_star=1.0, periods=periods, + t0_oversample=t0_oversample, refine_top_k=50, + return_arrays=True) + cuda.Context.synchronize() + ms = (time.perf_counter() - t0) / len(lcs) * 1000 + rows = [] + for r in res: + if 'error' in r: + rows.append(None); continue + sde_id = sde_identical(r['chi2'], r['periods']) + # depth SNR = sqrt(chi2_null - chi2_min); chi2_null ~ max over grid + cfin = np.asarray(r['chi2'])[np.isfinite(r['chi2'])] + dsnr = float(np.sqrt(max(cfin.max() - r['chi2_min'], 0.0))) + rows.append(dict(period=r['period'], sde_native=r['SDE'], + sde_id=sde_id, dsnr=dsnr)) + return rows, ms + + +def run_reference(lcs, periods): + from transitleastsquares import transitleastsquares + pmin, pmax = float(periods.min()), float(periods.max()) + rows = [] + t_tot = 0.0 + for (t, y, dy) in lcs: + model = transitleastsquares(t, y, dy) + t0 = time.perf_counter() + with open(os.devnull, 'w') as dn, contextlib.redirect_stdout(dn): + r = model.power(R_star=1.0, M_star=1.0, + period_min=pmin, period_max=pmax, + oversampling_factor=3, use_threads=cpu_count(), + show_progress_bar=False) + t_tot += time.perf_counter() - t0 + chi2 = np.asarray(getattr(r, 'chi2')) + pers = np.asarray(getattr(r, 'periods')) + sde_id = sde_identical(chi2, pers) + cmin = float(np.nanmin(chi2)) + dsnr = float(np.sqrt(max(np.nanmax(chi2) - cmin, 0.0))) + rows.append(dict(period=float(r.period), sde_native=float(r.SDE), + sde_id=sde_id, dsnr=dsnr)) + return rows, t_tot / len(lcs) * 1000 + + +def report(tag, rows, ms, p_inj, n_inj): + inj = [r for r in rows[:n_inj] if r] + rec = sum(recovered(r['period'], p_inj) for r in inj) + sid = np.median([r['sde_id'] for r in inj]) + snat = np.median([r['sde_native'] for r in inj]) + dsnr = np.median([r['dsnr'] for r in inj]) + print(" %-38s SDE(identical)=%6.2f SDE(native)=%6.2f " + "depthSNR=%5.2f recov=%d/%d %8.1f ms/LC" + % (tag, sid, snat, dsnr, rec, len(inj), ms)) + return dict(tag=tag, sde_id=sid, sde_native=snat, dsnr=dsnr, + recovered=rec, n=len(inj), ms=ms) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--regime', default='tess-ffi', choices=list(REGIMES)) + ap.add_argument('--nlc', type=int, default=12) + ap.add_argument('--depth', type=float, default=None, + help='override injection depth (test marginal signals)') + ap.add_argument('--skip-reference', action='store_true') + args = ap.parse_args() + + from cuvarbase import tls_grids + cfg = dict(REGIMES[args.regime]) + if args.depth is not None: + cfg['depth'] = args.depth + n_inj = args.nlc + lcs = [make_lc(cfg, 5000 + i, inject=True) for i in range(n_inj)] + lcs += [make_lc(cfg, 9000 + i, inject=False) for i in range(3)] + + periods = tls_grids.period_grid_ofir( + lcs[0][0], R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=cfg['pmin'], period_max=cfg['pmax']) + print("\n=== %s: ndata=%d nperiods=%d P_inj=%.2fd depth=%.4f " + "(%d inj LCs) ===" % (args.regime, cfg['ndata'], len(periods), + cfg['pinj'], cfg['depth'], n_inj)) + print(" SDE(identical) = cuvarbase SDE recomputed on each method's " + "chi2 spectrum;\n depthSNR = sqrt(chi2_null - chi2_min) at " + "the recovered period.\n") + + _ = run_cuvarbase(lcs[:2], periods, 3.0) + _ = run_cuvarbase(lcs[:2], periods, 33.0) + + out = [] + r, ms = run_cuvarbase(lcs, periods, 3.0) + out.append(report("cuvarbase t0os=3 (default)", r, ms, cfg['pinj'], n_inj)) + r, ms = run_cuvarbase(lcs, periods, 33.0) + out.append(report("cuvarbase t0os=33 (matched)", r, ms, cfg['pinj'], n_inj)) + if not args.skip_reference: + r, ms = run_reference(lcs, periods) + out.append(report("reference transitleastsquares", r, ms, cfg['pinj'], n_inj)) + + ref = next((o for o in out if 'reference' in o['tag']), None) + if ref: + print("\n --- vs reference (identical-SDE basis) ---") + for o in out: + if 'cuvarbase' in o['tag']: + print(" %-32s SDE ratio=%.2f depthSNR ratio=%.2f " + "speedup=%.0fx" + % (o['tag'], o['sde_id'] / ref['sde_id'], + o['dsnr'] / ref['dsnr'], ref['ms'] / o['ms'])) + + +if __name__ == '__main__': + main() diff --git a/scripts/tls_matched_timing.py b/scripts/tls_matched_timing.py new file mode 100644 index 0000000..72508a5 --- /dev/null +++ b/scripts/tls_matched_timing.py @@ -0,0 +1,78 @@ +"""Matched-fidelity throughput: cuvarbase fast TLS at the default coarse +epoch grid (t0_oversample=3) vs a reference-matched grid (t0_oversample=33) +on the compute-heavy regimes. cuvarbase only, no reference (reference is +>15 min/LC on Kepler). Gives the matched-fidelity ms/LC for the +apples-to-apples GTLS comparison. +""" +import argparse +import time +import warnings + +warnings.filterwarnings('ignore') + +import numpy as np + +REGIMES = { + 'tess-yr': dict(ndata=16850, cadence=30. / 60 / 24, noise=1e-3, + pinj=21.7, depth=0.004, pmin=0.6, pmax=175.), + 'kepler-4yr': dict(ndata=65440, cadence=30. / 60 / 24, noise=6e-4, + pinj=41.3, depth=0.003, pmin=0.6, pmax=500.), +} + + +def make_lc(c, seed): + rng = np.random.RandomState(seed) + t = np.arange(c['ndata']) * c['cadence'] + y = 1.0 + rng.randn(c['ndata']) * c['noise'] + q = 0.0763 * c['pinj'] ** (-2.0 / 3.0) + t0 = 0.3 * c['pinj'] + rel = np.abs(((t - t0 + 0.5 * c['pinj']) % c['pinj']) - 0.5 * c['pinj']) + y[rel < 0.5 * q * c['pinj']] -= c['depth'] + return t, y, np.full(c['ndata'], c['noise']) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--nlc', type=int, default=4) + args = ap.parse_args() + + import pycuda.driver as cuda + from cuvarbase.base import ensure_context + from cuvarbase import tls_grids + from cuvarbase.tls import tls_search_batch + ensure_context() + + def timed(lcs, periods, os_): + cuda.Context.synchronize() + t0 = time.perf_counter() + res = tls_search_batch(lcs, R_star=1.0, M_star=1.0, periods=periods, + t0_oversample=os_, refine_top_k=50, + return_arrays=False) + cuda.Context.synchronize() + ms = (time.perf_counter() - t0) / len(lcs) * 1000 + rec = sum(abs(r['period'] - c['pinj']) / c['pinj'] < 0.01 + for r in res if 'error' not in r) + return ms, rec + + print("\n%-12s %8s %10s %10s %8s %s" + % ("regime", "nperiods", "t0os=3 ms", "t0os=33 ms", "factor", + "recov (3/33)")) + print("-" * 74) + for name, c in REGIMES.items(): + globals()['c'] = c + lcs = [make_lc(c, 4000 + i) for i in range(args.nlc)] + periods = tls_grids.period_grid_ofir( + lcs[0][0], R_star=1.0, M_star=1.0, oversampling_factor=3, + period_min=c['pmin'], period_max=c['pmax']) + # warmups (compile both band sets) + timed(lcs[:1], periods, 3.0) + timed(lcs[:1], periods, 33.0) + ms3, r3 = timed(lcs, periods, 3.0) + ms33, r33 = timed(lcs, periods, 33.0) + print("%-12s %8d %10.1f %10.1f %7.1fx %d/%d, %d/%d" + % (name, len(periods), ms3, ms33, ms33 / ms3, + r3, args.nlc, r33, args.nlc)) + + +if __name__ == '__main__': + main() From 5cc8e8d3470ea3e758a83e92b17359b5325fd501 Mon Sep 17 00:00:00 2001 From: John Hoffman Date: Mon, 6 Jul 2026 22:44:56 -0500 Subject: [PATCH 3/6] TLS: apples-to-apples GTLS Fig.7 reproduction (30-171x faster at equal SDE) Reproduces arXiv:2607.00348 Fig.7 on one RTX A5000 with a matched search: identical Ofir period grid, identical per-period duration window, matched epoch density (t0_oversample=8 == GTLS skip=8), and one injected transit fed to every method. One identical SDE routine scores every spectrum. cuvarbase-TLS-matched is 30 -> 171x faster than GTLS-skip8 across 200->2000 d (growing with baseline; GTLS scales ~N^2.5 from per-batch kernel-launch overhead, cuvarbase ~linear), at 1-3% SDE parity and 100% recovery. It is also 23-40x faster than the paper's own RTX-4090 GTLS numbers (immune to the A5000-vs-4090 question). The paper's Fig.7 used GTLS skip=8, not full-scan. Documents the paper's BLS-comparison caveat: its Kunimoto qmin=2e-4/noverlap=3 config forces up to 5000 phase bins (sub-cadence durations for 30-min data), cheap for GTLS's cumsum but a 25x penalty for cuvarbase-BLS's per-duration re-binning, and noverlap=3 bypasses the fused kernel. A sensible BLS config (qmin=2e-3, fused noverlap=2) is faster than both TLS and GTLS; our improved batched BLS at the paper's exact config is ~23x faster than their 121.1 s. Adds analysis/GTLS_COMPARISON.md + the reproduced figure, the benchmark under scripts/gtls_benchmark/ (BLS curves require feature/bls-survey-speed), and the raw July-2026 A5000 result JSONs. Co-Authored-By: Claude Opus 4.8 (1M context) --- analysis/GTLS_COMPARISON.md | 214 ++++++ analysis/gtls_fig7_reproduction.png | Bin 0 -> 239378 bytes .../gtls_comparison_jul2026/results_cuv.json | 667 ++++++++++++++++++ .../gtls_comparison_jul2026/results_gtls.json | 194 +++++ .../results_gtls_skip8_big.json | 98 +++ scripts/gtls_benchmark/README.md | 39 + scripts/gtls_benchmark/bench_core.py | 182 +++++ scripts/gtls_benchmark/gtls_apples_bench.py | 393 +++++++++++ scripts/gtls_benchmark/plot_fig7.py | 142 ++++ 9 files changed, 1929 insertions(+) create mode 100644 analysis/GTLS_COMPARISON.md create mode 100644 analysis/gtls_fig7_reproduction.png create mode 100644 benchmarks/results/gtls_comparison_jul2026/results_cuv.json create mode 100644 benchmarks/results/gtls_comparison_jul2026/results_gtls.json create mode 100644 benchmarks/results/gtls_comparison_jul2026/results_gtls_skip8_big.json create mode 100644 scripts/gtls_benchmark/README.md create mode 100644 scripts/gtls_benchmark/bench_core.py create mode 100644 scripts/gtls_benchmark/gtls_apples_bench.py create mode 100644 scripts/gtls_benchmark/plot_fig7.py diff --git a/analysis/GTLS_COMPARISON.md b/analysis/GTLS_COMPARISON.md new file mode 100644 index 0000000..b7ff29b --- /dev/null +++ b/analysis/GTLS_COMPARISON.md @@ -0,0 +1,214 @@ +# cuvarbase vs GTLS — apples-to-apples reproduction of the GTLS Fig. 7 benchmark + +**What this is.** GTLS (Hu, Ge, Jin & Willis, arXiv:2607.00348, submitted 1 Jul 2026) +is the first and only *other* GPU implementation of Transit Least Squares — a CuPy +reimplementation of Hippke & Heller's (2019) TLS (`pip install gputls`, v0.5.1). +Their Fig. 7 reports single-light-curve search time vs light-curve baseline for +GTLS, reference CPU-TLS, and cuvarbase's GPU-BLS. This document reproduces that +figure **on one GPU, holding the search fair**, using our improved TLS +(`feature/tls-fast-survey`) and improved BLS (`feature/bls-survey-speed`). + +**Figure:** `gtls_fig7_reproduction.png` (this directory). Benchmark: `scripts/gtls_benchmark/`. Raw data: `benchmarks/results/gtls_comparison_jul2026/`. + +All measurements: single RTX A5000 (24 GB, sm_86), CUDA 12.x, cupy 13.6, +one injected batman transit per baseline (P=8.13 d, depth=4e-3, 110–400 ppm-class +noise, Keplerian-consistent duration so both grids bracket it), 30-min cadence. +GTLS ran on the *same* A5000 as cuvarbase, so all ratios below are same-hardware. + +--- + +## 1. The fairness protocol (what "apples-to-apples" required) + +The GTLS paper's absolute numbers are on an RTX 4090 (GTLS/BLS) and a Ryzen 7950X +(CPU-TLS). Rather than trust cross-hardware ratios, we run **every method on the +same A5000** and equalize the *search*, not just the hardware. Five knobs had to +be matched (each was a real gap): + +| axis | GTLS | cuvarbase default | how we matched it | +|---|---|---|---| +| **period grid** | Ofir, os=3, Pmax=S/2 | Ofir, os=3 | identical: the *same* array passed to all methods (grids already agreed to 0.05%: 191,837 vs 191,742 at 1500 d) | +| **epoch (T0) density** | `T0_fit_margin` → SKIP_POINT = 8 epochs/duration (default); =0 → every cadence (paper Fig 7) | `t0_oversample`=3 | cuvarbase-matched uses `t0_oversample=8`; GTLS run at both settings | +| **duration grid** | ~36/period over a q-window of ratio ~31 (log-1.1) | 15 over [0.5q,2q] | cuvarbase-matched uses `n_durations=38` and per-period `qmin/qmax` = GTLS's own kernel window | +| **template** | Hippke reference LD (a=23.1, b≈0.32) | LD (a=15, b=0) | left as-is — measured to cost <3% SDE (below) | +| **light curve / SNR** | — | — | one injected transit per baseline, fed to *all* methods → identical SNR by construction | + +Every method's chi²(P) (or BLS power) spectrum is additionally re-scored with **one +identical SDE routine**, so "detection significance" means the same thing for all. + +**On the epoch axis (the crux).** GTLS exposes epoch density through +`T0_fit_margin`: the default 0.125 compiles to `SKIP_POINT=8` = **8 trial epochs +per transit duration** in the coarse SDE scan; `T0_fit_margin=0` scans **every +cadence** (its most expensive O(N²) mode). We measured both. Our A5000 +`gtls_full` numbers (393 s at 1000 d) extrapolate to ~1200 s at 1500 d — 35× the +paper's 33.3 s, implausible even after hardware — whereas `gtls_skip8` (76 s at +1000 d → ~155 s at 1500 d, ≈60 s hardware-adjusted for a 4090) lands within ~2× of +the paper. **So the paper's Fig. 7 used GTLS's *default* (skip=8), not full-scan.** +The true apples-to-apples is therefore **cuvarbase-TLS at `t0_oversample=8` vs +GTLS-skip8** (both = 8 epochs/duration); `gtls_full` is shown only as a "finest +epoch" upper curve. + +--- + +## 2. Results — runtime (per light curve, same A5000) + +Per-light-curve search time, all on the same A5000 (GTLS `full`/`skip8` measured +directly through 1000 d; `skip8` also at the paper's 1500/2000/3000 d anchors; +`full` beyond 1000 d omitted — it reaches ~20 min/point): + +| baseline | GTLS full | **GTLS skip8 (paper cfg)** | **cuv TLS matched** | cuv TLS default | cuv BLS (Kunimoto) | cuv BLS (sensible) | +|---:|---:|---:|---:|---:|---:|---:| +| 200 d | 5.9 s | 4.1 s | **0.138 s** | 0.041 s | 0.538 s | 0.011 s | +| 500 d | 60.2 s | 22.3 s | **0.402 s** | 0.119 s | 1.485 s | 0.032 s | +| 1000 d | 392.6 s| 75.8 s | **0.883 s** | 0.232 s | 3.279 s | 0.101 s | +| 1500 d | (~1200 s*) | 177.9 s | **1.437 s** | 0.409 s | 5.292 s | 0.207 s | +| 2000 d | — | 348.3 s | 2.037 s | 0.627 s | 7.499 s | 0.346 s | +| 3000 d | — | (~830 s*) | 3.460 s | 1.162 s | 12.626 s | 0.730 s | + +\* extrapolated. GTLS scales **super-quadratically** (measured exponent ≈2.5–2.7), +because on a 24 GB GPU long light curves force tiny period batches → thousands of +Python-driven per-batch kernel launches. cuvarbase scales cleanly ~linearly. +(For reference the paper's own 4090 GTLS points are 33.3 s @1500 d and 138 s +@3000 d — i.e. skip=8 on faster hardware.) + +**Speedup, cuvarbase-TLS-matched vs GTLS-skip8 (same A5000, matched 8 +epochs/duration, matched durations & period grid, equal SDE):** + +| baseline | 200 | 500 | 1000 | 1500 | 2000 | +|---|---|---|---|---|---| +| **speedup** | **30×** | **55×** | **86×** | **124×** | **171×** | + +The epoch-matched speedup *grows monotonically* with baseline (GTLS's per-call +recompile + launch overhead compound); cuv-TLS *default* is a further ~3–4× on top, +and vs GTLS-*full* the ratio is 43× → 150× → 445×. + +**Cross-check against the paper's own hardware (immune to the A5000-vs-4090 +question).** Take the paper's *published* GTLS numbers on its RTX 4090 and compare +to cuvarbase on our *slower* A5000: + +| baseline | paper GTLS (RTX 4090) | cuvarbase-TLS-matched (A5000) | cuvarbase wins by | +|---|---|---|---| +| 1500 d | 33.3 s | 1.44 s | **23×** | +| 3000 d | 138 s | 3.46 s | **40×** | + +cuvarbase on the weaker GPU already beats GTLS on the stronger GPU by 23–40× — and +would widen further on matched hardware. (Our *same-GPU* GTLS is ~5× slower than +the paper's 4090 GTLS, more than the ~2× hardware gap: GTLS's runtime is dominated +by per-batch kernel-launch overhead that is very GPU/driver/CuPy-version-sensitive. +We anchor on both the same-GPU ratio and this paper-hardware cross-check so the +conclusion holds either way.) + +**Bonus — improved BLS.** At the paper's *exact* Kunimoto BLS config, our July +`feature/bls-survey-speed` batched BLS runs **5.3 s @1500 d on the A5000 vs the +paper's reported 121.1 s cuvarbase-BLS on a 4090 — ~23× faster on weaker +hardware** (opt1–opt4 + batched kernel; the paper's exact cuvarbase entry point / +version is unspecified). + +## 3. Results — detection significance (SDE parity) + +Scored by the one identical statistic, **every method agrees closely at every +baseline** — GTLS vs cuvarbase-TLS to ~1–3%, and the full 6-method spread (which +includes BLS, whose box template scores marginally higher on this signal) ≤~10%: + +| baseline | SDE: GTLS-skip8 / cuv-TLS-matched | full 6-method spread | +|---:|---|---| +| 200 d | 34.2 / 33.8 (−1%) | 33.4 – 35.2 | +| 500 d | 53.4 / 53.2 (−0.4%) | 53.1 – 56.5 | +| 1000 d | 89.5 / 88.7 (−0.9%) | 86.6 – 93.4 | +| 1500 d | — / 103.5 | 99.9 – 110.9 | +| 3000 d | — / 150.4 | 150.2 – 161.7 | + +100% recovery of the injected period in all cells. So the large speed gaps are +**not** bought with sensitivity — the whole point of the fair comparison. This +independently corroborates the parallel session's finding (commit c4d10ff) that +cuvarbase's coarse fast path sits within 1–3% of *reference CPU-TLS* SDE; here we +see the same ≤3% parity against *GTLS*. + +--- + +## 4. What GTLS does differently from cuvarbase + +Both implement the same TLS math (fold → limb-darkened template → χ² → SDE), and +several high-level strategies match (Ofir period grid; hierarchical coarse-then- +refine T0; a moving-average depth estimate). The differences that matter: + +**GTLS design choices** +- **Single-light-curve, per-call CuPy JIT.** `gtls(t,y).power()` compiles its + CUDA (`cp.RawModule(...).compile()`) on *every* call — no cross-call caching, + no batch API. Fine for one star, costly for a survey. +- **float32 throughout + `(int)` phase fold.** The fold is + `phase = t/P − (int)(t/P)` (truncation, not floor → wrong for t<0 / raw BKJD), + and cumulative sums / residuals accumulate in float32 over up to ~150k points. +- **cumsum moving average** for O(1) in-window depth at any duration; a global + log-1.1 duration grid masked per-period; edge padding + an explicit + edge-effect χ² subtraction for wrap-around transits. +- **Multi-GPU** via `subprocess` per device splitting the period grid (their 79 s + dual-4090 number). Only the coarse scan is parallelized; refinement is 1-GPU. + +**cuvarbase design choices (why it wins)** +- **Batch-native + cached kernels.** One kernel launch over *all* light curves, + one block per (period, LC); LRU-cached compiled kernels. Amortizes launch and + compile — the dominant survey costs. +- **Float-float (t_hi, t_lo) fold**: pure-FP32 FMA fold with 3e-8 phase error at a + 1400-d baseline, vs GTLS's float32/truncation fold (which drifts and mishandles + negative epochs). +- **Fold-once, phase-binned scan** with integrated-template tables (S1=∫T, + S2=∫T²): all durations and epochs come from a single fold, so finer duration + grids are nearly free. This is the same asymptotic trick as GTLS's cumsum but + applied inside a batched, bank-conflict-aware shared-memory kernel. +- **Clean ~linear scaling** in baseline; no period-batch/launch cliff. +- **Exact top-K refinement** kept off the SDE spectrum (SDE from the uniform + coarse grid), so precision is refined without deflating significance. + +Net: cuvarbase and GTLS share the *algorithm*; cuvarbase's *engineering* +(batching, kernel caching, FF fold, single-fold scan) is a generation ahead, and +that shows up as 1–2 orders of magnitude in wall-clock at equal detection. + +--- + +## 5. The BLS comparison — a fairness caveat in the paper + +The GTLS paper concludes "GTLS is 3.6× faster than GPU-BLS" (33.3 s vs 121.1 s at +1500 d). Its BLS is **cuvarbase** run with **Kunimoto et al. (2023, QLP DR notes +003, RNAAS 7:28)** parameters: `qmin=2e-4, qmax=0.15, dlogq=0.1, noverlap=3`. + +That comparison flatters GTLS: +- `qmin=2e-4` searches transit durations down to 0.02% of the period — *sub- + cadence* for 30-min data (~2.9 min at P=10 d). GTLS's own grid also goes that + fine, but its cumsum moving-average makes fine durations O(1); cuvarbase's BLS + kernel re-bins the folded curve into up to `1/qmin = 5000` phase bins per + duration level, so its cost scales with `1/qmin`. Same qmin, wildly different + cost. (Measured: BLS 1.47 s at qmin=2e-4 vs **0.059 s** at qmin=4e-3, 500 d.) +- `noverlap=3` is not a power of two, so it bypasses cuvarbase's fastest *fused* + BLS kernel (opt1) and runs 3 separate phase passes. +- The paper predates our July BLS optimizations (opt1–opt4). + +With a physically sensible BLS config for 30-min data (`qmin=2e-3` ≈ one cadence, +fused `noverlap=2`), cuvarbase-BLS runs **0.01–0.73 s** across 200–3000 d — faster +than TLS (as expected: box < template) and faster than GTLS. So the paper's BLS +result is config- and version-contingent, not fundamental. The clean, meaningful +comparison is **TLS-vs-TLS** (GTLS vs cuvarbase-TLS), where cuvarbase wins outright. + +--- + +## 6. What (if anything) to adopt from GTLS + +- **Multi-GPU scale-out.** The one capability GTLS has that cuvarbase TLS lacks. + Low priority (cuvarbase is already ~100× faster single-GPU and batches many LCs + per launch), but a clean win for the very largest surveys — and easy, since + cuvarbase's batch grid splits trivially across devices. +- **Richer SNR outputs** (GTLS returns snr / snrPink / snrFit / snrFitPink). Nice- + to-have reporting, not performance. +- **Nothing algorithmic.** GTLS's core tricks (Ofir grid, cumsum depth, coarse+ + refine T0) are already present in cuvarbase, generally in a more robust form. + Their float32/truncation fold and per-call recompile are things to *avoid*, not + adopt. + +## 7. Bottom line + +At **matched search space, matched epoch density, and equal SDE**, cuvarbase's TLS +is **tens to >100× faster than GTLS on the same GPU**, and its advantage grows with +baseline because GTLS's per-call recompile and period-batch launch overhead scale +super-quadratically while cuvarbase scales linearly. cuvarbase is also numerically +more robust (FF fold vs float32/int-truncation). The GTLS paper's BLS comparison is +not cost-matched and flatters GTLS; the honest, apples-to-apples story is that +cuvarbase is the faster GPU TLS by a wide, sensitivity-neutral margin. diff --git a/analysis/gtls_fig7_reproduction.png b/analysis/gtls_fig7_reproduction.png new file mode 100644 index 0000000000000000000000000000000000000000..e7a61150912c4abf5b29592032c1252d8d2c1867 GIT binary patch literal 239378 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%0DIt2ADMMAF`y{Cdu*cFEN^)$?UuK0!=#NN@>J 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

(u$ngy~CmNQ%O1?Sii|^900Ie)Xgy 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 CO2Co; literal 0 HcmV?d00001 diff --git a/benchmarks/results/gtls_comparison_jul2026/results_cuv.json b/benchmarks/results/gtls_comparison_jul2026/results_cuv.json new file mode 100644 index 0000000..8970897 --- /dev/null +++ b/benchmarks/results/gtls_comparison_jul2026/results_cuv.json @@ -0,0 +1,667 @@ +{ + "script": "gtls_apples_bench.py", + "timestamp": "2026-07-06T20:24:53", + "inj": { + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "u": [ + 0.4804, + 0.1867 + ], + "cadence_days": 0.020833333333333332 + }, + "match_template": false, + "baselines": [ + 200, + 500, + 1000, + 1500, + 2000, + 3000 + ], + "results": { + "200": { + "meta": { + "ndata": 9600, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 200, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 13.653358801550656 + }, + "nperiods": 23057, + "n_dur_matched": 38, + "methods": { + "cuv_tls_matched": { + "method": "cuv_tls_matched", + "time_s": 0.13755610957741737, + "batch_time_s": 0.13755610957741737, + "times_s": [ + 0.14206748269498348, + 0.13755610957741737, + 0.13514303229749203 + ], + "n_lcs": 1, + "t0_oversample": 8.0, + "n_durations": 38, + "period_native": 8.130030632019043, + "sde_native": 33.59562901812796, + "sde_identical": 33.75055979295671, + "best_period_identical": 8.130030632019043, + "depth_snr": 15.89531655761043, + "recovered": true, + "recovered_native": true, + "n_periods": 23057 + }, + "cuv_tls_default": { + "method": "cuv_tls_default", + "time_s": 0.04108639247715473, + "batch_time_s": 0.04108639247715473, + "times_s": [ + 0.04108639247715473, + 0.0410964023321867, + 0.040234677493572235 + ], + "n_lcs": 1, + "t0_oversample": 3.0, + "n_durations": 15, + "period_native": 8.130030632019043, + "sde_native": 33.292362285426535, + "sde_identical": 33.44892897086804, + "best_period_identical": 8.130030632019043, + "depth_snr": 15.484280367503382, + "recovered": true, + "recovered_native": true, + "n_periods": 23057 + }, + "cuv_bls_kunimoto": { + "method": "cuv_bls_kunimoto", + "cfg": "kunimoto", + "time_s": 0.5383373517543077, + "batch_time_s": 0.5383373517543077, + "times_s": [ + 0.5379391890019178, + 0.538496345281601, + 0.5383373517543077 + ], + "n_lcs": 1, + "n_periods": 23057, + "sde_identical": 35.2025799976132, + "best_period_identical": 8.13003035214252, + "recovered": true, + "qcfg": { + "qmin": 0.0002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 3 + } + }, + "cuv_bls_matched": { + "method": "cuv_bls_matched", + "cfg": "matched", + "time_s": 0.010561700910329819, + "batch_time_s": 0.010561700910329819, + "times_s": [ + 0.010561700910329819, + 0.011295026168227196, + 0.010273467749357224 + ], + "n_lcs": 1, + "n_periods": 23057, + "sde_identical": 34.70987328513982, + "best_period_identical": 8.13003035214252, + "recovered": true, + "qcfg": { + "qmin": 0.002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 2 + } + } + } + }, + "500": { + "meta": { + "ndata": 24000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 500, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 21.767011164065 + }, + "nperiods": 61017, + "n_dur_matched": 38, + "methods": { + "cuv_tls_matched": { + "method": "cuv_tls_matched", + "time_s": 0.40233735367655754, + "batch_time_s": 0.40233735367655754, + "times_s": [ + 0.4016232769936323, + 0.4110007472336292, + 0.40233735367655754 + ], + "n_lcs": 1, + "t0_oversample": 8.0, + "n_durations": 38, + "period_native": 8.130175590515137, + "sde_native": 53.01878151851455, + "sde_identical": 53.14530587775627, + "best_period_identical": 8.130175590515137, + "depth_snr": 22.741917673107043, + "recovered": true, + "recovered_native": true, + "n_periods": 61017 + }, + "cuv_tls_default": { + "method": "cuv_tls_default", + "time_s": 0.11858376115560532, + "batch_time_s": 0.11858376115560532, + "times_s": [ + 0.11858376115560532, + 0.11885972693562508, + 0.11000512167811394 + ], + "n_lcs": 1, + "t0_oversample": 3.0, + "n_durations": 15, + "period_native": 8.130175590515137, + "sde_native": 53.26162239029376, + "sde_identical": 53.38806041519828, + "best_period_identical": 8.129351615905762, + "depth_snr": 22.191700304067716, + "recovered": true, + "recovered_native": true, + "n_periods": 61017 + }, + "cuv_bls_kunimoto": { + "method": "cuv_bls_kunimoto", + "cfg": "kunimoto", + "time_s": 1.4846770018339157, + "batch_time_s": 1.4846770018339157, + "times_s": [ + 1.4818594921380281, + 1.4846770018339157, + 1.4850654806941748 + ], + "n_lcs": 1, + "n_periods": 61017, + "sde_identical": 56.52696500954521, + "best_period_identical": 8.130175631606068, + "recovered": true, + "qcfg": { + "qmin": 0.0002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 3 + } + }, + "cuv_bls_matched": { + "method": "cuv_bls_matched", + "cfg": "matched", + "time_s": 0.03246651217341423, + "batch_time_s": 0.03246651217341423, + "times_s": [ + 0.03197992965579033, + 0.03349187970161438, + 0.03246651217341423 + ], + "n_lcs": 1, + "n_periods": 61017, + "sde_identical": 55.77652871561681, + "best_period_identical": 8.130175631606068, + "recovered": true, + "qcfg": { + "qmin": 0.002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 2 + } + } + } + }, + "1000": { + "meta": { + "ndata": 48000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 1000, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 30.909105594304876 + }, + "nperiods": 125932, + "n_dur_matched": 38, + "methods": { + "cuv_tls_matched": { + "method": "cuv_tls_matched", + "time_s": 0.8827326986938715, + "batch_time_s": 0.8827326986938715, + "times_s": [ + 0.881113650277257, + 0.8827326986938715, + 0.8842885997146368 + ], + "n_lcs": 1, + "t0_oversample": 8.0, + "n_durations": 38, + "period_native": 8.129958152770996, + "sde_native": 88.61718125069527, + "sde_identical": 88.71963989110473, + "best_period_identical": 8.129958152770996, + "depth_snr": 31.712350389535917, + "recovered": true, + "recovered_native": true, + "n_periods": 125932 + }, + "cuv_tls_default": { + "method": "cuv_tls_default", + "time_s": 0.23190558142960072, + "batch_time_s": 0.23190558142960072, + "times_s": [ + 0.23190558142960072, + 0.24890235997736454, + 0.2289747204631567 + ], + "n_lcs": 1, + "t0_oversample": 3.0, + "n_durations": 15, + "period_native": 8.129958152770996, + "sde_native": 86.52490525299153, + "sde_identical": 86.62860999090962, + "best_period_identical": 8.130370140075684, + "depth_snr": 30.684333707845962, + "recovered": true, + "recovered_native": true, + "n_periods": 125932 + }, + "cuv_bls_kunimoto": { + "method": "cuv_bls_kunimoto", + "cfg": "kunimoto", + "time_s": 3.2789248321205378, + "batch_time_s": 3.2789248321205378, + "times_s": [ + 3.278851928189397, + 3.2854464910924435, + 3.2789248321205378 + ], + "n_lcs": 1, + "n_periods": 125932, + "sde_identical": 93.42207706283025, + "best_period_identical": 8.129958453040548, + "recovered": true, + "qcfg": { + "qmin": 0.0002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 3 + } + }, + "cuv_bls_matched": { + "method": "cuv_bls_matched", + "cfg": "matched", + "time_s": 0.10147341340780258, + "batch_time_s": 0.10147341340780258, + "times_s": [ + 0.10238155163824558, + 0.10147341340780258, + 0.10146233811974525 + ], + "n_lcs": 1, + "n_periods": 125932, + "sde_identical": 92.4722290623255, + "best_period_identical": 8.129958453040548, + "recovered": true, + "qcfg": { + "qmin": 0.002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 2 + } + } + } + }, + "1500": { + "meta": { + "ndata": 72000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 1500, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 37.804438676647734 + }, + "nperiods": 191742, + "n_dur_matched": 38, + "methods": { + "cuv_tls_matched": { + "method": "cuv_tls_matched", + "time_s": 1.4373753536492586, + "batch_time_s": 1.4373753536492586, + "times_s": [ + 1.4392143823206425, + 1.4373753536492586, + 1.43576823733747 + ], + "n_lcs": 1, + "t0_oversample": 8.0, + "n_durations": 38, + "period_native": 8.130034446716309, + "sde_native": 103.42288798192644, + "sde_identical": 103.51420005935486, + "best_period_identical": 8.130034446716309, + "depth_snr": 40.07062782524991, + "recovered": true, + "recovered_native": true, + "n_periods": 191742 + }, + "cuv_tls_default": { + "method": "cuv_tls_default", + "time_s": 0.40924578718841076, + "batch_time_s": 0.40924578718841076, + "times_s": [ + 0.4187461659312248, + 0.40924578718841076, + 0.4073683172464371 + ], + "n_lcs": 1, + "t0_oversample": 3.0, + "n_durations": 15, + "period_native": 8.130034446716309, + "sde_native": 99.8471592139927, + "sde_identical": 99.93955063884552, + "best_period_identical": 8.130034446716309, + "depth_snr": 38.40967211209207, + "recovered": true, + "recovered_native": true, + "n_periods": 191742 + }, + "cuv_bls_kunimoto": { + "method": "cuv_bls_kunimoto", + "cfg": "kunimoto", + "time_s": 5.291578171774745, + "batch_time_s": 5.291578171774745, + "times_s": [ + 5.279095048084855, + 5.291578171774745, + 5.304737301543355 + ], + "n_lcs": 1, + "n_periods": 191742, + "sde_identical": 107.46817842744791, + "best_period_identical": 8.13003429185609, + "recovered": true, + "qcfg": { + "qmin": 0.0002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 3 + } + }, + "cuv_bls_matched": { + "method": "cuv_bls_matched", + "cfg": "matched", + "time_s": 0.20667587965726852, + "batch_time_s": 0.20667587965726852, + "times_s": [ + 0.20533782057464123, + 0.20667587965726852, + 0.20865457504987717 + ], + "n_lcs": 1, + "n_periods": 191742, + "sde_identical": 110.94920831744149, + "best_period_identical": 8.13003429185609, + "recovered": true, + "qcfg": { + "qmin": 0.002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 2 + } + } + } + }, + "2000": { + "meta": { + "ndata": 96000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 2000, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 43.712076332288056 + }, + "nperiods": 258051, + "n_dur_matched": 38, + "methods": { + "cuv_tls_matched": { + "method": "cuv_tls_matched", + "time_s": 2.0367227513343096, + "batch_time_s": 2.0367227513343096, + "times_s": [ + 2.063674498349428, + 2.0367227513343096, + 2.035205539315939 + ], + "n_lcs": 1, + "t0_oversample": 8.0, + "n_durations": 38, + "period_native": 8.130036354064941, + "sde_native": 117.45164141534184, + "sde_identical": 117.53842896522384, + "best_period_identical": 8.130036354064941, + "depth_snr": 44.88384804589285, + "recovered": true, + "recovered_native": true, + "n_periods": 258051 + }, + "cuv_tls_default": { + "method": "cuv_tls_default", + "time_s": 0.6267105527222157, + "batch_time_s": 0.6267105527222157, + "times_s": [ + 0.6267105527222157, + 0.6304767020046711, + 0.6124172136187553 + ], + "n_lcs": 1, + "t0_oversample": 3.0, + "n_durations": 15, + "period_native": 8.130036354064941, + "sde_native": 116.67198138296588, + "sde_identical": 116.75982396228854, + "best_period_identical": 8.129830360412598, + "depth_snr": 43.67934833927807, + "recovered": true, + "recovered_native": true, + "n_periods": 258051 + }, + "cuv_bls_kunimoto": { + "method": "cuv_bls_kunimoto", + "cfg": "kunimoto", + "time_s": 7.498604532331228, + "batch_time_s": 7.498604532331228, + "times_s": [ + 7.498385973274708, + 7.498604532331228, + 7.510715387761593 + ], + "n_lcs": 1, + "n_periods": 258051, + "sde_identical": 127.21260712267089, + "best_period_identical": 8.130036261714308, + "recovered": true, + "qcfg": { + "qmin": 0.0002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 3 + } + }, + "cuv_bls_matched": { + "method": "cuv_bls_matched", + "cfg": "matched", + "time_s": 0.34627664647996426, + "batch_time_s": 0.34627664647996426, + "times_s": [ + 0.35058100521564484, + 0.34386174753308296, + 0.34627664647996426 + ], + "n_lcs": 1, + "n_periods": 258051, + "sde_identical": 127.25345502897856, + "best_period_identical": 8.130036261714308, + "recovered": true, + "qcfg": { + "qmin": 0.002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 2 + } + } + } + }, + "3000": { + "meta": { + "ndata": 144000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 3000, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 53.53614130584746 + }, + "nperiods": 391590, + "n_dur_matched": 38, + "methods": { + "cuv_tls_matched": { + "method": "cuv_tls_matched", + "time_s": 3.459543852135539, + "batch_time_s": 3.459543852135539, + "times_s": [ + 3.459543852135539, + 3.461102804169059, + 3.4592786859720945 + ], + "n_lcs": 1, + "t0_oversample": 8.0, + "n_durations": 38, + "period_native": 8.130058288574219, + "sde_native": 150.27533983989235, + "sde_identical": 150.35053699759433, + "best_period_identical": 8.130058288574219, + "depth_snr": 56.2134288328531, + "recovered": true, + "recovered_native": true, + "n_periods": 391590 + }, + "cuv_tls_default": { + "method": "cuv_tls_default", + "time_s": 1.1623164396733046, + "batch_time_s": 1.1623164396733046, + "times_s": [ + 1.1623164396733046, + 1.1622154470533133, + 1.2753417380154133 + ], + "n_lcs": 1, + "t0_oversample": 3.0, + "n_durations": 15, + "period_native": 8.130058288574219, + "sde_native": 150.15194994799836, + "sde_identical": 150.2269788947036, + "best_period_identical": 8.129920959472656, + "depth_snr": 54.83777535001808, + "recovered": true, + "recovered_native": true, + "n_periods": 391590 + }, + "cuv_bls_kunimoto": { + "method": "cuv_bls_kunimoto", + "cfg": "kunimoto", + "time_s": 12.626147827133536, + "batch_time_s": 12.626147827133536, + "times_s": [ + 12.612651254981756, + 12.626147827133536, + 12.632280489429832 + ], + "n_lcs": 1, + "n_periods": 391590, + "sde_identical": 158.65902805502566, + "best_period_identical": 8.130058422685028, + "recovered": true, + "qcfg": { + "qmin": 0.0002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 3 + } + }, + "cuv_bls_matched": { + "method": "cuv_bls_matched", + "cfg": "matched", + "time_s": 0.7296613436192274, + "batch_time_s": 0.7296613436192274, + "times_s": [ + 0.7476502638310194, + 0.7272099461406469, + 0.7296613436192274 + ], + "n_lcs": 1, + "n_periods": 391590, + "sde_identical": 161.72125764714372, + "best_period_identical": 8.130058422685028, + "recovered": true, + "qcfg": { + "qmin": 0.002, + "qmax": 0.15, + "dlogq": 0.1, + "noverlap": 2 + } + } + } + } + }, + "env": { + "python": "3.11.10", + "numpy": "1.26.4", + "host": "d26a19997861", + "cupy": "13.6.0", + "gpu": "NVIDIA RTX A5000", + "cc": "86", + "gpu_mem_GB": 25.3, + "gputls": "?", + "cuvarbase": "?", + "batman": "2.5.1" + } +} \ No newline at end of file diff --git a/benchmarks/results/gtls_comparison_jul2026/results_gtls.json b/benchmarks/results/gtls_comparison_jul2026/results_gtls.json new file mode 100644 index 0000000..b94a572 --- /dev/null +++ b/benchmarks/results/gtls_comparison_jul2026/results_gtls.json @@ -0,0 +1,194 @@ +{ + "script": "gtls_apples_bench.py", + "timestamp": "2026-07-06T20:28:31", + "inj": { + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "u": [ + 0.4804, + 0.1867 + ], + "cadence_days": 0.020833333333333332 + }, + "match_template": false, + "baselines": [ + 200, + 500, + 1000, + 1500 + ], + "results": { + "200": { + "meta": { + "ndata": 9600, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 200, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 13.653358801550656 + }, + "nperiods": 23057, + "n_dur_matched": 38, + "methods": { + "gtls_full": { + "method": "gtls", + "t0_fit_margin": 0.0, + "time_s": 5.91833676956594, + "times_s": [ + 5.91833676956594 + ], + "compile_s": 0.8467772686854005, + "search_s": 5.071559500880539, + "period_native": 8.130030176386265, + "sde_native": 34.570831298828125, + "sde_identical": 34.12735534425686, + "best_period_identical": 8.130030176386265, + "depth_snr": 0.064205851315256, + "recovered": true, + "recovered_native": true, + "n_periods": 23057, + "n_durations": 52 + }, + "gtls_skip8": { + "method": "gtls", + "t0_fit_margin": 0.125, + "time_s": 4.130032569169998, + "times_s": [ + 4.130032569169998 + ], + "compile_s": 0.6914869425818324, + "search_s": 3.4385456265881658, + "period_native": 8.130030176386265, + "sde_native": 34.657554626464844, + "sde_identical": 34.21832717591669, + "best_period_identical": 8.130030176386265, + "depth_snr": 0.06429166510831873, + "recovered": true, + "recovered_native": true, + "n_periods": 23057, + "n_durations": 52 + } + } + }, + "500": { + "meta": { + "ndata": 24000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 500, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 21.767011164065 + }, + "nperiods": 61017, + "n_dur_matched": 38, + "methods": { + "gtls_full": { + "method": "gtls", + "t0_fit_margin": 0.0, + "time_s": 60.17078699544072, + "times_s": [ + 60.17078699544072 + ], + "compile_s": 1.1312147621065378, + "search_s": 59.039572233334184, + "period_native": 8.130175785064758, + "sde_native": 53.85988235473633, + "sde_identical": 53.24109400906245, + "best_period_identical": 8.130175785064758, + "depth_snr": 0.09136765145994989, + "recovered": true, + "recovered_native": true, + "n_periods": 61017, + "n_durations": 58 + }, + "gtls_skip8": { + "method": "gtls", + "t0_fit_margin": 0.125, + "time_s": 22.259925242513418, + "times_s": [ + 22.259925242513418 + ], + "compile_s": 1.3329955143854022, + "search_s": 20.926929728128016, + "period_native": 8.130175785064758, + "sde_native": 54.02534866333008, + "sde_identical": 53.40667252573126, + "best_period_identical": 8.130175785064758, + "depth_snr": 0.09142911576104762, + "recovered": true, + "recovered_native": true, + "n_periods": 61017, + "n_durations": 58 + } + } + }, + "1000": { + "meta": { + "ndata": 48000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 1000, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 30.909105594304876 + }, + "nperiods": 125932, + "n_dur_matched": 38, + "methods": { + "gtls_full": { + "method": "gtls", + "t0_fit_margin": 0.0, + "time_s": 392.6445415560156, + "times_s": [ + 392.6445415560156 + ], + "compile_s": 2.5499397944658995, + "search_s": 390.0946017615497, + "period_native": 8.12995822670441, + "sde_native": 90.32481384277344, + "sde_identical": 89.23306877100191, + "best_period_identical": 8.12995822670441, + "depth_snr": 0.12784462932992, + "recovered": true, + "recovered_native": true, + "n_periods": 125932, + "n_durations": 63 + }, + "gtls_skip8": { + "method": "gtls", + "t0_fit_margin": 0.125, + "time_s": 75.80983891524374, + "times_s": [ + 75.80983891524374 + ], + "compile_s": 3.2179553182795644, + "search_s": 72.59188359696418, + "period_native": 8.12995822670441, + "sde_native": 90.60254669189453, + "sde_identical": 89.51449068514454, + "best_period_identical": 8.12995822670441, + "depth_snr": 0.12784742666257284, + "recovered": true, + "recovered_native": true, + "n_periods": 125932, + "n_durations": 63 + } + } + } + } +} \ No newline at end of file diff --git a/benchmarks/results/gtls_comparison_jul2026/results_gtls_skip8_big.json b/benchmarks/results/gtls_comparison_jul2026/results_gtls_skip8_big.json new file mode 100644 index 0000000..63b9787 --- /dev/null +++ b/benchmarks/results/gtls_comparison_jul2026/results_gtls_skip8_big.json @@ -0,0 +1,98 @@ +{ + "script": "gtls_apples_bench.py", + "timestamp": "2026-07-06T20:59:05", + "inj": { + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "u": [ + 0.4804, + 0.1867 + ], + "cadence_days": 0.020833333333333332 + }, + "match_template": false, + "baselines": [ + 1500, + 2000, + 3000 + ], + "results": { + "1500": { + "meta": { + "ndata": 72000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 1500, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 37.804438676647734 + }, + "nperiods": 191742, + "n_dur_matched": 38, + "methods": { + "gtls_skip8": { + "method": "gtls", + "t0_fit_margin": 0.125, + "time_s": 177.87423043884337, + "times_s": [ + 177.87423043884337 + ], + "compile_s": 3.721640777774155, + "search_s": 174.15258966106921, + "period_native": 8.130034279159807, + "sde_native": 105.51361083984375, + "sde_identical": 104.18246598078773, + "best_period_identical": 8.130034279159807, + "depth_snr": 0.16124744383685868, + "recovered": true, + "recovered_native": true, + "n_periods": 191742, + "n_durations": 66 + } + } + }, + "2000": { + "meta": { + "ndata": 96000, + "period": 8.13, + "depth": 0.004, + "noise": 0.004, + "baseline": 2000, + "cadence": 0.020833333333333332, + "t0": 2.8455, + "a_over_Rstar": 17.01495360210589, + "T14_days": 0.1618178876422576, + "q_true": 0.019903799217989862, + "snr": 43.712076332288056 + }, + "nperiods": 258051, + "n_dur_matched": 38, + "methods": { + "gtls_skip8": { + "method": "gtls", + "t0_fit_margin": 0.125, + "time_s": 348.3279370646924, + "times_s": [ + 348.3279370646924 + ], + "compile_s": 4.988396569155157, + "search_s": 343.3395404955372, + "period_native": 8.130036228065206, + "sde_native": 120.33523559570312, + "sde_identical": 118.84809423734643, + "best_period_identical": 8.130036228065206, + "depth_snr": 0.18092451137905285, + "recovered": true, + "recovered_native": true, + "n_periods": 258051, + "n_durations": 68 + } + } + } + } +} \ No newline at end of file diff --git a/scripts/gtls_benchmark/README.md b/scripts/gtls_benchmark/README.md new file mode 100644 index 0000000..b4d3e3f --- /dev/null +++ b/scripts/gtls_benchmark/README.md @@ -0,0 +1,39 @@ +# GTLS apples-to-apples benchmark + +Reproduces Figure 7 of the GTLS paper (Hu, Ge, Jin & Willis, arXiv:2607.00348) — +single-light-curve search time vs light-curve baseline — with the search held +**fair** across implementations, on one GPU. Full analysis and results: +`analysis/GTLS_COMPARISON.md`. + +## Files +- `bench_core.py` — GPU-independent core: light-curve injection (batman, Keplerian + duration), the shared Ofir period grid, and the one identical SDE re-scorer. +- `gtls_apples_bench.py` — the runner. Sweeps baselines × methods (GTLS full/skip8, + cuvarbase TLS matched/default, cuvarbase BLS kunimoto/sensible), matching period + grid, per-period duration window, epoch density, and injected transit; writes JSON. +- `plot_fig7.py` — merges result JSONs and renders the reproduced figure + tables. + +## Requirements (GPU host) +`cupy`, `pycuda`, `scikit-cuda`, `batman-package`, `numpy<2` (numba/gtls pin), +plus **both** cuvarbase feature branches merged: +- `feature/tls-fast-survey` — the improved TLS (`tls_search_batch`); +- `feature/bls-survey-speed` — the improved BLS (`eebls_gpu_batch`). + +The TLS-vs-GTLS curves run on `feature/tls-fast-survey` alone; the **BLS** curves +require `feature/bls-survey-speed` (otherwise stock BLS is timed and the numbers +will differ from the writeup). GTLS = `pip install gputls` (v0.5.1) + cupy. + +## Run +```bash +python gtls_apples_bench.py \ + --baselines 200,500,1000,1500,2000,3000 \ + --methods cuv_tls_matched,cuv_tls_default,cuv_bls_kunimoto,cuv_bls_matched \ + --cuv-reps 3 --out results_cuv.json +# GTLS (slow at long baselines — its runtime scales ~N^2.5): +python gtls_apples_bench.py --baselines 200,500,1000,1500 \ + --methods gtls_full,gtls_skip8 --gtls-reps 1 --out results_gtls.json +python plot_fig7.py fig7_reproduction.png results_cuv.json results_gtls.json +``` + +Result JSONs from the July 2026 A5000 run are in +`benchmarks/results/gtls_comparison_jul2026/`. diff --git a/scripts/gtls_benchmark/bench_core.py b/scripts/gtls_benchmark/bench_core.py new file mode 100644 index 0000000..f6a3fea --- /dev/null +++ b/scripts/gtls_benchmark/bench_core.py @@ -0,0 +1,182 @@ +"""Apples-to-apples GTLS vs cuvarbase (TLS + BLS) — shared, GPU-independent core. + +This module holds everything that does NOT touch a GPU: light-curve +injection (with a Keplerian-consistent duration so BOTH search grids bracket +the true transit), the single shared Ofir period grid fed to every method, +an identical-SDE recompute so all methods are scored by the same statistic, +and the timing bookkeeping. The GPU method calls live in the runner. + +Design decisions (fairness): +- ONE light curve per baseline, fed to every method -> identical SNR by + construction (the comparison between methods can never differ in SNR). +- Injected transit uses Kepler's 3rd law for a(P) so its duration equals the + physically expected Keplerian duration -> it lands inside cuvarbase's + narrow 0.5-2x-Earth q band AND inside GTLS's wide duration grid. Neither + method is handed a transit its grid cannot represent. +- ONE Ofir period grid (period_grid_ofir) is generated once and passed to + gtls.power(periods=...), tls_search_batch(periods=...) and the BLS search, + so the period axis is bit-identical across methods. +- SDE is recomputed with the SAME function on every method's chi2(P) spectrum. +""" +import importlib.util +import numpy as np + +import os + + +def _load(name, path): + spec = importlib.util.spec_from_file_location(name, path) + m = importlib.util.module_from_spec(spec) + spec.loader.exec_module(m) + return m + + +# Prefer the installed package (pod / any env with cuvarbase importable); +# fall back to loading the single numpy-only module by path (local, no pycuda). +try: + from cuvarbase import tls_grids +except Exception: + _CUV = os.environ.get("CUVARBASE_DIR", + "/Users/johnhoffman/Documents/cuvarbase/cuvarbase") + tls_grids = _load("tls_grids", _CUV + "/tls_grids.py") + +# Physical constants (SI) for Kepler's third law +_G = 6.67430e-11 +_MSUN = 1.98840e30 +_RSUN = 6.95700e8 +_SPD = 86400.0 + + +def keplerian_a_over_Rstar(period_days, M_star=1.0, R_star=1.0): + """a/R_star for a circular orbit from Kepler's third law.""" + P = period_days * _SPD + a_m = (_G * M_star * _MSUN * P**2 / (4.0 * np.pi**2))**(1.0 / 3.0) + return a_m / (R_star * _RSUN) + + +def transit_snr(depth, noise, period, duration_days, baseline_days, cadence_days): + """Total transit SNR ~ (depth/noise) * sqrt(N_in_transit_total).""" + n_transits = max(1, int(np.floor(baseline_days / period))) + pts_per_transit = duration_days / cadence_days + n_in = n_transits * pts_per_transit + return depth / noise * np.sqrt(max(n_in, 1.0)) + + +def make_lc(baseline_days, cadence_days, period, depth, noise, seed, + M_star=1.0, R_star=1.0, u=(0.4804, 0.1867), inject=True): + """Regular-cadence LC with an optional batman limb-darkened transit whose + a/R_star follows Kepler's 3rd law (=> physical Keplerian duration). + + batman is only available on the GPU pod; imported lazily so this module + loads locally for grid/SDE checks. + """ + import batman + rng = np.random.RandomState(seed) + n = int(round(baseline_days / cadence_days)) + t = np.arange(n) * cadence_days + y = 1.0 + rng.randn(n) * noise + dy = np.full(n, noise, dtype=float) + meta = dict(ndata=n, period=period, depth=depth, noise=noise, + baseline=baseline_days, cadence=cadence_days) + if inject: + a = keplerian_a_over_Rstar(period, M_star, R_star) + t0 = 0.35 * period + t.min() + pm = batman.TransitParams() + pm.t0 = t0 + pm.per = period + pm.rp = float(np.sqrt(depth)) # depth ~ (Rp/Rs)^2 + pm.a = float(a) + pm.inc = 90.0 + pm.ecc = 0.0 + pm.w = 90.0 + pm.u = list(u) + pm.limb_dark = "quadratic" + m = batman.TransitModel(pm, t) + y = y + (m.light_curve(pm) - 1.0) + # physical T14 (edge-on) for bookkeeping / grid-bracket check + b = 0.0 + T14 = period / np.pi * np.arcsin( + 1.0 / a * np.sqrt((1.0 + pm.rp)**2 - b**2)) + meta.update(t0=t0, a_over_Rstar=a, T14_days=float(T14), + q_true=float(T14 / period), + snr=float(transit_snr(depth, noise, period, T14, + baseline_days, cadence_days))) + return t, y, dy, meta + + +def shared_period_grid(t, oversampling_factor=3, period_min=0.6, + period_max=None): + """The single Ofir grid every method searches (Pmax defaults to S/2).""" + return tls_grids.period_grid_ofir( + t, R_star=1.0, M_star=1.0, oversampling_factor=oversampling_factor, + period_min=period_min, period_max=period_max) + + +def recompute_sde_from_sr(sr, periods, oversampling_factor=3): + """The one identical SDE routine. Takes a signal-residue spectrum SR(P) + (large = better fit) already ascending in period. SDE = detrended-SR peak + z-score with a median filter (window = OS*30, TLS convention, odd, capped + at 91). Every method is scored through THIS function so the statistic is + identical; only the spectrum differs.""" + from scipy.signal import medfilt + sr = np.asarray(sr, dtype=float) + p = np.asarray(periods, dtype=float) + ok = np.isfinite(sr) & np.isfinite(p) + sr, p = sr[ok], p[ok] + if sr.size < 5: + return dict(SDE=0.0, best_period=float("nan"), depth_snr=0.0) + w = min(int(oversampling_factor * 30), 91) + if w % 2 == 0: + w += 1 + trend = medfilt(sr, kernel_size=w) if (3 <= w < len(sr)) \ + else np.zeros_like(sr) + resid = sr - trend + sd = resid.std() + SDE = resid / sd if sd > 0 else resid * 0.0 + ibest = int(np.argmax(SDE)) + return dict(SDE=float(SDE[ibest]), best_period=float(p[ibest]), + sr_max=float(np.nanmax(sr))) + + +def recompute_sde(chi2, periods, oversampling_factor=3): + """Score a chi2(period) spectrum: SR = 1 - chi2/chi2_null(=max), then the + identical SDE routine above.""" + chi2 = np.asarray(chi2, dtype=float) + ok = np.isfinite(chi2) & (chi2 < 1e29) + c = chi2[ok] + p = np.asarray(periods, dtype=float)[ok] + if c.size < 5: + return dict(SDE=0.0, best_period=float("nan"), depth_snr=0.0) + chi2_null = np.nanmax(c) + out = recompute_sde_from_sr(1.0 - c / chi2_null, p, oversampling_factor) + out["depth_snr"] = float(np.sqrt(max(chi2_null - np.nanmin(c), 0.0))) + out["chi2_min"] = float(np.nanmin(c)) + return out + + +def recovered(p_found, p_true, tol=0.02): + for k in (1.0, 2.0, 0.5, 3.0, 1 / 3.0): + if abs(p_found - k * p_true) / (k * p_true) < tol: + return True + return False + + +if __name__ == "__main__": + # Local self-test of the fairness invariants (no GPU, no batman needed + # for the grid parts). + for base in (200, 1500, 3000): + cad = 30.0 / 60 / 24 + t = np.arange(int(base / cad)) * cad + pg = shared_period_grid(t) + # pick a period giving >=3 transits even at the shortest baseline + P = 8.13 + a = keplerian_a_over_Rstar(P) + T14 = P / np.pi * np.arcsin(1.0 / a * np.sqrt((1 + 0.05)**2)) + q = T14 / P + # is q inside cuvarbase's 0.5-2x Earth band at this period? + _, _, qvals = tls_grids.duration_grid_keplerian( + np.array([P]), 1.0, 1.0, 1.0, n_durations=15) + band = (0.5 * qvals[0], 2.0 * qvals[0]) + print(f"base={base:5d} Npg={len(pg):7d} P={P} T14={T14*24:.2f}h " + f"q_true={q:.4f} cuvar_band=[{band[0]:.4f},{band[1]:.4f}] " + f"in_band={band[0] <= q <= band[1]}") diff --git a/scripts/gtls_benchmark/gtls_apples_bench.py b/scripts/gtls_benchmark/gtls_apples_bench.py new file mode 100644 index 0000000..d72e99b --- /dev/null +++ b/scripts/gtls_benchmark/gtls_apples_bench.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python +"""Apples-to-apples reproduction of GTLS paper (arXiv:2607.00348) Figure 7: +runtime vs light-curve baseline for GTLS vs cuvarbase TLS vs cuvarbase BLS, +all on the SAME GPU, SAME light curve, SAME period grid, SAME per-period +duration search extent, SAME epoch (t0) density, and (optionally) the SAME +limb-darkened template. Every method is additionally scored by ONE identical +SDE routine so we can confirm equal detection, not just equal speed. + +Fairness protocol (see notes at bottom): + * ONE injected batman transit per baseline, fed to every method -> identical + SNR between methods by construction. + * ONE Ofir period grid (cuvarbase.tls_grids.period_grid_ofir, Pmax=S/2) + passed explicitly to gtls.power(periods=), tls_search_batch(periods=), + and BLS (freqs=sort(1/periods)). + * cuvarbase-TLS "matched" uses per-period qmin/qmax = GTLS's own kernel + duration window and n_durations chosen for the same log-1.1 resolution + (~38), and t0_oversample matched to GTLS's SKIP_POINT (=1/T0_fit_margin). + * cuvarbase-TLS "default" is the shipping survey default (t0_os=3, 15 dur, + Keplerian [0.5q,2q]) — the "production" number, clearly separated. + * BLS uses the paper's Kunimoto params (qmin=2e-4, qmax=0.15, dlogq=0.1, + noverlap=3) on the identical period grid. + +Runs on a GPU pod with: cupy, gputls, cuvarbase (TLS branch + BLS branch +merged), batman, numpy, scipy. + +Usage: + python gtls_apples_bench.py --baselines 200,500,1000,1500,2000,3000 \ + --methods gtls_full,gtls_skip8,cuv_tls_matched,cuv_tls_default,cuv_bls \ + --out results.json +""" +import argparse +import gc +import json +import platform +import time +import traceback +import warnings + +warnings.filterwarnings("ignore") +import numpy as np + +# ---- shared, GPU-independent helpers (LC gen, grid, identical-SDE) ---------- +import bench_core as bc # co-located module + +CAD = 30.0 / 60.0 / 24.0 # 30-min Kepler long cadence, days +# Injected transit (fixed across baselines; a from Kepler's 3rd law -> physical +# Keplerian duration so BOTH search grids bracket it): +INJ_PERIOD = 8.13 +INJ_DEPTH = 0.004 +INJ_NOISE = 0.004 +INJ_U = (0.4804, 0.1867) # G2V Kepler LD (== GTLS/TLS reference) + +# ---- GTLS per-period duration window (from GPUFun.py durationsGrid kernel) -- +_R_SUN = 695508000.0 +_R_JUP = 69911000.0 +_SPD = 86400.0 +_SCALE = 1e15 +_PI_GM_MIN = 20848.0 +_PI_GM_MAX = 416970.0 +_RS_MIN = _R_SUN * 0.05 +_RS_MAX = _R_SUN * 4.0 +_FRAC_MAX = 0.15 + + +def gtls_dur_window(P_days): + """Vectorized GTLS per-period (qmin,qmax) fractional-duration window.""" + P = np.asarray(P_days, float) + Ps = P * _SPD + pf_min = (4.0 * Ps) / (_PI_GM_MIN * _SCALE) + pf_max = (4.0 * Ps) / (_PI_GM_MAX * _SCALE) + T14Min = _RS_MIN * pf_min ** (1.0 / 3.0) + T14Max = (_RS_MAX + _R_JUP * 2.0) * pf_max ** (1.0 / 3.0) + dmin = np.minimum(T14Min / Ps, _FRAC_MAX) + dmax = np.minimum(T14Max / Ps, _FRAC_MAX) + # guard qmin>0 and qmin impact parameter b = a*cos(inc) ~ 0.32 + p.ecc = 0.0 + p.w = 90.0 + p.limb_dark = limb_dark + p.u = list(u) + # transit half-width in phase ~ (1/pi)*asin(sqrt((1+rp)^2-b^2)/a); span it + tt = np.linspace(-0.05, 0.05, n_samples) + m = batman.TransitModel(p, tt) + flux = m.light_curve(p) + oot = flux[0] + depth = oot - np.min(flux) + if depth < 1e-10: + raise ValueError("template depth ~0") + fluxn = (flux - oot) / depth + 1.0 + phases = (tt - tt[0]) / (tt[-1] - tt[0]) + return phases, fluxn + + tm.create_reference_transit = hippke_reference + return True + + +def run_cuv_tls(lcs, periods, t0_oversample, qmin, qmax, n_durations, + u=INJ_U, reps=3, label="cuv_tls"): + from cuvarbase.tls import tls_search_batch + periods = np.sort(np.asarray(periods, float)) + + kw = dict(R_star=1.0, M_star=1.0, periods=periods, + oversampling_factor=3, n_durations=n_durations, + t0_oversample=t0_oversample, refine_top_k=50, + u=list(u), limb_dark="quadratic", return_arrays=True) + if qmin is not None: + kw["qmin"] = np.asarray(qmin, float) + kw["qmax"] = np.asarray(qmax, float) + + def call(): + return tls_search_batch(lcs, **kw) + + total_s, ts, res = timed(_pycuda_sync, call, warmups=1, reps=reps) + # per-LC = batch time / n_lcs (single-LC head-to-head when len(lcs)==1) + per_lc = total_s / len(lcs) + r0 = res[0] + if "error" in r0: + return dict(method=label, time_s=per_lc, error=r0["error"]) + sde = bc.recompute_sde(np.asarray(r0["chi2"], float), + np.asarray(r0["periods"], float)) + return dict(method=label, time_s=per_lc, batch_time_s=total_s, times_s=ts, + n_lcs=len(lcs), t0_oversample=t0_oversample, + n_durations=n_durations, period_native=float(r0["period"]), + sde_native=float(r0["SDE"]), sde_identical=sde["SDE"], + best_period_identical=sde["best_period"], + depth_snr=sde["depth_snr"], + recovered=bc.recovered(sde["best_period"], INJ_PERIOD), + recovered_native=bc.recovered(float(r0["period"]), INJ_PERIOD), + n_periods=int(len(periods))) + + +# --------------------------------------------------------- cuvarbase BLS ------ +def run_cuv_bls(lcs, periods, cfg="kunimoto", reps=3, label="cuv_bls"): + """BLS on the identical period grid. eebls_gpu_batch returns only power + spectra -> argmax for the identical-SDE score. + + cfg='kunimoto': the GTLS paper's BLS config (qmin=2e-4, qmax=0.15, + dlogq=0.1, noverlap=3) -> up to 5000 phase bins, fused kernel bypassed. + cfg='matched': BLS duration grid matched to the TLS run's per-period + window (qmin/qmax = GTLS window) with noverlap=2 so the fused kernel + (opt1) is used -> BLS's true speed at TLS-comparable duration fidelity. + """ + from cuvarbase.bls import eebls_gpu_batch + periods = np.sort(np.asarray(periods, float)) + freqs = np.sort((1.0 / periods).astype(np.float32)) + if cfg == "kunimoto": + kw = dict(qmin=2e-4, qmax=0.15, dlogq=0.1, noverlap=3) + elif cfg == "matched": + # BLS at a physically sensible transit-duration range (>=0.2% of the + # period, brackets the injected q~0.02) with the fused kernel + # (noverlap=2, power of two). This is BLS's true competitive speed; + # the Kunimoto qmin=2e-4 (5000 bins) is what makes 'kunimoto' heavy. + kw = dict(qmin=2e-3, qmax=0.15, dlogq=0.1, noverlap=2) + else: + raise ValueError(cfg) + + def call(): + return eebls_gpu_batch(lcs, freqs, **kw) + + total_s, ts, powers = timed(_pycuda_sync, call, warmups=1, reps=reps) + per_lc = total_s / len(lcs) + p0 = np.asarray(powers[0], float) + ok = np.isfinite(p0) + fr = freqs[ok].astype(float) + order = np.argsort(1.0 / fr) # ascending period + sde = bc.recompute_sde_from_sr(p0[ok][order], (1.0 / fr)[order]) + scal = {k: (float(np.median(v)) if hasattr(v, "__len__") else v) + for k, v in kw.items()} + return dict(method=label, cfg=cfg, time_s=per_lc, batch_time_s=total_s, + times_s=ts, n_lcs=len(lcs), n_periods=int(len(periods)), + sde_identical=sde["SDE"], best_period_identical=sde["best_period"], + recovered=bc.recovered(sde["best_period"], INJ_PERIOD), + qcfg=scal) + + +# ------------------------------------------------------------------ main ------ +def env_info(): + info = dict(python=platform.python_version(), numpy=np.__version__, + host=platform.node()) + try: + import cupy as cp + info["cupy"] = cp.__version__ + info["gpu"] = cp.cuda.runtime.getDeviceProperties(0)["name"].decode() + info["cc"] = str(cp.cuda.Device(0).compute_capability) + info["gpu_mem_GB"] = round(cp.cuda.Device(0).mem_info[1] / 1e9, 1) + except Exception as e: + info["gpu"] = "cupy/gpu unavailable: %r" % e + for pkg in ("gputls", "cuvarbase", "batman"): + try: + m = __import__(pkg) + info[pkg] = getattr(m, "__version__", "?") + except Exception as e: + info[pkg] = "unavailable: %r" % e + return info + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--baselines", default="200,500,1000,1500,2000,3000") + ap.add_argument("--methods", + default="gtls_full,gtls_skip8,cuv_tls_matched," + "cuv_tls_default,cuv_bls") + ap.add_argument("--match-template", action="store_true", + help="patch cuvarbase template to Hippke geometry") + ap.add_argument("--gtls-reps", type=int, default=1) + ap.add_argument("--cuv-reps", type=int, default=3) + ap.add_argument("--out", default="gtls_apples_results.json") + args = ap.parse_args() + + baselines = [int(x) for x in args.baselines.split(",") if x] + methods = [m.strip() for m in args.methods.split(",") if m.strip()] + + if args.match_template and any(m.startswith("cuv_tls") for m in methods): + maybe_patch_template() + print("[template] cuvarbase reference transit patched to Hippke geometry") + + out = dict(script="gtls_apples_bench.py", + timestamp=time.strftime("%Y-%m-%dT%H:%M:%S"), + inj=dict(period=INJ_PERIOD, depth=INJ_DEPTH, noise=INJ_NOISE, + u=list(INJ_U), cadence_days=CAD), + match_template=args.match_template, baselines=baselines, + results={}) + + for base in baselines: + print("\n" + "=" * 72) + print("BASELINE %d d" % base) + print("=" * 72, flush=True) + t, y, dy, meta = bc.make_lc(base, CAD, INJ_PERIOD, INJ_DEPTH, + INJ_NOISE, seed=1000 + base, u=INJ_U) + periods = bc.shared_period_grid(t) + qmin, qmax = gtls_dur_window(periods) + n_dur_matched = gtls_matched_n_durations(periods) + print(" ndata=%d nperiods=%d SNR=%.1f q_true=%.4f " + "n_dur_matched=%d" % (meta["ndata"], len(periods), + meta.get("snr", -1), meta.get("q_true", -1), n_dur_matched), + flush=True) + row = dict(meta=meta, nperiods=int(len(periods)), + n_dur_matched=n_dur_matched, methods={}) + + for m in methods: + try: + if m == "gtls_full": + r = run_gtls(t, y, dy, periods, 0.0, reps=args.gtls_reps) + elif m == "gtls_skip8": + r = run_gtls(t, y, dy, periods, 0.125, reps=args.gtls_reps) + elif m == "cuv_tls_matched": + r = run_cuv_tls([(t, y, dy)], periods, t0_oversample=8.0, + qmin=qmin, qmax=qmax, + n_durations=n_dur_matched, + reps=args.cuv_reps, label="cuv_tls_matched") + elif m == "cuv_tls_default": + r = run_cuv_tls([(t, y, dy)], periods, t0_oversample=3.0, + qmin=None, qmax=None, n_durations=15, + reps=args.cuv_reps, label="cuv_tls_default") + elif m in ("cuv_bls", "cuv_bls_kunimoto"): + r = run_cuv_bls([(t, y, dy)], periods, cfg="kunimoto", + reps=args.cuv_reps, label=m) + elif m == "cuv_bls_matched": + r = run_cuv_bls([(t, y, dy)], periods, cfg="matched", + reps=args.cuv_reps, label=m) + else: + print(" unknown method %s" % m); continue + row["methods"][m] = r + print(" %-18s %9.3f s/LC SDE(id)=%6.2f P=%.4f rec=%s" + % (m, r.get("time_s", float("nan")), + r.get("sde_identical", float("nan")), + r.get("best_period_identical", float("nan")), + r.get("recovered")), flush=True) + except Exception as e: + traceback.print_exc() + row["methods"][m] = dict(error=repr(e)) + print(" %-18s ERROR %r" % (m, e), flush=True) + gc.collect() + + out["results"][str(base)] = row + with open(args.out, "w") as f: + json.dump(out, f, indent=2, default=str) + + out["env"] = env_info() + with open(args.out, "w") as f: + json.dump(out, f, indent=2, default=str) + print("\nwrote %s" % args.out) + print(json.dumps(out["env"], indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/gtls_benchmark/plot_fig7.py b/scripts/gtls_benchmark/plot_fig7.py new file mode 100644 index 0000000..01cbe41 --- /dev/null +++ b/scripts/gtls_benchmark/plot_fig7.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +"""Reproduce GTLS paper (arXiv:2607.00348) Fig. 7 apples-to-apples on one GPU, +plus an SDE-parity panel. Merges any number of results_*.json files (e.g. +results_cuv.json results_gtls.json). Writes fig7_reproduction.png. + +Usage: python plot_fig7.py out.png results_cuv.json results_gtls.json ... +""" +import json +import sys +import numpy as np +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.ticker import ScalarFormatter, NullFormatter + +OUT = sys.argv[1] if len(sys.argv) > 1 else "fig7_reproduction.png" +FILES = sys.argv[2:] or ["results_cuv.json", "results_gtls.json"] + +# merge: baseline -> method -> result +merged, gpu, inj = {}, "GPU", {} +for fn in FILES: + try: + d = json.load(open(fn)) + except Exception: + continue + gpu = d.get("env", {}).get("gpu", gpu) + inj = d.get("inj", inj) + for b, row in d.get("results", {}).items(): + merged.setdefault(b, {}) + for m, r in row.get("methods", {}).items(): + merged[b][m] = r + +bl = sorted(int(b) for b in merged) + +STYLE = { # colorblind-safe; grouped by family + "gtls_full": ("#d55e00", "o", "-", "GTLS full-T0 scan (paper Fig.7 setting)"), + "gtls_skip8": ("#e69f00", "s", "-", "GTLS skip=8 (its efficient default)"), + "cuv_bls_kunimoto": ("#cc79a7", "P", "-", "cuvarbase BLS (Kunimoto qmin=2e-4, nov=3 — paper's BLS cfg)"), + "cuv_tls_matched": ("#0072b2", "D", "-", "cuvarbase TLS (matched: grid+durations+epochs to GTLS)"), + "cuv_tls_default": ("#009e73", "^", "-", "cuvarbase TLS (survey default: t0os=3, 15 dur)"), + "cuv_bls_matched": ("#56b4e9", "v", "-", "cuvarbase BLS (sensible cfg: qmin=2e-3, fused nov=2)"), +} +ORDER = ["gtls_full", "gtls_skip8", "cuv_bls_kunimoto", "cuv_tls_matched", + "cuv_tls_default", "cuv_bls_matched"] + +def series(m, key): + xs, ys = [], [] + for b in bl: + v = merged[str(b)].get(m, {}).get(key) + if isinstance(v, (int, float)) and np.isfinite(v): + xs.append(b); ys.append(v) + return np.array(xs, float), np.array(ys, float) + +# published paper anchors (single-LC; GTLS/BLS on RTX 4090, TLS on 7950X CPU) +PAPER = {"gtls": [(1500, 33.3), (3000, 138.0)], "bls": [(1500, 121.1)], + "tls_cpu": [(1500, 522.0), (3000, 3289.0)]} + +fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(9.6, 10.2), + gridspec_kw={"height_ratios": [2.5, 1]}) + +for m in ORDER: + c, mk, ls, lab = STYLE[m] + x, y = series(m, "time_s") + if len(x): + ax1.plot(x, y, marker=mk, ls=ls, color=c, lw=2, ms=7, label=lab, zorder=4) +# GTLS full compile-subtracted (search only) +xs, ys = series("gtls_full", "search_s") +if len(xs): + ax1.plot(xs, ys, ":", color=STYLE["gtls_full"][0], lw=1.3, alpha=.7, + label="GTLS full, search only (JIT-compile subtracted)", zorder=3) +# published paper points +px, py = zip(*PAPER["gtls"]); ax1.scatter(px, py, marker="*", s=280, + facecolor="none", edgecolor="#d55e00", linewidths=2, zorder=6) +bx, by = zip(*PAPER["bls"]); ax1.scatter(bx, by, marker="*", s=280, + facecolor="none", edgecolor="#cc79a7", linewidths=2, zorder=6) +tx, ty = zip(*PAPER["tls_cpu"]); ax1.plot(tx, ty, "--", color="#7f7f7f", lw=1.6, + marker="X", ms=10, label="reference TLS (CPU 7950X, paper)", zorder=3) +ax1.scatter([], [], marker="*", s=200, facecolor="none", edgecolor="k", + linewidths=1.5, label="★ published paper value (RTX 4090)") + +ax1.set_xscale("log"); ax1.set_yscale("log") +ax1.set_xlabel("light-curve baseline [days] (30-min cadence)") +ax1.set_ylabel("search time per light curve [s]") +snr = inj.get("period"), inj.get("depth") +ax1.set_title("GTLS Fig. 7 reproduced apples-to-apples on one GPU (%s)\n" + "identical Ofir period grid · identical per-period duration window " + "· matched epoch density · one injected transit" % gpu, fontsize=10.5) +ax1.grid(True, which="both", alpha=.25) +ax1.legend(fontsize=7.6, loc="lower right", framealpha=.96, ncol=1) +for b in (1500, 3000): + ax1.axvline(b, color="k", alpha=.06, lw=10) + +for m in ORDER: + c, mk, ls, lab = STYLE[m] + x, y = series(m, "sde_identical") + if len(x): + ax2.plot(x, y, marker=mk, color=c, lw=1.5, ms=6) +ax2.axhline(7, color="k", ls="--", lw=1, alpha=.6) +ax2.text(bl[0], 8, "SDE=7 detection threshold", fontsize=8, alpha=.7) +ax2.set_xscale("log") +ax2.set_xlabel("light-curve baseline [days]") +ax2.set_ylabel("SDE (one identical\nstatistic per spectrum)") +ax2.set_title("Detection significance is identical across all methods — " + "the speed gap is not bought with sensitivity", fontsize=10) +ax2.grid(True, which="both", alpha=.25) +# explicit x ticks (log axis otherwise only labels 10^3) +ticks = [b for b in (200, 300, 500, 1000, 1500, 2000, 3000) if bl[0] <= b <= bl[-1]] +for ax in (ax1, ax2): + ax.set_xticks(ticks) + ax.get_xaxis().set_major_formatter(ScalarFormatter()) + ax.get_xaxis().set_minor_formatter(NullFormatter()) + ax.set_xlim(bl[0] * 0.9, bl[-1] * 1.12) +fig.tight_layout() +fig.savefig(OUT, dpi=145, bbox_inches="tight") +print("wrote", OUT) + +# ---- text table + speedups ---- +hdr = "baseline " + "".join("%18s" % m.replace("cuv_", "").replace("_", " ") + for m in ORDER) +print("\n" + hdr) +for b in bl: + r = "%6dd " % b + for m in ORDER: + t = merged[str(b)].get(m, {}).get("time_s") + r += "%18s" % (("%.3f s" % t) if isinstance(t, (int, float)) else "-") + print(r) +print("\nSDE (identical) per baseline:") +for b in bl: + ss = [merged[str(b)].get(m, {}).get("sde_identical") for m in ORDER] + ss = [s for s in ss if isinstance(s, (int, float))] + print(" %6dd: %.1f–%.1f (spread %.1f%%)" % ( + b, min(ss), max(ss), 100 * (max(ss) - min(ss)) / np.mean(ss))) +print("\nSpeedup cuvarbase-TLS-matched vs GTLS:") +for b in bl: + M = merged[str(b)] + cm = M.get("cuv_tls_matched", {}).get("time_s") + gf = M.get("gtls_full", {}).get("time_s") + gs = M.get("gtls_skip8", {}).get("time_s") + if cm and (gf or gs): + print(" %6dd: vs GTLS-full %-7s vs GTLS-skip8 %-7s" % ( + b, ("%.0fx" % (gf / cm)) if gf else "-", + ("%.0fx" % (gs / cm)) if gs else "-")) From 818acb26579e1c1f8846105f912b0d34c36eb5c9 Mon Sep 17 00:00:00 2001 From: John Hoffman Date: Tue, 7 Jul 2026 10:10:34 -0500 Subject: [PATCH 4/6] TLS/GTLS: add cold single-shot (one-star, compile-included) comparison Answers the single-light-curve / cold-start question. On a fresh A5000, one LC, fresh process, kernel JIT compile INCLUDED, on-disk kernel cache cleared (true first-run case), full launch-to-answer wall time: baseline cuvarbase-TLS-matched GTLS-skip8 cold ratio 200 d 4.1 s 10.7 s 2.6x 500 d 4.5 s 27.8 s 6.1x 1000 d 4.8 s 83.8 s 17x 1500 d 5.6 s 191.0 s 34x cuvarbase's cold cost is a ~fixed ~3-4s kernel compile that barely grows with baseline; GTLS's is its exploding search, so the ratio grows 2.6x -> 34x. From the 2nd star onward (pycuda/cupy disk cache warm) cuvarbase drops to ~0.5-2s and the ratio snaps back toward the warm 30-171x; GTLS recompiles + re-searches every call. SDE parity holds cold too. Adds analysis/GTLS_COMPARISON.md section 2b, the raw cold data, and the harness (scripts/gtls_benchmark/cold_shot.py + cold_driver.sh). Co-Authored-By: Claude Opus 4.8 (1M context) --- analysis/GTLS_COMPARISON.md | 26 +++++ .../cold_single_shot_a5000.txt | 24 +++++ scripts/gtls_benchmark/cold_driver.sh | 22 +++++ scripts/gtls_benchmark/cold_shot.py | 96 +++++++++++++++++++ 4 files changed, 168 insertions(+) create mode 100644 benchmarks/results/gtls_comparison_jul2026/cold_single_shot_a5000.txt create mode 100644 scripts/gtls_benchmark/cold_driver.sh create mode 100644 scripts/gtls_benchmark/cold_shot.py diff --git a/analysis/GTLS_COMPARISON.md b/analysis/GTLS_COMPARISON.md index b7ff29b..0e20c11 100644 --- a/analysis/GTLS_COMPARISON.md +++ b/analysis/GTLS_COMPARISON.md @@ -103,6 +103,32 @@ paper's reported 121.1 s cuvarbase-BLS on a 4090 — ~23× faster on weaker hardware** (opt1–opt4 + batched kernel; the paper's exact cuvarbase entry point / version is unspecified). +## 2b. Single light curve — GTLS's home turf, and the cold-start case + +Every number above is already **single-light-curve** (GTLS has no batch API, so +cuvarbase was timed one LC at a time too — batching would only widen the gap). The +warm speedups assume the kernel JIT is compiled, which amortizes across any real +workload. For the strict **cold single shot** — one star, a fresh process, kernel +compile *included*, and the on-disk pycuda/cupy kernel cache *cleared* before every +run (first-run / fresh-container worst case) — full launch-to-answer wall time on a +second A5000: + +| baseline | cuvarbase-TLS (matched) | GTLS-skip8 | cold ratio | +|---:|---:|---:|---:| +| 200 d | 4.1 s | 10.7 s | **2.6×** | +| 500 d | 4.5 s | 27.8 s | **6.1×** | +| 1000 d | 4.8 s | 83.8 s | **17×** | +| 1500 d | 5.6 s | 191.0 s | **34×** | + +cuvarbase's cold cost is a ~fixed **~3–4 s kernel compile** that barely grows with +baseline (its search is 0.04–1.4 s); GTLS's cost is its *search*, which explodes — +so the ratio grows from 2.6× (both fixed-cost-bound at short baselines) to 34× at +Kepler length. This is the pessimistic floor: from the **2nd star onward** (disk +kernel cache warm) cuvarbase drops to ~0.5–2 s and the ratio snaps back toward the +warm 30–171×, while GTLS recompiles *and* re-searches on every call. SDE parity +holds cold too. (Raw: `benchmarks/results/gtls_comparison_jul2026/cold_single_shot_a5000.txt`; +harness: `scripts/gtls_benchmark/cold_shot.py` + `cold_driver.sh`.) + ## 3. Results — detection significance (SDE parity) Scored by the one identical statistic, **every method agrees closely at every diff --git a/benchmarks/results/gtls_comparison_jul2026/cold_single_shot_a5000.txt b/benchmarks/results/gtls_comparison_jul2026/cold_single_shot_a5000.txt new file mode 100644 index 0000000..fb566b4 --- /dev/null +++ b/benchmarks/results/gtls_comparison_jul2026/cold_single_shot_a5000.txt @@ -0,0 +1,24 @@ +Cold single-shot: one light curve, fresh process, NO in-process warmup, on-disk +pycuda/cupy kernel cache CLEARED before each run (true first-run / fresh-container +case). NVIDIA RTX A5000, 7 Jul 2026. Injected transit P=8.13d depth=4e-3. +full_wall_s = python import + CUDA context init + JIT compile + search +search_compile = compile + search only (context pre-initialized) + +method baseline full_wall_s search_compile nper SDE +cuv_tls_default 200 4.27 2.826 23057 33.48 +cuv_tls_matched 200 4.10 2.852 23057 34.12 +gtls_skip8 200 10.69 8.402 23057 34.41 +cuv_tls_default 500 4.75 3.620 61017 57.59 +cuv_tls_matched 500 4.52 3.226 61017 57.09 +gtls_skip8 500 27.79 25.730 61017 58.53 +cuv_tls_default 1000 5.58 4.346 125932 85.35 +cuv_tls_matched 1000 4.81 3.668 125932 85.79 +gtls_skip8 1000 83.75 81.607 125932 87.28 +cuv_tls_default 1500 5.83 4.612 191742 102.07 +cuv_tls_matched 1500 5.59 4.264 191742 104.03 +gtls_skip8 1500 191.00 188.714 191742 106.91 + +Cold ratio (full_wall, cuv_tls_matched vs gtls_skip8): 2.6x / 6.1x / 17x / 34x +Grows because cuvarbase's cost is a ~fixed ~3-4s compile while GTLS's is its +exploding search. 2nd star onward (disk cache warm): cuvarbase ~0.5-2s -> ratio +snaps back toward the warm 30-171x. GTLS recompiles+re-searches every call. diff --git a/scripts/gtls_benchmark/cold_driver.sh b/scripts/gtls_benchmark/cold_driver.sh new file mode 100644 index 0000000..4a3ed38 --- /dev/null +++ b/scripts/gtls_benchmark/cold_driver.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# TRUE cold single-shot: clear the on-disk kernel caches before EACH run so the +# JIT compile happens from scratch every time (first-run / fresh-container case). +# Each (method, baseline) is a fresh process. Reports full_wall (python import + +# CUDA context init + compile + search) and search_compile (compile + search). +export PATH=/usr/local/cuda/bin:$PATH +cd /root +printf "%-17s %6s %11s %13s %8s %6s\n" method baseline full_wall_s search_compile nper SDE +for base in 200 500 1000 1500; do + for m in cuv_tls_default cuv_tls_matched gtls_skip8; do + rm -rf ~/.cache/pycuda /root/.cache/pycuda ~/.cupy ~/.nv/ComputeCache 2>/dev/null + t0=$(date +%s.%N) + OUT=$(python3 cold_shot.py --method "$m" --baseline "$base" 2>/dev/null | grep RESULT) + t1=$(date +%s.%N) + wall=$(awk "BEGIN{printf \"%.2f\", $t1-$t0}") + sc=$(echo "$OUT" | grep -oE 'search_compile_s=[0-9.]+' | cut -d= -f2) + nper=$(echo "$OUT" | grep -oE 'nper=[0-9]+' | cut -d= -f2) + sde=$(echo "$OUT" | grep -oE 'SDE=[0-9.]+' | cut -d= -f2) + printf "%-17s %6d %11s %13s %8s %6s\n" "$m" "$base" "$wall" "${sc:-ERR}" "${nper:-?}" "${sde:-?}" + done +done +echo "DONE_COLD" diff --git a/scripts/gtls_benchmark/cold_shot.py b/scripts/gtls_benchmark/cold_shot.py new file mode 100644 index 0000000..8f00216 --- /dev/null +++ b/scripts/gtls_benchmark/cold_shot.py @@ -0,0 +1,96 @@ +"""Cold single-shot timing: one light curve, one fresh process, NO warmup, so +the kernel JIT compile is INCLUDED for both cuvarbase and GTLS. The CUDA context +is initialized before the clock starts (a fixed driver cost both pay), so the +measured number is compile + search — the true one-star cold cost. The launching +shell also times the whole process (python import + context init + this). + +Standalone Ofir grid + LC (numpy/batman only) so the GTLS process never imports +cuvarbase (fair: each loads only its own stack). + +Usage: python cold_shot.py --method {gtls_skip8|cuv_tls_matched|cuv_tls_default} --baseline 1500 +""" +import argparse +import time +import warnings +warnings.filterwarnings("ignore") +import numpy as np + +G = 6.67430e-11; RSUN = 6.957e8; MSUN = 1.9884e30; SPD = 86400.0; RJUP = 6.9911e7 + + +def ofir_grid(t, os=3, pmin=0.6, n_transits_min=2): + T = (t.max() - t.min()) * SPD + fmin = n_transits_min / T + fmax = 1 / (2 * np.pi) * np.sqrt(G * MSUN / (3 * RSUN) ** 3) + A = (2 * np.pi) ** (2 / 3) / np.pi * RSUN / (G * MSUN) ** (1 / 3) / (T * os) + C = fmin ** (1 / 3) - A / 3 + n = int(np.ceil((fmax ** (1 / 3) - fmin ** (1 / 3) + A / 3) * 3 / A)) + x = np.arange(n) + 1 + per = 1 / ((A / 3 * x + C) ** 3) / SPD + return np.sort(per[per > pmin]) + + +def make_lc(baseline, cad=30 / 60 / 24, P=8.13, depth=4e-3, noise=4e-3, seed=1): + import batman + rng = np.random.RandomState(seed) + n = int(round(baseline / cad)); t = np.arange(n) * cad + y = 1 + rng.randn(n) * noise; dy = np.full(n, noise) + a = (G * MSUN * (P * SPD) ** 2 / (4 * np.pi ** 2)) ** (1 / 3) / RSUN + pm = batman.TransitParams() + pm.t0 = 0.35 * P; pm.per = P; pm.rp = float(np.sqrt(depth)); pm.a = float(a) + pm.inc = 90; pm.ecc = 0; pm.w = 90; pm.u = [0.4804, 0.1867] + pm.limb_dark = "quadratic" + y = y + (batman.TransitModel(pm, t).light_curve(pm) - 1) + return t, y, dy + + +def gtls_qwin(P): + Ps = P * SPD + pfmin = 4 * Ps / (20848 * 1e15); pfmax = 4 * Ps / (416970 * 1e15) + dmin = np.minimum((RSUN * 0.05) * pfmin ** (1 / 3) / Ps, 0.15) + dmax = np.minimum((RSUN * 4.0 + 2 * RJUP) * pfmax ** (1 / 3) / Ps, 0.15) + dmin = np.clip(dmin, 1e-5, 0.15 * 0.999); dmax = np.clip(dmax, dmin * 1.0001, 0.999) + return dmin, dmax + + +ap = argparse.ArgumentParser() +ap.add_argument("--method", required=True) +ap.add_argument("--baseline", type=int, required=True) +args = ap.parse_args() + +t, y, dy = make_lc(args.baseline) +periods = ofir_grid(t) + +if args.method.startswith("gtls"): + import cupy as cp + cp.arange(1).sum(); cp.cuda.Stream.null.synchronize() # init context (untimed) + from gputls import gtls + t0fit = 0.0 if args.method == "gtls_full" else 0.125 + c0 = time.perf_counter() + res = gtls(t=t, y=y, dy=dy, verbose=False).power( + periods=periods, R_star=1, M_star=1, oversampling_factor=3, + T0_fit_margin=t0fit, verbose=False, show_progress_bar=False) + cp.cuda.Stream.null.synchronize() + dt = time.perf_counter() - c0 + print("RESULT %s %d nper=%d search_compile_s=%.3f P=%.4f SDE=%.2f" + % (args.method, args.baseline, len(periods), dt, res.period, res.SDE)) +else: + import pycuda.autoprimaryctx # init context at import (untimed) + import pycuda.driver as drv + drv.Context.synchronize() + from cuvarbase.tls import tls_search_batch + if args.method == "cuv_tls_matched": + qmn, qmx = gtls_qwin(periods) + kw = dict(qmin=qmn, qmax=qmx, n_durations=38, t0_oversample=8.0) + else: + kw = dict(n_durations=15, t0_oversample=3.0) + c0 = time.perf_counter() + res = tls_search_batch([(t, y, dy)], R_star=1, M_star=1, periods=periods, + oversampling_factor=3, refine_top_k=50, + u=[0.4804, 0.1867], limb_dark="quadratic", + return_arrays=False, **kw)[0] + drv.Context.synchronize() + dt = time.perf_counter() - c0 + print("RESULT %s %d nper=%d search_compile_s=%.3f P=%.4f SDE=%.2f" + % (args.method, args.baseline, len(periods), dt, + res.get("period", -1), res.get("SDE", -1))) From d62daaa840939374c924d61f79ea98dd1f85845f Mon Sep 17 00:00:00 2001 From: John Hoffman Date: Tue, 7 Jul 2026 10:29:59 -0500 Subject: [PATCH 5/6] =?UTF-8?q?docs:=20README=20performance=20pass=20?= =?UTF-8?q?=E2=80=94=20TLS-vs-GTLS=20+=20corrected=20BLS=20vs-0.2.6=20numb?= =?UTF-8?q?ers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add TLS to the survey-scale performance story: 30-170x faster than GTLS (the only other GPU TLS) at 1-3% SDE parity, 23-40x vs GTLS's own 4090 numbers, thousands-x vs CPU transitleastsquares (links analysis/GTLS_COMPARISON.md). - Update the experimental TLS description to the validated fast path (tls_search_batch): arbitrary ndata, SDE parity with reference + GTLS, still flagged experimental pending full injection-recovery. - Correct the BLS "vs previous release" framing: the v1.0 kernel is inherited from 0.2.6 essentially unchanged; the survey-speed campaign (PR #66) makes it 2.9-9.2x faster (kernel) / 2.0-12.7x (end-to-end), plus 34x from kernel caching in a naive loop. (Replaces a misleading "~1x" characterization.) Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 62 ++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index d469c0c..ed4a419 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ cuvarbase is built for processing millions of lightcurves, and it is proven in p The headline numbers, all traceable to benchmark data in this repository: - **Standard BLS is 257-354x faster than astropy's `BoxLeastSquares`**, measured consistently across all 7 GPU architectures tested (V100 through H200) +- **Transit Least Squares is 30-170x faster than GTLS** — the only other GPU TLS — on the same GPU at matched search settings and equal (1-3%) detection significance, and thousands of times faster than the reference CPU `transitleastsquares` ([details](#transit-least-squares-tls)) - **Keplerian frequency grids search 4-37x fewer frequencies** than uniform grids at survey baselines by exploiting the orbital-mechanics link between period and transit duration - **All four major surveys for ~$33 of GPU time**: running both Lomb-Scargle and BLS over ZTF + HAT-Net + TESS + Kepler scale lightcurve collections costs roughly $33 total on a rented RTX A5000 at $0.20/hr (tables below) @@ -41,6 +42,18 @@ frequencies, single lightcurves), nifty-ls on CPU is faster than cuvarbase's GPU LS — the GPU advantage appears at survey-scale frequency grids (>~100K frequencies) and batched workloads. Use nifty-ls for one-off small searches. +### Transit Least Squares (TLS) + +cuvarbase's survey-scale TLS ([Hippke & Heller 2019](https://ui.adsabs.harvard.edu/abs/2019A%26A...623A..39H/abstract)) is, to our knowledge, the fastest GPU TLS available. Reproducing the benchmark from the GTLS paper ([arXiv:2607.00348](https://arxiv.org/abs/2607.00348)) apples-to-apples on one RTX A5000 — identical Ofir period grid, matched per-period duration window, matched epoch density, one injected transit — cuvarbase-TLS is **30–170x faster than GTLS** over 200–2000 day baselines (the gap grows with baseline), at **1–3% detection-significance (SDE) parity** and 100% recovery: + +| Baseline | GTLS | cuvarbase TLS | Speedup | +|--------|-------:|-------------:|--------:| +| 200 d | 4.1 s | 0.14 s | **30x** | +| 1000 d | 75.8 s | 0.88 s | **86x** | +| 2000 d | 348 s | 2.0 s | **171x** | + +It also beats GTLS's *own* published RTX-4090 numbers by 23–40x from a slower A5000, and runs thousands of times faster than the reference CPU `transitleastsquares`. Full methodology and the reproduced figure: [analysis/GTLS_COMPARISON.md](analysis/GTLS_COMPARISON.md). + See [docs/BENCHMARK_RESULTS.md](docs/BENCHMARK_RESULTS.md) for methodology, competitive analysis, and cost projections. ## About @@ -76,12 +89,17 @@ This module ships in this release but has **known correctness issues** and is not recommended for science use yet. It emits a `UserWarning` on import. - **Transit Least Squares ([TLS](https://ui.adsabs.harvard.edu/abs/2019A%26A...623A..39H/abstract))** (`cuvarbase.tls`) - GPU transit - detection with optimal depth fitting and Ofir (2014) period grids. - The epoch grid is duration-scaled and failed trial periods are masked - out of the SDE/FAP statistics, but the rework has not yet been - validated against the reference `transitleastsquares` package. Light - curves above ~3,500 points exceed the kernel's shared-memory budget - (a `ValueError` is raised). + detection with a limb-darkened template, optimal depth fitting, and + Ofir (2014) period grids. The survey-scale fast path + (`tls_search_batch`, default) folds each light curve once into phase + bins and refines the top candidates exactly, handling **arbitrary + light-curve length** (the legacy per-point kernel is still available + and caps at ~3,500 points). Detection significance now matches both + the reference `transitleastsquares` and the GTLS package to **1–3%** + with 100% injected-transit recovery in our tests (see + [Performance](#transit-least-squares-tls)), but a full + injection-recovery completeness campaign is still outstanding — so it + remains flagged experimental and emits a `UserWarning` on import. - **NUFFT-based Likelihood Ratio Test** (`cuvarbase.nufft_lrt`, contributed by **Jamila Taaki** / [@xiaziyna](https://github.com/xiaziyna)) - @@ -203,23 +221,21 @@ v1.0 is a major modernization of cuvarbase — the first major release since the ### ⚡ Performance Improvements (Major Update) -**Dramatically Faster BLS Transit Detection** — **257-354x faster** than astropy `BoxLeastSquares`, consistent across all 7 GPU architectures tested (V100 through H200): -- Adaptive block sizing automatically selects the CUDA block size from - the dataset size. In the v1.0 release benchmark it measures parity to - ~1.3x over the fixed-block kernel on realistic Keplerian grids (RTX - A5000, Jun 2026; - `benchmarks/results/bls_adaptive_keplerian_benchmark_rtxa5000_jun2026.json`). - Earlier pre-release measurements showed 1.4-5.3x (up to 90x for tiny - lightcurves), but those gains shrank once thread-safe kernel caching - landed and amortized the per-call kernel handling the adaptive path - used to avoid -- Particularly beneficial for ground-based surveys and sparse time series -- Thread-safe kernel caching with LRU eviction for production environments -- **New function**: `eebls_gpu_fast_adaptive()` - drop-in replacement with automatic optimization -- Best cost-efficiency: RTX 4000 Ada at **$0.14 per million lightcurves** -- See [docs/BENCHMARK_RESULTS.md](docs/BENCHMARK_RESULTS.md) for full results across GPUs - -This optimization makes large-scale BLS searches practical and efficient for all-sky surveys. +**Faster BLS transit search** — **257-354x faster** than astropy `BoxLeastSquares`, consistent across all 7 GPU architectures tested (V100 through H200). Relative to the last release (0.2.6), whose BLS *kernel* v1.0 inherits essentially unchanged: + +- **Survey-speed kernels** (fused-noverlap, conflict-scatter, occupancy-aware + chunking) make the per-frequency kernel **2.9-9.2x faster** and end-to-end + survey searches **2.0-12.7x faster** than the pre-optimization v1.0 path +- **Batched multi-lightcurve search** (`eebls_gpu_batch`) is new — 0.2.6 offered + only single-lightcurve calls, which recompiled the kernel on *every* call; + v1.0's LRU kernel cache alone makes a naive per-lightcurve loop **34x faster** +- **Adaptive block sizing** (`eebls_gpu_fast_adaptive()`) auto-tunes the CUDA + block size from the dataset (~1.3x over the fixed-block kernel on realistic + Keplerian grids) +- Best cost-efficiency: RTX 4000 Ada at **$0.14 per million lightcurves**; + see [docs/BENCHMARK_RESULTS.md](docs/BENCHMARK_RESULTS.md) for full results across GPUs + +This makes large-scale BLS searches practical and efficient for all-sky surveys. ### Breaking Changes - **Dropped Python 2.7 support** - now requires Python 3.9+ From 51c2cb3975110495fb2065610a097f8218f5fc7b Mon Sep 17 00:00:00 2001 From: John Hoffman Date: Tue, 7 Jul 2026 12:49:41 -0500 Subject: [PATCH 6/6] TLS: fix UnboundLocalError in tls_search_batch refinement fallback (PR #68 review) _finish_lc's coarse-parameter fallback (taken when a light curve's top-K exact refinements all return the failure sentinel while the coarse scan still has valid periods) reads t0_h/dur_h/depth_h, but those were fetched only under `return_arrays or not K`. In the default batch path (refine_top_k=50 -> K>0, return_arrays=False) they were never bound, so reaching the fallback raised UnboundLocalError and aborted the whole batch. Surfaced by the adversarial PR review. Fetch the coarse arrays whenever the fallback can fire (any LC with all refined scores <= 0), still skipping the D2H on the common path. Adds a deterministic regression test that forces every refinement to the sentinel and asserts the default batch falls back instead of crashing. Co-Authored-By: Claude Opus 4.8 (1M context) --- cuvarbase/tests/test_tls_fast.py | 36 ++++++++++++++++++++++++++++++++ cuvarbase/tls.py | 9 +++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/cuvarbase/tests/test_tls_fast.py b/cuvarbase/tests/test_tls_fast.py index 05eeed5..62c6090 100644 --- a/cuvarbase/tests/test_tls_fast.py +++ b/cuvarbase/tests/test_tls_fast.py @@ -183,5 +183,41 @@ def test_bad_n_durations(self): tls.tls_search_batch([lc], n_durations=100) +class TestRefinementFallback: + """PR #68 review regression: the coarse-parameter fallback in _finish_lc + must not depend on return_arrays being set.""" + + def test_all_refinements_fail_falls_back_no_crash(self, monkeypatch): + # Force every top-K exact refinement to return the failure sentinel + # (rscore <= 0) while the coarse phase-binned scan still finds valid + # periods. With the default batch args (refine_top_k > 0 and + # return_arrays=False) the else-branch in _finish_lc must fall back to + # the coarse best-fit t0/duration/depth — it must NOT raise + # UnboundLocalError because those coarse arrays were fetched only under + # `return_arrays or not K`. + from cuvarbase import tls + orig = tls._get_cached_fast_kernels + + def patched(*a, **k): + kern = dict(orig(*a, **k)) # copy cached {'search','refine'} + + def fail_refine(*args, **kwargs): # rscore_g is positional arg 16 + args[16].fill(np.float32(-1.0)) + + kern['refine'] = fail_refine + return kern + + monkeypatch.setattr(tls, '_get_cached_fast_kernels', patched) + periods = shared_grid() + lcs = [make_transit_lc(3.3, 0.03, 0.012, seed=1), + make_transit_lc(7.7, 0.02, 0.012, seed=2)] + res = tls.tls_search_batch(lcs, periods=periods) # defaults + assert len(res) == 2 + for r in res: + assert 'error' not in r + assert np.isfinite(r['period']) and r['period'] > 0 + assert np.isfinite(r['duration']) and np.isfinite(r['depth']) + + if __name__ == '__main__': pytest.main([__file__, '-v']) diff --git a/cuvarbase/tls.py b/cuvarbase/tls.py index 65b4fc9..4e1b29d 100644 --- a/cuvarbase/tls.py +++ b/cuvarbase/tls.py @@ -1508,7 +1508,14 @@ def _band_block_size(nb): rdur_h = rdur_g[:nc * K].get().reshape(nc, K) rdepth_h = rdepth_g[:nc * K].get().reshape(nc, K) - if return_arrays or not K: + # Coarse per-period best-fit params. Needed when return_arrays is set, + # when there is no refinement (K == 0), AND as the fallback in + # _finish_lc when a light curve's top-K exact refinements all return + # the sentinel (the else-branch below reads t0_h/dur_h/depth_h). Fetch + # only when actually needed so the common default path pays no extra + # D2H. (`rscore_h` is only touched when K > 0, where it is bound.) + if (return_arrays or not K + or bool((rscore_h.max(axis=1) <= 0.0).any())): t0_h = t0_g[:nc * nperiods].get().reshape(nc, nperiods) dur_h = dur_g[:nc * nperiods].get().reshape(nc, nperiods) depth_h = depth_g[:nc * nperiods].get().reshape(nc, nperiods)