-
Notifications
You must be signed in to change notification settings - Fork 8
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
Firwin #53
base: main
Are you sure you want to change the base?
Firwin #53
Conversation
c042c55
to
cab3736
Compare
To implement firwin, there is a need to get_windows on all window types defined in the Scipy signal.windows namespace, most of which are implemented in a _windows.py file. This commit keeps all the window variants that needs to be implemented as comments in an Enum for future implementation, along with some functions that are used across the implementation of all window types. The intended strategy to combat the variadic function type associated to the Scipy's get_windows function is to enclose all function arguments in a struct that represents the given function type, while ensuring all structs representing the Windows type implement a common trait.
060cf5b
to
21010f8
Compare
As noted in scipy, the ufuncs as provided by scipy is just a thin wrapper around a library known as Cephes. To avoid duplicate work, we temporarily resort to using this library. However, it distinguishes between f64 vs f32. This does not necessarily play well with the established types thus far, and may require a ground-up rewrite in the future. We however will currently just use this as is.
The error variants here should guide the user as to the wrong use of functions, instead of taking down the entire program with a panic, which is especially necessary in an embedded environment.
By using as a trait, we can target between f32, f64, Vec<F> and even Array<F, Dimensions>. A bug with the f32 implementation of i0 was found, and is ignored in the tests.
We also introduce the convenience enum to go along with the function.
There still is a need for double specification of window size across both firwin and the Window struct that uses it, unless we have a string to enum converter I suppose...; Either way, its suboptimal currently.
a92506d I have added error types after looking through how the Rust ecosystem generally does errors. This might still not be ideal and needs more work. We should leave panics instead to Python-facing side of the library. |
0e90be5
to
a1783ca
Compare
a1783ca Implementing as a trait for different types was the only way I could see to ensure that the Float Similar to scipy, we used Cephes, however, it was found that i0 of f32 as from the provided C library has an issue, so the corresponding test is ignored. A macro could probably be done to implement all the functions for Vec and Array<F, D> after having them defined in the trait and manually done for f32/f64 (due to function names being a tad scattered). |
4598cbd There's still some redundancy in argument specification of Firwin. But I'm not sure how to resolve it. Nonetheless it behaves to expectation now. |
} else if a > F::from(0).unwrap() { | ||
F::from(0).unwrap() | ||
} else { | ||
panic!("Expected a positive input.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Result
{ | ||
let a = ripple.abs(); | ||
if a < F::from(8).unwrap() { | ||
panic!("Requested maximum ripple attenuation is too small for the Kaiser formula."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Result
🎉 Awesome! This will take a little bit to review, but at first read through this looks like a great addition! You have a ton of great comments. DiscussionYour concerns over patterns are valid. I have avoided adding an idiomatic error type, but it is probably time to take a look at the bigger picture. Some of the idiomatic patterns are clashing with the Python interface such as Error vs throw and kwargs vs args Struct. Error w/ and w/o
|
All returnable
Parrallelising with this thought, I spent a bit of today mulling over providing if Nonetheless, we should still try lean into Rust's compiler gurantees by having slightly more terse functions that are sci-rs specific that may not necessarily mirror scipy very well. For example, the last thing we would want is to take a Vector of 2-tuples for args/kwargs mirroring, which can be a nightmare where the 2nd argument is a mix of scalars, arrays, and hashmaps; we then let the macro mirror the Python styled variadic function, and perhaps even a
Just to double check, should the function
We would then have to wait for stainless-steel/special#18. I generally think the biggest struggle with regards to this is that I am only aware of the
Would the following be considered as unwrap in a tight for loop?
After having been immersed into rust through having implemented this for a while, I realise that it probably in fact not the wisest to mirror the file structure of scipy either, especially if I were to mirror the ufunc expectation (as I have had for The only sticking point I have to say is that I would probably expect |
Older commits didn't utilise some properities afforded by Real and RealField. Also added a useless(?) cfg(attr = "alloc") compiler attribute for consistency.
What are your thoughts on https://www.netlib.org/slatec/src/besi.f from https://doi.org/10.1145/355719.355726? It seems to be public domain, but it certainly demands a Fortran to Rust translation. Edit: |
8c167ed
to
fcf7e80
Compare
As an aside on the special math functions, I have a special-rs repository where I have functions for:
A lot of them are translations either from cephos (the scipy backend) or boost (from C++). What has prevented me from making more PR's here is that I was unsure how to deal with the function arguments quite honestly. Since, for example the One can take inspiration from BLAS function signatures by using assert_eq!(4.0_f32.gamma(), 6.0); // Gamma(4) = 3!
assert!((0.0_f64).gamma().is_nan()); // Gamma(0) is undefined it is then trivial to make a generic function pub fn gamma<T>(z: T) -> T
where
z: Gamma
{
z.gamma()
} The possible downside of this approach is that |
Also, as an aside on all of the pub(crate) trait RealGammaConsts: Sized {
const MIN_TO_USE_STIRLING: Self;
const P: [Self; 7];
const Q: [Self; 8];
const INTERVAL: [Self; 2];
const MIN_USE_SMALL: Self;
}
macro_rules! impl_realgammaconsts {
($($T: ty)*) => ($(
impl RealGammaConsts for $T {
const MIN_TO_USE_STIRLING: Self = 33.0;
const P: [Self; 7] = [
1.60119522476751861407E-4,
1.19135147006586384913E-3,
1.04213797561761569935E-2,
4.76367800457137231464E-2,
2.07448227648435975150E-1,
4.94214826801497100753E-1,
9.99999999999999996796E-1,
];
const Q: [Self; 8] = [
-2.31581873324120129819E-5,
5.39605580493303397842E-4,
-4.45641913851797240494E-3,
1.18139785222060435552E-2,
3.58236398605498653373E-2,
-2.34591795718243348568E-1,
7.14304917030273074085E-2,
1.00000000000000000320E0,
];
const INTERVAL: [Self; 2] = [2.0, 3.0];
const MIN_USE_SMALL: Self = 1.0E-7;
}
)*)
}
impl_realgammaconsts! {f32 f64} Honestly, I do not know what the performance (or the maintenance) implications are, but is a possible solution where Again, this limits the ability to generically implement functions over any field type (and in particular, over types like |
Lots of great discussion in this thread! It has been very thought provoking, so you can jump to the conclusion for a TLDR. Thoughts on DirectionI have some overarching thoughts and would appreciate your feedback.
Responses
I agree with you. I have been thinking about reorganizing imports or establishing a prelude pattern.
I'm all for translating or vendoring in some dependencies that are pretty relevant. I think we have experienced friction because of all the great design points under consideration here.
My commitment to _st and _dyn has been waning of late. It is a function coloring problem that is guarded by the compiler. Many new features I have needed lately involve explicit interface generics or allocation. As our feature set has been growing, even keeping a commitment to _st vs _dyn is waning as the cfg feature alloc attributes are a wart that just keeps spreading. I would appreciate thoughts from others. I don't plan on getting rid of feature alloc, but I don't ever disable it anymore.
It could be a tight loop. In my usages, filter design is infrequent, but it could be frequent for some people. I would want to move the unwrap of 2 outside of the loop.
I like this approach. consts in trait impls are a good use of Rust language features here. ReflectionsI recognize that some of the loose implementation thus far is creating some confusion. I feel this is the result of chasing features and performance whilst punting hard interface decisions. In a way, I am happy with this as we do have features and some critical interface design has fallen through as Rust support for const generics fizzled out in ways that were necessary for the original design. I don't want this library to turn into a macro library (like a C all header library or C++ template library). Beyond the increase in compile times, I find those difficult to navigate, test, and contribute to ... However, macros for trait and function impls seem to be the best solution for both interface, constants, and conversion issues. Using goto definition and ending up at a (nested or proc) macro call site stinks, but we will have a lot of repeated code otherwise.
Another recent interface exampleRecently, I needed the performance of filtering to be better. #[inline(always)]
pub fn sosfilt_fast32<const N: usize, const M: usize>(y: &[f32; N], sos: &mut [Sos<f32>; M], z: &mut [f32; N]) {
_sosfilt32(y, sos, z);
} This took an interface that was slightly dynamic and made it static. We could keep adding traits, but when should we stop adding traits and just copy paste (macro) an implementation? This interface change led to significant improvements but would live in the codebase as a static interface parallel to the dynamic slice length interface. This is reduces maintainability and compile time but unlocks optimal performance. If we go looking at prior arts in other languages, many interfaces are happy to go terse+macro/codegen/template to get exactly the right version of the code. To achieve the desired behavior for each scipy feature, Rust's language features push us either towards:
Possible Interface DirectionAre we heading towards pythonic callsites via macro-style interface? let a = gamma!(x);
let a = sosfilt!(f32, N, y, z, M, sos);
I've had my embedded C hat on for a little bit, and there is a grace to the simplicity of some consts or macros. It can be quite easy for a user to navigate calling dgemm or sgemm. I like it for its usage simplicity but it would require some (proc)macros to get right. Compile times and maintainability may get out of hand. LLM agents may make this a tenable route.
I'm not a huge fan of macros to blanket trait implementations, but it is very common in rust libs. As you have in your example, there are problems including conflicts, compile time, and the orphan rule. nalgebra in particular suffers some serious compile time cost. There have been some serious issues with trait changes propagating through dependencies like our dependency on RealField. I perceive the most efficient compile time and assembly to come from writing out the interface options. Using fewer Rust language features drastically improves the compile time as usage of traits and macros generates a lot of extra work for the compiler. This is the opposite of the pythonic interface direction. Another Possible Interface DirectionAre we heading towards Rusty interface diverging from scipy? Here, each scipy function becomes a trait and one or more impls. struct SosfiltTiled<F: SciReal, N: const usize, M: const usize> { ... }
impl<F: SciReal, N: const usize, M: const usize> Sosfilt for SosfiltTiled <F, N, M> {
// do the specialized implementation
}
struct SosfiltIterator<F: SciReal > { ... }
impl<F: SciReal> Sosfilt for SosfiltIterator<F> {
// do the specialized implementation
} let filter = SosfiltTiled { ... }
let a = filter.sosfilt(); This leans into Rust design, diverging from prior arts with C by relying on one name and diverging from python by removing the monadic control flow and naming. Each action requires capturing context, then using a It is a very flexible approach, but it will break nearly every interface we have. Heading in this direction means re-writing Crate structureI think we are eventually going to be a workspace structured similarly to polars. (especially if we are going towards a Rusty interface)
ConclusionI think we should start heading towards the Rusty interface that diverges from the pythonic style. We can break up scipy functions into a struct and a trait. This allows defining constants and specialized implementations that unroll/tile/vectorize/etc. I think we need to establish our own floating point value trait like TLDRMy disposition is to take the following steps:
Feedback is welcome! Thanks again! |
Thoughts on Direction
I agree. Having a function which does the calculation and is tested, but maybe not having the best interface, is better than no function at all.
That's fine. I think its good to have a scope of what numeric types are supported by default. Another recent interface example
This just seems wrong. While I am not an expert in Rust code, from a C++ perspective, this seems like it would create unnecessarily long compile times if Another Possible Interface Direction
I would avoid the macro-style interface to have variadic function parameters. I believe your conclusion on a Rusty interface makes the most sense.
I think this makes the most sense in the Rust ecosystem. If I think about what a Rust end-user, one would like to make a function that generically accepts something that could call My main issue is the necessity of having a struct associating each 'input' type to a function. But maybe this makes sense from the perspective of the impl Gamma for f32 {
fn gamma(self) -> Self {
// some implementation
}
}
impl Gamma for f64 {
fn gamma(self) -> Self {
// basically same implementation as f32?
}
} Though, I guess if there existed a fn r_gamma<F: RealField>(n: F) -> F {
// Gamma implementation for real-valued
}
impl Gamma for f32 {
fn gamma(self) -> Self {
r_gamma(self)
}
}
impl Gamma for f64 {
fn gamma(self) -> Self {
r_gamma(self)
}
} But then this is very similar to what I was already doing, except I was just using a macro to write the impl Gamma for f32 {
fn gamma(self) -> Self {
r_gamma(self)
}
}
impl Gamma for f64 {
fn gamma(self) -> Self {
r_gamma(self)
}
} part out. It is on this last point that I might be confused. ConclusionOverall, I think I generally agree with your viewpoints. I am not too stuck in my opinions and am happy to help where possible. I will defer to you and others about the general structure on the crate(s) for this project. Also, should perhaps this discussion be moved then to an issue? Rather than on this PR? |
Hi, please feel free to open as an issue if there is a need for more discussion. Clearly we need a rework on the relevant interfaces. Otherwise, I personally don't really have much more to add with regards to interfaces as mentioned by both. Personally, no objection to the proposed direction forward. The code will still be in this branch and can easily be reworked to satisfy the relevant new interfaces if we aren't merging; or we can just take it in as is and fail-forward as Jacob has proposed. The only concern I have thus far is: Is having BSD code in the repo for some span suitable; even if not in releases? I'm not familiar with licensing (and hence why I just jumped the gun and imported Cephes to begin with, creating this debacle, my apologies.) Interfaces aside, I found a book called |
This PR aims to implement Firwin.
Blockers before merging:
Original Message:
Currently opening as a draft PR to have eyes on the code before it grows even larger into eventually implementing firwin.
Questions I guess I need answers about specific to this repository to ensure the PR is decent:
*_st
or*_dyn
?mod.rs
to follow the scipy's submodule convention.... Individual files otherwise don't really make sense, and finding a function is best done either via grepping or through the[Source]
button as created bycargo docs
.Convolution
,B-splines
,Fitting
, ...? I think this is achieveable with//!
? Or I might be mistaken.unsafe { F::from(-1).unwrap_unchecked) }
or something similar. Is this the norm for "constants" which we can guarantee to be safe, and so I too should be doing this?I will continue to force-push onto this branch with any suggestions such that each commit is consistent with its commit title and message.