Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix output length of resample #596

Merged
merged 6 commits into from
Dec 2, 2024
Merged

Fix output length of resample #596

merged 6 commits into from
Dec 2, 2024

Conversation

martinholters
Copy link
Member

This comprises a few changes that can be considered in on their own, but later ones depend on earlier ones for proper results. In commit order:

  • outputlength(::FIRRational, inputlength) is fixed to correctly predict the number of valid samples produced by filt!. (For context: filt! expects a sufficiently large output buffer and returns the number of samples actually written.)
  • outputlength(::FIRArbitrary, inputlength) is improved to do this correctly more often. Unfortunately, it can still be off-by-one sometimes, which seems almost inevitable. The reason is -- somewhat simplified -- that filt! increases the time incrementally, which may be numerically different from considering the whole time-span at once. More complicated in reality, but conceptually: Starting at sample 1 and going in steps of 0.1, how many steps do you need to reach sample 2? Ten, because 10 * 0.1 == 1? No, one more, because 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 < 1. Now, to be sure to have outputlength give the correct result, I think one would have to replicate the same iterative index/phase computations, which seems excessive.
  • Add rounding mode support to inputlength, specifically for RoundUp and RoundDown (where the latter is the old behavior and still the default). With RoundDown, inputlength returns the largest input length such that the actual output length will be at most the given one, i.e. outputlength(H, inputlength(H, yL)) <= yL < outputlength(H, inputlength(H, yL)+1). OTOH, with RoundUp, inputlength returns the shortest input length such that the actual output length will be at least the given one, i.e. outputlength(H, inputlength(H, yL, RoundUp)-1) < yL <= outputlength(H, inputlength(H, yL, RoundUp)).
  • In resample, use inputlength in RoundUp mode to pad the input to the minimum length to get at least ceil(Int, length(x)*rate) output samples, then trim the output to the required size as necessary.

Some points I'd especially appreciate feedback on:

  • Are the current tests sufficient or should I add more? If so, which?
  • Are we confident with the @asserts or should that be more defensive?
  • Is the RoundingMode API for inputlength appropriate? Only RoundUp and RoundDown really make sense there, I guess, so maybe not? (Or should we just drop the RoundDown behavior which might not be needed at all?)

Fixes #186.

test/resample.jl Outdated Show resolved Hide resolved
Comment on lines +103 to +106
@test length(resample(sin.(1:1:35546), 1/55.55)) == 640
@test length(resample(randn(1822), 0.9802414928649835)) == 1786
@test length(resample(1:16_367_000*2, 10_000_000/16_367_000)) == 20_000_000
@test resample(zeros(1000), 0.012) == zeros(12)
Copy link
Member Author

Choose a reason for hiding this comment

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

These lengths now all match expectations:

julia> 35546 / 55.55
639.8919891989199

julia> 1822 * 0.9802414928649835
1786.0

julia> 16_367_000*2 * 10_000_000/16_367_000
2.0e7

julia> 1000 * 0.012
12.0

Copy link

codecov bot commented Nov 25, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 97.90%. Comparing base (ed6c5f6) to head (27898c7).
Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master     #596   +/-   ##
=======================================
  Coverage   97.90%   97.90%           
=======================================
  Files          19       19           
  Lines        3248     3252    +4     
=======================================
+ Hits         3180     3184    +4     
  Misses         68       68           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

test/filt_stream.jl Outdated Show resolved Hide resolved
@wheeheee
Copy link
Member

After some simplification, I think there is a way to make sure that the output length matches. Is there an obstruction to generating ϕAccumulator with a pre-calculated range? Don't have time to try it out now, however.

@martinholters
Copy link
Member Author

martinholters commented Nov 27, 2024

After some simplification, I think there is a way to make sure that the output length matches.

For FIRArbitrary you mean, I suppose? That would be great. I'm curious to see it!

Is there an obstruction to generating ϕAccumulator with a pre-calculated range?

Not sure what you're trying to say here. I guess we could consider changing the state from ϕAccumulator to a counter and then calculate ϕAccumulator from something like mod(counter*Δ, Nϕ). Then it would be much easier to determine the final value for that counter, I think. EDIT: But of course, in the long run, with large values of the counter, the accuracy of counter*Δ may become arbitrarily bad.

I tried to avoid putting too many changes in this PR for sake of reviewability, and rewriting the inner workings of FIRArbitrary is somewhat on the border. It's not immediately required for fixing #186, but surely closely related, so might make sense.

@wheeheee
Copy link
Member

wheeheee commented Nov 28, 2024

Yeah, that was what I meant. Didn't consider the numerical error...if only there was a better way to calculate mod(init_acc + counter*Δ, Nϕ).
About the points you raised, I actually think the current implementation makes sense, but for more documentation.
EDIT: oh, it's still WIP, never mind.

@martinholters
Copy link
Member Author

One more thing I'm not 100% sure about: Currently, the target output length is ceil(length(input), ratio). Why ceil? Why not round?

src/Filters/stream_filt.jl Outdated Show resolved Hide resolved
src/Filters/stream_filt.jl Outdated Show resolved Hide resolved
This may still give results that are inconsistent with how many samples `filt!`
produces as the latter accumulates time-deltas (modulo Nϕ) which may be
numerically different. However, `outputlength(H, length(x)) == filt!(y, H, x)`
should hold more often this way.

Also adjust `inputlength` accordingly.
@wheeheee
Copy link
Member

wheeheee commented Nov 29, 2024

One more thing I'm not 100% sure about: Currently, the target output length is ceil(length(input), ratio). Why ceil? Why not round?

I suppose you mean ceil(Int, length(input) * ratio)? The only reason I can think of is if length(input) * ratio < 0.5, this still gives an output, and somebody wants that.

@martinholters
Copy link
Member Author

I suppose you mean ceil(Int, length(input) * ratio)?

Of course, yes.

The only reason I can think of is if length(input) * ratio < 0.5, this still gives an output, and somebody wants that.

Hm... That's not a reason that convinces me. A single sample does not seem that much more helpful than none. Would round be more intuitive? (It is to me.) Change?

@wheeheee
Copy link
Member

Perhaps we should take reference to what scipy or Matlab does?

@martinholters
Copy link
Member Author

Good point. Looks like they both round up (for Matlab I found that to be documented as well), so reasonable for us to do the same.

I guess then this is good to go then.

@martinholters martinholters changed the title WIP/RFC: Fix output length of resample Fix output length of resample Nov 29, 2024
@wheeheee
Copy link
Member

Merge?

@wheeheee
Copy link
Member

Hm, but actually the assertion is not always true...

julia> resample(ones(100), 2.00)
ERROR: AssertionError: length(y) >= outLen

@martinholters
Copy link
Member Author

Ah yes, turns out inputlength was incorrect for cases where no rounding in the length computation occurred. Fix incoming.

Should we reconsider this question: Are we confident with the @asserts or should that be more defensive?

@wheeheee
Copy link
Member

wheeheee commented Dec 2, 2024

Hm, I think the asserts should be correct. Not sure what you mean by more defensive, do you propose to remove them?

@martinholters
Copy link
Member Author

Not sure what you mean by more defensive, do you propose to remove them?

That particular one, yes. It arguably would have been better to get a result that is one sample shorter than expected than to see an assertion error. However, that might also have meant that this error in inputlength might have hidden for much longer. So I tend to leaving these @asserts in and dealing with bugs thus uncovered as they come in.

@wheeheee
Copy link
Member

wheeheee commented Dec 2, 2024

I agree with that approach. Though users may still want the output that's already calculated...maybe there is a good way to do this kind of exception handling I'm unaware of? Or return length(y) >= outLen etc. together, which is also unwieldy...

@martinholters
Copy link
Member Author

Then let's leave the @asserts in. We can always reconsider if these actually do fire in practice.

Do you want to take this for another test flight and see if you find more problems or should we merge?

@wheeheee
Copy link
Member

wheeheee commented Dec 2, 2024

Ran it with random inputs, was ok. LGTM, go ahead.

@martinholters martinholters merged commit 1edcf64 into master Dec 2, 2024
11 checks passed
@martinholters martinholters deleted the mh/fix-resample branch December 2, 2024 08:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Resample doesn't resample at given rate
2 participants