Release v1.2.0: pre-whitening, native multi-band, SuperSmoother, LINCC interop - #17
Open
tjayasinghe wants to merge 32 commits into
Open
Release v1.2.0: pre-whitening, native multi-band, SuperSmoother, LINCC interop#17tjayasinghe wants to merge 32 commits into
tjayasinghe wants to merge 32 commits into
Conversation
…tors Frequency analysis of classical pulsators (delta Scuti, gamma Dor, SPB) still funnels through interactive Period04-style sessions, one star at a time, with the stopping decision left to the operator's eye. This adds the whole loop as a batch-capable, GPU-accelerated function whose every judgement call is an explicit, recorded setting. solution = cup.prewhiten((time, mag, mag_err)) print(solution.summary()) - prewhiten/spectrum.py: a least-squares amplitude spectrum on the cufinufft / finufft / torch / numpy backends. The Gram terms of the normal equations depend only on the observation times, so SpectrumEngine computes them once and each iteration costs a *single* transform: a 50-frequency solution needs ~52 transforms, not ~150. Matches a brute-force weighted least-squares fit to 5e-16; backends agree to 1e-9. - prewhiten/fit.py: simultaneous multi-sinusoid least squares by variable projection with Kaufman's Jacobian, so the optimiser sees K frequencies rather than 3K+1 parameters and every linear parameter is solved exactly. Three refinement policies; narrow designs go through Cholesky normal equations, wider ones through a rank-revealing gelsy. - prewhiten/engine.py: the extraction loop, with combinable stopping criteria (Breger S/N, Baluev FAP, delta-BIC, amplitude floor), a Loumos & Deeming resolution guard, bounded refinement so a frequency cannot slide onto its neighbour, and a post-polish prune so every reported component still passes the test it was admitted by. The reason a run stopped is always recorded. - prewhiten/uncertainty.py: least-squares covariance (default, and the only estimator that sees the correlations between close frequencies), the Montgomery & O'Donoghue analytic formulae, and a residual bootstrap, each optionally inflated by the Schwarzenberg-Czerny correlation factor. Phases are referenced to the weighted mean epoch, at which phase and frequency are uncorrelated. Monte Carlo over 200 realisations: reported 1-sigma errors match the realised scatter to within ~5% in frequency, amplitude and phase. - prewhiten/combinations.py: harmonics and combination frequencies with uncertainty-aware tolerances and a chance-coincidence rate per identification, so a meaningless match is visible as one. - prewhiten/spacing.py: g-mode comb scan (immune to missing radial orders, and promoted past the sub-multiple ambiguity that makes a naive scan report dP/2), tilted period-spacing series extraction, echelle coordinates, buoyancy radius. - prewhiten/batch.py: the same over many curves on the existing worker pools, one row per extracted component. Timings on this machine (CPU, finufft): 3000 points / 3 modes 0.05 s; 20 000 / 15 modes 1.2 s; 100 000 / 20 modes 6.3 s. GPU and CPU solutions agree to 3.5e-15 in frequency. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cuperiod prewhiten star.csv --snr 4.6 -n 30 --spacing --csv modes.csv cuperiod batch-prewhiten "lcs/*.csv" --out modes.parquet --workers 8 `prewhiten` prints the ranked components with their uncertainties and S/N and the reason the run stopped, optionally searches the independent modes for a g-mode period-spacing pattern (--spacing), and can write the full solution as JSON, the component table as CSV, and the amplitude spectra as .npz. `batch-prewhiten` takes the same extraction options and the same worker/sink options as `batch`, writing one row per extracted component. Both build a validated PreWhitenSettings from the flags, so an impossible combination is a BadParameter rather than a traceback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An **Analysis** picker switches the window between *Periodogram* and *Pre-whitening*. Everything shared stays shared — the same controller, the same loaders and source browser, the same spectrum / phased / raw views, the same off-thread compute with generation gating and result caching — and only the result docks swap. The existing workflow is untouched: no periodogram code path changed behaviour, and the three result docks are tabbed together so switching analysis never rearranges a layout the user has arranged. - controls_panel: the analysis picker hides the method and peak-count rows and rebuilds the options form from PreWhitenSettings. The form is generated by the existing PydanticSettingsForm, so every pre-whitening knob is exposed with the right type, bounds and tooltip without a single bespoke widget. Settings are cached per analysis, so switching back restores your values. - compute: PreWhitenTask runs the extraction on the worker pool, sharing the generation counter with PeriodogramTask so switching mid-run supersedes cleanly. - state: run_prewhiten() with its own LRU cache (ResultCache is now generic over its value type), so flipping between analyses of the same star is instant. - solution_panel: the extracted components with 1-sigma uncertainties, S/N, FAP and any combination identification, plus the stopping reason and fit statistics. Selecting a row folds the phased view on that component, exactly as a periodogram peak does. Copy and full-precision CSV export. - spacing_panel: the comb scan over trial spacings and the echelle diagram, run on demand over the *independent* components so identified combinations cannot pollute the pattern. Reports the mean spacing, its tilt, the rms, and Pi_0. - spectrum_view: gains an overlay trace, so pre-whitening reuses the full spectrum view — downsampled full-resolution rendering, draggable selection band, log axes, crosshair readout, CSV/PNG export — with the amplitude spectrum as the main curve, the residual spectrum overlaid, and the components as peak markers. Fixes a bug found driving the window headlessly: switching analysis cleared the phased view's light curve (not just its fold), leaving an empty panel until the next file load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- guide/prewhitening.md: what one iteration does, the stopping criteria and how pure a frequency list they actually buy, the three uncertainty estimators and why phases are referenced to the weighted mean epoch, combination frequencies and their chance-coincidence rate, the g-mode period-spacing tools, batch and CLI usage, and a settings reference. With the primary literature it implements. - Wired into the guide index, the site index (toctree + card + intro), the API reference (a Pre-whitening section plus the two new settings models), the CLI guide (both new commands with full option tables), and the GUI guide (the Analysis picker and the two new docks). - README gains a pre-whitening section and the new CLI lines; the quickstart gains a short pulsator example; CITATION's abstract describes the capability. - version -> 1.2.0.dev0 (bumped to 1.2.0 at release), docs conf fallback -> 1.2.0, and asteroseismology/pulsations/pre-whitening added to the package keywords. Docs build clean under -W. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five independent reviewers over the new code, each finding verified by two skeptics who tried to refute it by running it. Eight candidates, five survived. spacing.py — a legitimate period-spacing series was silently reported as absent. The refit loop aborted whenever the fitted *intercept* of `dP(P) = a + b*P` went non-positive. `a` is a nuisance parameter of the parameterisation, not a spacing: it is negative for any series whose spacing grows steeply, and the guard also fired on the noisier intermediate fits made on partial chains. Only `a + b*P` over the observed range must be positive, and `_longest_chain` already rejects pairs where it is not. Guard removed; step counts now go through `_step_counts`, which treats a non-positive prediction as "one order" instead of letting it produce a negative count. A 20-mode gamma Dor series (dP 1858 -> 4190 s) went from `None` to 20/20 modes; over 200 realisations with 20 s scatter, `None` returns went from 165 to 0. spacing.py — the sub-multiple promotion could report a *multiple* of the truth. The promotion test asks whether widening the comb keeps its teeth, which is a counting argument, but it was judged on the amplitude-weighted response: with a 20:1 amplitude spread, dropping every other (weak) mode barely moved it and a 12-mode comb at dP=0.03 was promoted to 0.06 — a factor-two error in the headline Pi_0. It is now judged on the unweighted response of the winning peak's *own members* (`_comb_members`), which is blind to both the amplitude spread and to contaminating peaks that are on neither comb. The peak itself is still chosen on the weighted response. Over 300 combs with lognormal(0, 2) amplitudes, wrong spacings went from ~7% to 0. gui/state.py — switching the Analysis picker mid-run wedged the app. Clearing the pending key means no completion handler will ever fire, so the `busy_changed(False)` that re-enables Compute never arrived and the button stayed disabled for the rest of the session (Ctrl+Enter too, since it gates on the same state). `set_analysis` now releases the busy state itself. gui/widgets/controls_panel.py — pre-whitening a multiband curve could silently analyse a raw all-band stack. `set_bands` passed the *hidden* method combo's text instead of the pre-whitening sentinel, so a curve loaded after switching analysis offered and selected "combined (all bands)". On a two-band curve 0.8 mag apart that returned 0 components (rms 0.40) where the single band recovers the planted signal cleanly. The two load orders now agree. The "stacked" option additionally centres each band first (`_stack_centred`) — pre-whitening fits one constant for the whole curve and cannot absorb per-band zero points. prewhiten/batch.py — a directory sink could produce an unreadable dataset. Component cells were Python `None`, so a chunk in which no star had an identified combination typed that column `null` while a later chunk typed it `string`; a pyarrow dataset takes its schema from the first fragment and the cast is not implemented. Cells are now NaN/"" filled with stable types (`rank` included, as float), matching what `periodogram_to_row` already did. Nine regression tests added, each failing before its fix. 414 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Improvements from a second hardening pass over the v1.2 subsystem: - fix: bootstrap frequency errors were exactly zero for every component except the newest. The engine handed its per-iteration refinement policy (default last, which pins all established frequencies) to the replicate fits, so their scatter collapsed. Replicates now always sweep every frequency, boxed by the same per-frequency bounds as the fit they characterise. - feat: the spectral window as a first-class diagnostic. SpectrumEngine .window(), cuperiod.spectral_window(), and PreWhitenResult.window expose |W(f)| at zero extra transforms (its sums are already part of the cached normal equations). The GUI spectrum view gains a window overlay toggle scaled to the tallest peak, and prewhiten --save-spectrum writes it into the .npz. - perf: the Baluev (2008) false-alarm probability is evaluated natively (cuperiod.baluev_fap), matching astropy to 1e-12 on centred times and staying accurate on raw Julian dates where astropy's one-pass time variance loses ~11 digits. Pre-whitening no longer imports astropy.timeseries: ~1 s off the first solution per process. - perf: the VarPro Jacobian is built lazily, so trust-region steps the optimiser rejects never pay for derivatives; batch_prewhiten forces store_spectra=False in workers. - gui: reclaimed spectrum-toolbar space (tighter spacing, Reset, pinned x-combo width) so the new toggle never squeezes buttons into ellipses. 441 tests pass (+21 new), ruff/mypy clean, docs build under -W. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default search band topped out at the median-gap pseudo-Nyquist with nyquist_factor=1, which for nightly ground-based sampling is ~0.5-2.5 cycles/day. On the bundled ASAS-SN HADS demo (VSX P = 0.089757 d, f = 11.14 c/d) the entire signal sat out of band, so the extraction fitted its daily aliases instead: P = 0.123 d, residual rms 0.245, chi2_r 581 -- a plausible-looking, completely wrong frequency solution. The automatic band is now max(pseudo-Nyquist x nyquist_factor, 50 c/d), with nyquist_factor defaulting to 5 like every periodogram method, exposed as cuperiod.prewhiten.default_maximum_frequency. The GUI computes its auto value (grid hint and greyed spin box) through the same helper, replacing its hard-coded per-analysis factor and its own too-low 10 c/d floor for the pre-whitening analysis. The demo star now resolves to P = 0.089757 d -- the VSX period to the last digit -- with the 2f/3f/4f harmonics extracted and combination-labelled, residual rms 0.073, and a clean HADS sawtooth in the phased view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The frequency-domain periodograms pay one trig sum per trial frequency, so the wide band that keeps delta Scuti / HADS stars in reach is free for them. Their GUI auto maximum now shares the pre-whitening floor (max(pseudo-Nyquist x factor, 50 c/d)) through one predicate, gui.meta.reaches_short_periods(), used by both the controller tuning and the grid-hint display. GLS now recovers the bundled HADS demo (best P = 0.0897567 d, FAP 7e-203) where its old 10 c/d auto ceiling saw only aliases. Fold-based methods (PDM, CE, string-length) pay a full fold per trial frequency and deliberately keep the 10 c/d floor; library-side defaults for GLS/MHAOV are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The markers were placed at each component fitted amplitude while the curve underneath is the single-frequency amplitude spectrum. The two agree for a well-separated mode -- which is why synthetic tests never caught it -- but diverge as soon as components are correlated. On the bundled ASAS-SN HADS demo every harmonic carries yearly alias sidelobes (df = 1/365.25 d = 0.00274 c/d, ~6.6 Rayleigh), so three or four components sit around each harmonic and trade amplitude in the joint fit. F3 (3f) is fitted at 0.0855 where the data holds 0.0466 -- confirmed by an independent single-sinusoid fit, 0.0467 -- and F9 at 0.0335 where the spectrum has 0.0073. The high ones hung in empty space above the curve, claiming peaks that are not there; the low ones hid inside the forest and merely looked plausible. A marker on a spectrum plot asserts that the curve is that high there, so it is now drawn at the plotted spectrum sampled at the component frequency (vectorized: the pre-whitening grid runs past a million samples and this is on the UI thread). The fitted amplitude is not lost -- it moves to the hover readout with the component S/N, next to the Frequencies dock that already lists both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fitted amplitude comes from the joint solution of every component at once; the amplitude spectrum reads each frequency as if it were alone. They agree for a mode resolved from its neighbours, and diverge when a component is correlated with another -- close pairs, and far more often in ground-based data, a mode beside its own alias sidelobe. Every Sinusoid now records spectrum_amplitude (the direct, single-frequency measurement), the derived amplitude_ratio, and a blended flag governed by PreWhitenSettings.blend_tolerance (default 2x -- loose enough to be a ~3 sigma statement even for a component that only just cleared S/N 4, since the ratio carries the noise of both measurements). On the bundled HADS demo 4 of 17 components are flagged, from 0.24x to 4.62x. It is deliberately not a significance test: an alias sidelobe is really in the data, and nothing is dropped because of the flag. It says the amplitude means something only alongside the components it is correlated with. Surfaced everywhere: PreWhitenResult.n_blended, an A/Asp column and a footnote in summary(), two float columns in the batch catalogue (no boolean -- a NaN fill has no boolean equivalent for a star that yielded nothing), and a marked column in the GUI Frequencies dock. Also in the GUI, from inspecting the same star: - the data curve gets its own toggle, so the residual and window traces it draws over can be read on their own; - double-clicking the spectrum restores the default view; - fix: the Frequencies table ignored its own per-column number formats. The sort key was written to EditRole, which QTableWidgetItem stores in the same slot as DisplayRole, so every numeric column rendered Qt six-digit default -- too few digits for a frequency, too many for an uncertainty. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hover label is a TextItem anchored to a marker in plot coordinates, and nothing ever hid it once that marker stopped existing. Unchecking peaks right after hovering one to read its numbers -- the natural order, since you hover to read and then hide the clutter -- left the label stranded over an empty plot. The same stale label survived a new result (showing the previous solution numbers over the new spectrum) and a log/axis switch that moved its anchor to coordinates it no longer matched. It is now dismissed whenever the markers are redrawn, which covers all three, and it reappears on the next hover event -- those fire on every mouse move over a marker, so the label is back within a pixel of movement. Hovering a glow halo, which carries no peak index, also dismisses it instead of leaving a different peak numbers up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…kers The shaded band marks a selected peak, so it belongs to the peak layer. Leaving it on screen when the markers were hidden kept the single most prominent piece of clutter the toggle is usually turned off to be rid of. Band visibility is now one rule -- shown iff something is selected and peaks are being marked -- applied through a single helper rather than the five scattered setVisible calls it replaces. The selection itself is untouched while hidden, so the phased view keeps folding on the same period and the band returns in place when peaks come back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replace the astropy-delegated multi-band Lomb-Scargle with three native models (VanderPlas & Ivezić 2015), all NUFFT/GPU-fast on finufft, cufinufft, and torch: - offsets (default): the shared-phase (1,0) model in closed form — the per-band constant offsets are profiled out analytically, leaving a band-projected Zechmeister-Kürster assembly fed by K+2 NUFFTs. ~400x faster than astropy's flexible solver on a 6-band, 200k-frequency search, and exactly the single-band GLS when K=1. - perband: the multi-phase (0,1) model with the *published* chi2_0 weighting of eq. 23 (astropy's 'fast' method mis-weights bands by the summed squared periodogram, which depends on the frequency grid). - flex: astropy's flexible regularized model reproduced to ~2e-10 from per-band harmonic trig sums plus batched normal-equation solves, including the trace-scaled ridge and both center/ridge conventions. Multi-band false-alarm probabilities (astropy has none): a within-band bootstrap of the null peak power. Times never change under the null, so on NUFFT backends all resamples batch through the same K+2 transforms via n_trans stacking. Exposed as cup.multiband_fap() and, via GLSSettings(mb_fap_bootstrap=...), as extras['fap'] at spectrum peaks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ength Each band keeps its own mean light curve / histogram / normalized fold; only the statistics are pooled — dof-weighted for PDM (Stellingwerf fixed-dof convention), point-count-weighted for CE and string length. Every periodogram method now accepts a MultiBandLightCurve, on every backend the single-band kernels support. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MultiBandLightCurve.from_file reads a long-format CSV/ECSV/FITS/Parquet table and splits it on a (auto-detected or named) band column without a pandas dependency. The CLI's 'run --band' now actually produces a joint multi-band fit (it silently dropped to single-band before), and file-path batch inputs honor band_column, so a directory of survey tables runs multi-band end to end. Also adds benchmarks/multiband_recovery.py: single-band vs multi-band recovery on simulated Rubin-like six-band cadence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cup.alias_diagnostics(pg, data) measures the sampling's spectral window, predicts the alias family of the best peak (window-peak aliases f0 +/- m*f_w, harmonics, subharmonics; classic sidereal-day/solar-day/synodic-month/year suspects when no data is given), matches each prediction to the periodogram's local optima within a Rayleigh-width tolerance, scores the competitors, and issues an AMBIGUOUS verdict when a non-harmonic alias rivals the chosen peak. Works with both objective senses (GLS/BLS/MHAOV maxima and PDM/CE/string-length minima) and multi-band input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two tiers over the same column-resolution core (survey presets for ZTF DR22, ZTF alerts, and Rubin DP1 object/DIA forced sources, plus ColumnMap auto-detection from the nest's schema without computing): - nested_periodogram: row-wise map_rows adapter for a NestedFrame or an lsdb Catalog (meta auto-built), single- or multi-band, per-row failures yield NaN instead of killing a survey-scale run. - partition_periodogram: partition-wise batched kernel that pulls the flat Arrow buffers + list offsets once per partition and reuses one GPU engine (cufinufft plan cache) across every object in it — the catalog-scale GPU path nothing in the LINCC stack provides today. Targets the post-rename API (map_rows/join_nested) so one code path covers nested-pandas 0.6.10 (the lsdb/pandas-2 world) and 0.7.x (pandas 3). New optional extras [nested] and [lsdb]; a [tool.uv] conflict forks the lock so the lsdb extra's nested-pandas<0.7 pin cannot downgrade the dev environment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The (chunk, p, p) normal-equation stack now targets a fixed byte budget instead of a fixed frequency count, so large-nterms/many-band models cannot blow device memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…99.7% joint 300 simulated faint RRab (0.2 mag noise, six-band WFD epoch shares) at 30/60/120 total epochs over 3 years. The shared-phase offsets model recovers 81.7%/99.7%/100% where the best single band recovers 0%/37.3%/97.3% — and it dominates the per-band-phase models (17.7%/20.0% at 30 epochs), confirming shared phase is what buys sparse-cadence recovery and motivating it as cuPeriod's multi-band default. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rewrites docs/guide/multiband.md around the native subsystem (three GLS models with selection guidance and the Rubin-cadence recovery table, pooled fold methods, bootstrap FAP, alias diagnostics), adds docs/guide/interop.md for the nested-pandas/lsdb adapter (both tiers, survey presets, dask-GPU deployment, the two dependency worlds, Rubin flux-domain guidance), updates the API reference, quickstart, index, installation, README positioning, and the benchmark suite README, and merges the multi-band entries into the unreleased 1.2.0 changelog block. Also fixes a stale example in the multiband_fap docstring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The batch runner and the interop partition kernel built a CufinufftGLS engine per worker/partition but dropped it on the multi-band branch, so every star paid full plan setup again. multiband_power now accepts the same engine the single-band path uses: the offsets model's K+2 transforms, the flex harmonic sums, and the perband per-band powers all route through the engine's bucketed plan cache (a plan is fixed by mode count and n_trans alone, so one pair serves every band, harmonic, and star). The bootstrap-FAP pass deliberately stays planless - its n_trans varies with the grid, and caching a plan per value would balloon device memory. Fold methods accept and ignore the parameter; their kernels are already module-cached. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…k section COLUMN_PRESETS was re-exported through a bare import, which autosummary cannot attach a docstring to; an explicit assignment with a #: comment puts it back in the API reference. The benchmarks page gains a section mirroring the multi-band recovery table (0% -> 81.7% at 30 epochs) so the headline result is visible next to the other benchmark evidence, not only in the guide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Friedman's variable-span smoother as a phase-folding period search - the last method named in the v1.2 goal. One vectorized array-API kernel (numpy CPU / cupy CUDA / torch anywhere) plus a numba-parallel CPU tier that runs 20k trial frequencies on a 600-point curve in 0.05 s. Every window sum comes from prefix sums over circularly padded folds, so the periodic path is exact and the plain smoother's edge pathologies cannot occur; the leave-one-out span selection subtracts each point's own contribution, matching the reference algebra identically. Scores follow gatspy (1 - mean|resid|/dy over the baseline deviation, maximized); the multiband variant pools per-band scores with gatspy's baseline-error weights. Parity vs the supersmoother package and gatspy is asserted to ~1e-9 on point counts where the upstream odd-window fix (adopted here) coincides with the released 0.4 truncation semantics. Deliberate deviations, all documented: degenerate (duplicate-phase) windows fall back to the weighted mean instead of raising, and the bass-enhancement factor is clamped to close the reference's alpha-in- (9,10) NaN bug. Both reference packages join the dev extra (BSD-2) as test-only pins; the numba mypy override now tolerates releases without py.typed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A deliberately synthetic sparse six-band RRab (the one simulated star in the tour, and the notebook now says so): 61 epochs over three years at WFD-like shares, where the deepest single band lands 21% from the truth with the true period nowhere in its top five, and the joint offsets search recovers it to 0.004%. The section walks the full loop - the pseudo-Nyquist grid gotcha (both the single- and multi-band defaults top out below the true frequency on this cadence), bootstrap FAP at the 1/(n+1) floor, an alias_diagnostics clean verdict over the +-1 c/d daily aliases, and all six bands folded onto one shape. Executed end-to-end; outputs embedded like the rest of the notebook. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Method catalogue section (statistic, non-parametric strengths, the integer-multiples caveat and how to read a high-scoring family), rows in the decision/backends/performance/multiband tables, tuning highlights, and changelog entries covering the method, its backends (0.05 s numba vs 6.6 s numpy at 20k frequencies), the gatspy-exact multiband pooling, and the reference-parity pins with the four documented deviations. Package- wide counts move to eight methods while every benchmark-suite claim stays scoped to the seven methods the suite actually ran - SuperSmoother is pinned against its reference packages in unit tests instead, and the benchmarks page now says exactly that. Also records the multi-band GLS plan-cache reuse under a new Performance heading. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…RR Lyrae Bundle 100 SDSS Stripe 82 RR Lyrae (Sesar et al. 2010) with literature periods -- real ugriz cadence, 80 RRab + 20 RRc, first-100-by-ID with no quality selection -- via a re-runnable download script pulling from the astroML-data mirror (the original MPIA host is dead), with the parsed column layout pinned against gatspy doctest values. benchmarks/multiband_real.py runs one identical blind search per star (0.15-1.2 d, 5 samples per Rayleigh, ~97k frequencies, default settings) through all nine multi-band models plus per-band GLS baselines, scored strict (1%, no harmonic credit) and harmonic-aware (2%), CPU scored and GPU compared. Headlines: PDM and String-Length 93% strict (String-Length 97% harmonic-aware), SuperSmoother 96% harmonic-aware with the documented 2P-multiples gap concentrated on RRc, the three GLS models comparable at 76-78% on this dense data (the simulated benchmark covers the sparse regime where offsets wins), 54% of non-harmonic misses on the +-1/+-2 c/d window aliases, GPU picking the identical top period on 100% of runs. make_report.py renders it as REPORT.md section 4 (table with Wilson CIs, alias-loci figure, data-driven prose) and renumbers later sections, fixing a pre-existing duplicate Table 3 label along the way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gelog New section 7 on the docs benchmarks page and a matching section in benchmarks/README (setup, nine-model table, single-band baseline, honest reading incl. the dense-vs-sparse regime split and the SuperSmoother 2P-multiples gap); SuperSmoother suite-scoping paragraphs updated now that it is exercised on real data; stale REPORT.md deep link fixed for the section renumbering; one-sentence Stripe 82 headline in the README validation bullet; changelog entries for the dataset and the validation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
batch_periods now defaults to 0 = auto-sized from a transient-memory budget (~512 MiB of workspaces on device backends, the previous chunk sizes on host numpy, always adapted to the light-curve length), and SuperSmoother accumulates scores on the device with one final host copy instead of a stream-synchronizing copy per chunk. Chunking never affects the result; explicit batch_periods values are honored as before, and estimate_device_bytes now reflects the device budget. Single-shot GPU multiband on the Stripe 82 validation stars: MHAOV 23.8 s -> 0.48 s/star (now at CPU-tier parity), SuperSmoother 23.7 s -> 0.87 s/star, identical spectra on every star. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fresh cpu+gpu passes over all 100 stars with the fixed chunking: recovery is bit-identical (deterministic search), GPU top-period agreement is again 100.0% of 900 runs, and the GPU timing columns now show MHAOV 0.48 s/star and SuperSmoother 0.87 s/star instead of ~24 s. The report generator's laggard sentence no longer attributes slowness to the old fixed batch_periods chunking (and agrees in number), and the single-curve performance section notes that its recorded MHAOV GPU rows predate the fix. multiband_real.py gains --out so partial verification runs cannot clobber the committed results parquet; docs/benchmarks.md CPU medians synced to the fresh run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
benchmark.py gains a --sections CLI (default keeps the resumable skip-if-parquet-exists behaviour; naming sections forces a re-measure) and SuperSmoother joins FREQ_SETTINGS and both scaling sweeps, giving it its first recorded single-curve numbers (cpu 0.099 s, gpu 0.284 s, torch:cuda 0.222 s at the 30k grid). Reference-tool timings now fail soft to a blank cell when the ad-hoc install was pruned. Fresh sweep on a quiet machine with torch 2.11.0+cu128 restored: untouched methods reproduce the July numbers to within noise, and the v1.2 auto-batch fix shows up exactly where expected - single-band MHAOV GPU 0.135 s -> 0.038 s (0.18x -> 0.68x vs the numba CPU tier, and a near-wash across the whole grid-size sweep). make_report.py now computes the win/wash/slower buckets, the torch and batch-throughput claims, and the section-8 method lists from the parquets instead of hard-coding them, and the predates-the-fix caveat is gone. Docs tables and prose in docs/benchmarks.md and docs/guide/backends.md synced to the new numbers, including retiring the SuperSmoother not-yet-measured row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Pre-whitening reported a correlation factor the bootstrap never applied. The engine recomputed Schwarzenberg-Czerny D from the residuals and stored it whatever the estimator was, but `component_uncertainties` deliberately skips the sqrt(D) inflation for `uncertainty="bootstrap"`. So a bootstrap run printed `D=6.25` over error bars carrying no factor, and wrote the same wrong number to every `batch_prewhiten` catalogue row. The stored value is now the one the estimator actually applied. - `MultiBandLightCurve.from_dataframe` ignored `band_column` when `columns=` was also given: the explicit ColumnMap replaced the map built from `band_column`, so the call raised ColumnResolutionError telling the caller to pass `band_column` — which they had. Now merged, matching `from_file`. - The LINCC interop kernel called `multiband_power` without the `supports_multiband` check `api.py` performs, and `_Kernel.evaluate` turns per-object failures into NaN rows — so a multi-band run with a single-band method returned a silently all-NaN frame. Validation is hoisted into `_Kernel.validate()` and called up front by both entry points. Each gets a regression test. Also corrects the docstrings that stated the old behaviour (the "all three estimators are inflated" claim, the "re-fit all components simultaneously" step, `max_gap`'s units, `prune`'s coupling to `stop_criteria`, `SpacingSpectrum.best_spacing`'s sub-multiple promotion, the 152-vs-60 coefficient-vector count, and `SpectrumEngine`'s two- vs three-point minimum). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A pre-push audit compared every user-facing claim against the code and the benchmark parquets. Fixes, by theme: Release metadata (following the v1.1.0 pattern in f1e83d0): version 1.2.0 in pyproject, the __init__ fallback and uv.lock; CITATION.cff bumped to 1.2.0 / 2026-08-14 and its abstract corrected to eight methods (SuperSmoother was missing); CHANGELOG [Unreleased] retitled to [1.2.0] with a compare link; README BibTeX synced. Stale benchmark numbers, re-synced to benchmarks/results/*.parquet after the auto-batch re-runs: peak batch throughput 587 -> 574 lc/s (README, docs/guide/batch.md), CPU-vs-reference 18x -> 20x and 2106x -> 2177x (README), the multi-band median CPU s/star column (benchmarks/README.md), the sinusoid CE/String-Length injection cell 42-98% -> 98-100% (docs/benchmarks.md), and the GLS/MHAOV GPU parity bound ~1e-6/1e-7 -> ~1e-6 and ~1e-5. Claims that contradicted the code: the [fast] extra covers every method but GLS, not all eight; SuperSmoother's 2P/3P family is bounded by minimum_frequency, not maximum_frequency; the BLS peak extra is `t0`, not `transit_time`; `--band` is the multi-band switch, not a column-name override; multiband_fap's second argument is a GLSSettings; nyquist_factor means the average-sampling Nyquist for GLS and the median-sampling one elsewhere; precision="auto" forces float32 on MPS only; `cuperiod methods` lists every backend, not the installed ones; n_skipped counts light curves; only PDM is benchmarked at batch parity; the GUI has no menu bar, its Band selector is in Controls, and it opens empty rather than with a demo curve (examples/data is not shipped in the wheel). CHANGELOG: bug fixes moved out of ### Performance into a ### Fixed section, the 16 MyST roles stripped so they render on GitHub and PyPI, and the HADS "before" numbers corrected — they had been quoted from two different runs. benchmarks/REPORT.md regenerated from the committed parquets; examples/cuperiod_tour.ipynb re-executed (its stored output still advertised version 1.0.0 without SuperSmoother, and printed the BLS depth as 0.0000 because it is an absolute flux, now shown as a fraction). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts v1.2.0.
mainis an ancestor of this branch, so it fast-forwards.What's in the release
cuperiod.prewhiten): iterativesinusoid extraction with principled stopping criteria, error propagation,
combination-frequency identification, and g-mode period-spacing tools — plus a GUI
analysis mode and
prewhiten/batch-prewhitenCLI commands.joint models (
GLSSettings.mb_model) and bootstrap FAP; pooled multi-band PDM, CE andstring-length; multi-band SuperSmoother.
pinned against the
supersmootherandgatspyreference implementations, with anumba-parallel CPU tier and array-API kernels for cupy/torch.
cuperiod.alias_diagnostics) for any periodogram.cuperiod.interop): run periodograms directly overnested-pandas
NestedFrameand lsdb HATS catalogues.Pre-release audit
The last two commits are a pre-push audit that compared every user-facing claim against the
code and the benchmark parquets. It found and fixed:
Schwarzenberg-Czerny
Dthe bootstrap estimator never applied (and wrote it to everybatch_prewhitenrow);MultiBandLightCurve.from_dataframediscardedband_columnwhencolumns=was also given (this one shipped in v1.1.0); and the LINCC kernel skipped thesupports_multibandcheck, so a multi-band run with a single-band method returned asilently all-NaN frame.
18× → 20×, 2106× → 2177×, plus the multi-band timing column and the GPU parity bounds.
CITATION.cff(still 1.1.0, and stillclaiming seven methods), and the
[Unreleased]changelog heading.Verification
pytest -qruff check src testsmypysphinx-build -W --keep-goinguv build+twine checkAfter merge
Tag
v1.2.0and push it —release.ymlpublishes to PyPI onv*tags via Trusted Publishing.