pop_resample computes a wrong resampling ratio for non-integer sampling rates, and labels the result with the requested rate
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 85/100
Research direction
Start in src/eegprep/functions/popfunc/pop_resample.py at _resample_ratio and trace how its p and q values reach _resample_segment and event-latency rescaling. Add focused tests for the listed non-integer rate pairs and a pop_resample round trip, then verify that the returned ratio, output data length, pnts, and srate agree.
Written by the indexing model from the issue text.
Description
Found while auditing the base dependencies for browser installability (#395). The dependency question turned out to be the smaller half.
The defect
src/eegprep/functions/popfunc/pop_resample.py:208:
def _resample_ratio(freq, srate):
rational_approx = sp.nsimplify(float(freq) / float(srate), tolerance=1e-12)
p, q = rational_approx.as_numer_denom()
return int(p), int(q)
sympy.nsimplify looks for a simple symbolic expression, not a rational one. When the ratio is not close to a simple fraction it returns an irrational product, and then as_numer_denom() hands back a numerator that is not an integer, which int() silently truncates.
For a 2000 Hz recording resampled to 999.9 Hz, the true ratio is 0.49995:
nsimplify -> 2**(222/367)*3**(8/367)*5**(270/367)*7**(31/367)/12
as_numer_denom -> p = 2**(222/367)*... (float 5.999400), q = 12
int(p), int(q) -> (5, 12) -> ratio 0.416667 # 16.66% low
p and q then drive both the polyphase resampling (_resample_segment(segment, p, q, ...), line 183) and the event-latency rescaling (line 199), while line 192 stamps output["srate"] = float(freq) unconditionally. So the data is resampled by the wrong factor and then labeled with the rate that was requested. Event latencies are rescaled by the same wrong ratio, so the result is internally consistent and gives the reader nothing to notice.
How often
Sweeping realistic downsampling pairs, source rates 250, 256, 500, 512, 1000, 1024, 2000, 2048, 5000, 999.9, 1023.4, 512.03 against targets 100, 125, 128, 200, 250, 256, 500, 512:
24 of 86 pairs return a wrong ratio. Every one of them has a non-integer source rate; every integer pair is exact. The worst cases:
| asked for | actually produced | labeled | error |
|---|---|---|---|
| 512.03 Hz -> 128 Hz | 117.04 Hz | 128 Hz | 8.57% |
| 1023.4 Hz -> 125 Hz | 116.96 Hz | 125 Hz | 6.43% |
| 1023.4 Hz -> 250 Hz | 238.79 Hz | 250 Hz | 4.48% |
| 1023.4 Hz -> 128 Hz | 122.81 Hz | 128 Hz | 4.06% |
| 999.9 Hz -> 256 Hz | 249.97 Hz | 256 Hz | 2.35% |
A non-integer srate is not exotic: hardware reports rates like 999.9 and 1023.4, and any srate that arrived as a float from an earlier division lands in the same place.
The fix is in the standard library, and it deletes a dependency
from fractions import Fraction
def _resample_ratio(freq, srate):
ratio = Fraction(float(freq) / float(srate)).limit_denominator(10**6)
return ratio.numerator, ratio.denominator
Fraction(...).limit_denominator() returns a rational by construction, so the truncation cannot happen. It reproduces nsimplify exactly on all thirteen well-behaved pairs I checked (250/1000 -> 1/4, 44100/48000 -> 147/160, 1000/1024 -> 125/128, and so on) and is exact on the ones nsimplify gets wrong: 999.9/2000 -> 9999/20000, 1023.4/2000 -> 5117/10000, both with zero error.
The denominator bound is the one judgment call. 10**6 keeps the polyphase filter orders sane while covering rates to four decimal places; _resample_segment is where a very large q would hurt, so it is worth choosing that bound deliberately rather than inheriting it from this issue.
It also removes sympy from the install, which pop_resample.py:9 imports for this one call and nothing else uses anywhere in src/. That is 4.1 MB and one package off every install, and in the browser it is 4.1 MB off a budget where the whole read path is 4.2 MB (#395).
Tests worth having
- The rate pairs above, asserting the returned ratio against
Fraction, so a future change of method cannot silently reintroduce truncation. - A round trip through
pop_resampleassertingoutput["pnts"]matchessrateand the data length, which is the invariant the current code breaks: the label and the data disagree, and nothing checks them against each other.
Reproduction script is three lines of sympy plus fractions; happy to open the pull request with the fix and the tests if that is useful.
- Dominant language
- Python
- Stars
- 35
- Forks
- 6
- Avg merge
- 3d 20h
- Merged PRs (30d)
- 66
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from sccn/eegprep
-
Difficulty 1/5 Under an hour Newbie friendliness 91/100
-
Difficulty 4/5 3-5 days Newbie friendliness 67/100
-
epic
-
documentation
-
question
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
bancolombia/sentinel#23 ·
-
test md OpenCI
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
bug client
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100