Skip to content

MAINT: signal.lombscargle: fix numerical backward-compat - #21697

Merged
tylerjereddy merged 6 commits into
scipy:mainfrom
adammj:lombscargle
Oct 13, 2024
Merged

tylerjereddy merged 6 commits into
scipy:mainfrom
adammj:lombscargle

Conversation

@adammj

@adammj adammj commented Oct 12, 2024

Copy link
Copy Markdown
Contributor

Reference issue

#21696

What does this implement/fix?

Change floating_mean default from True to False.

Additional information

Additional verbiage to be figured out. Initial commit is just a placeholder.

@adammj
adammj requested review from ilayn and larsoner as code owners October 12, 2024 00:18
@github-actions github-actions Bot added scipy.signal maintenance Items related to regular maintenance tasks labels Oct 12, 2024
@tylerjereddy tylerjereddy added this to the 1.15.0 milestone Oct 12, 2024
@tylerjereddy

Copy link
Copy Markdown
Contributor

Thanks for your patience on this. Perhaps the signal regulars will have a few suggestions given the ecosystem discussion in the matching issue.

@adammj

adammj commented Oct 12, 2024

Copy link
Copy Markdown
Contributor Author

No worries. I sympathize with the large ecosystem that makes use of scipy. But it also feels unfortunate not to default to the preferred/better option, and hope the user discovers it.

@adammj

adammj commented Oct 12, 2024

Copy link
Copy Markdown
Contributor Author

I've changed the default, and also tried to suggest in the docstring that the user might want to consider using floating_mean=True. I'm open to any further suggested modifications.

I still propose that the default should be changed eventually—with sufficient notice. I understand there are many downstream users. Because scipy is the most popular toolbox that has this functionality, and is geared towards generalists, I believe providing for a "floating-mean" (which is generally preferred and recommended in literature) is the better choice. On balance, the current status quo is more likely to produce erroneous results.

@DietBru DietBru left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only have cosmetic suggestions:

The following line

Input arrays should be 1-D with a real floating dtype.

could be changed to something like:

Input arrays should be 1-D of a real floating data type, which are converted into float64 arrays before processing.

It describes a little more accurately, what is happening.

Comment thread scipy/signal/_spectral_py.py Outdated
>>> ax_p.set_xlabel('Angular frequency [rad/s]')
>>> ax_p.set_ylabel('Power')
>>> ax_p.legend(prop={'size': 7})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cosmetic nitpick: If you add line with ... instead of a pure blank line, i.e,:

>>> ax_p.legend(prop={'size': 7})
...
>>> ax_n.plot(w, pgram_norm, label='default')

you'll get a continuous code block, which is easier to copy and paste.

@neutrinoceros

neutrinoceros commented Oct 13, 2024 •

Copy link
Copy Markdown

FWIW I checked that this patch resolves failures in astropy's test suite. Thank you !

@lucascolley lucascolley changed the title MAINT: lombscargle numerical backward-compat #21696 MAINT: signal.lombscargle: fix numerical backward-compat Oct 13, 2024

@tylerjereddy tylerjereddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expert reviewer seems happy, downstream seems happy, CI seems happy. The diff and rendered docstring seem "ok" to me (maybe a few places where more bacticks would render variable names a bit nicer, but not worth holding this up for).

I'll go ahead and squash merge with adjusted commit message now.

@tylerjereddy
tylerjereddy merged commit 4c2a19b into scipy:main Oct 13, 2024
@tylerjereddy

Copy link
Copy Markdown
Contributor

thanks @adammj @DietBru @neutrinoceros

@dnadlinger

Copy link
Copy Markdown

This still doesn't restore backwards compatibility for me in a library using lombscargle to identify the primary frequency component in a routine for fitting potentially randomly sampled and noisy/decaying sinusoidal data (https://github.com/OxfordIonTrapGroup/oitg/blob/master/oitg/fitting/sinusoid.py / https://github.com/OxfordIonTrapGroup/oitg/blob/master/test/fitting/test_sinusoid.py). Example:
image

The code might be a bit janky in terms of both numerics and implementation, but worked fine for many SciPy releases with precenter=True. To restore functionality on 1.15+, I need to manually do y - np.mean(y). floating_mean=True or False doesn't make a difference. Is this intentional?

@adammj

adammj commented Aug 16, 2025

Copy link
Copy Markdown
Contributor Author

@dnadlinger I'll take a look this weekend. Can you tell me which of your tests fail?

@dnadlinger

dnadlinger commented Aug 16, 2025 •

Copy link
Copy Markdown

@adammj: Oh, great, thanks a lot! I've pushed a branch without the workaround and with a Poetry lock file at https://github.com/OxfordIonTrapGroup/oitg/tree/scipy-lombscargle-repro. You should be able to reproduce the error by running poetry run python -m unittest test/fitting/test_sinusoid.py. If you don't use Poetry, just running the command with SciPy available in a checkout should probably also work; I don't think there are external dependencies. The tests use random data, so might pass by accident from time to time (it would be prudent to use a fixed seed…), but by and large, they all fail on 1.15+, but all pass on 1.14 (or 1.15 after manually subtracting np.mean).

The code has some plumbing (FitBase) to pipe additional parameter initialisers, etc. around, but in a nutshell, https://github.com/OxfordIonTrapGroup/oitg/blob/ce05f2780d050b19784abf0db46121a570ac8823/oitg/fitting/sinusoid.py#L55-L58 previously gave an estimate of the frequency of the sinusoid close enough for curve_fit to later converge, while the new implementation can be significantly off. Just copying out the lombscarge() call and applying it to the t/y values from the unit tests should show the difference between the old and new implementations.

@tylerjereddy

Copy link
Copy Markdown
Contributor

It might be useful to open an issue summarizing some of this to reduce the chances of it falling off our radar.

I think we're aiming for our next 1.16.x series release around Sept. 10th.

@adammj

adammj commented Aug 16, 2025

Copy link
Copy Markdown
Contributor Author

@tylerjereddy I don't think this is an issue with lombscargle.

@dnadlinger the issue arises in sinusoid.py. You're storing the mean of y (p_dict["c"] = np.mean(y) on line 59) -after- you call lombscargle on line 56. However, because you've set precenter=True, the following is run on line 249 of _spectral_py.py y -= y.mean(), which modifies y in place. I would recommend that you move p_dict["c"] = np.mean(y) to before the call to lombscargle. When you do this, all of your tests pass*.

I added an * there because you're using np.random without a seed. So, I can occasionally get 1 test (test_random_data) to fail when I run the unittests enough times. I'd recommend you add a seed to all of your unittests.

@dnadlinger

dnadlinger commented Aug 16, 2025 •

Copy link
Copy Markdown

@adammj: Thanks a lot for investigating this even without a reduced test case! Nevertheless, isn't this still a breaking API change? lombscarge(t, y, precenter=True) now modifies y in place, while it didn't before. This also isn't documented in the API docs.

Technically, the revamped doc comment does state that it performs y -= y.mean(), which is in place, but still, it's a breaking API change that should probably have been avoided (especially since precenter is supposed to be a legacy API) or at least documented as such.

@adammj

adammj commented Aug 16, 2025

Copy link
Copy Markdown
Contributor Author

I disagree with the characterization that "it's a breaking API change", because the previous API never said what it did with y. I'm in the camp that just because something is "misused" doesn't make that misuse an unwritten requirement for the API. I also don't speak for scipy's maintainers.

My only other thought is that changing that line from y -= y.mean() to y = y - y.mean() would unnecessarily increase the memory requirements.

@dnadlinger

Copy link
Copy Markdown

@tylerjereddy: I created #23474 to track further discussions.

@adammj: Thanks – I responded to your points in the description of #23474 as well. My view as a library developer is that silent breaking changes to "reasonable" user progams (i.e. even in the absence of explicit guarantees, what a user might reasonably expect) are the worst kind of breakage, especially in minor versions, as they are harder to track down than any acutal breaking API change. I'd rather see the flag removed outright, because then there would at least be an obvious error message (practially, of course, we'd want to deprecate it first).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintenance Items related to regular maintenance tasks scipy.signal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants