-
-
Notifications
You must be signed in to change notification settings - Fork 25
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
sqrt
implementation
#84
Conversation
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.
Hi @squ1dd13,
Thank you for the PR, it looks great!
Overall it looks awesome. I have left some opinionated feedback on the patch, please feel free to push back on any of the suggestions. Also, happy to keep discussing if you have more thoughts on any of subjects.
Looking forward for the polished version! :)
Thanks for all the feedback, I'm glad you're interested :D Since there are several points to address regarding I've made a bunch of minor tweaks (mainly internal) but I haven't done anything major to the API for now because I want to hear what you think about the above. TL;DR I'm all for getting rid of |
For fractions I think it's probably essential to force the user to recognise that they are only working with an approximation, hence the requirement for them to supply some sort of value specifying the accuracy they want the result to have. However, would it make sense for This would be somewhat at odds with the current documentation, which states
although the other part of this,
is actually exactly what the implementation of // Stop and yield the current guess if it's close enough to the true value.
if squared_and_truncated == truncated_target {
break SqrtApprox::Rational(current_approx);
} Edit: I should probably clarify that I've never been 100% sure on how to use |
@squ1dd13, I can't reply in threads to the posts in the general comment section, so I'll quote you in this reply to be more specific about things.
That makes a lot of sense. I agree that it sounds reasonable to pass a link to a pre-allocated instance and do not destroy it for future reuse. Also, I guess it could make sense to have a default lazy-static pre-allocated value for it as a fallback.
Perhaps, all possible optimisations have value regardless of their relative size. If they obscure API too much, then we may need to think about adding abstractions to simplify that (good thing Rust supports zero-cost abstractions really well :)
I feel like that could be just a ref to GenericInteger in there.
Decimal is implied to be a thin wrapper around Fraction, which contains extra info about how we want the fraction to be represented in a decimal format.
The comparison takes precision into account precision to follow the "string" way of comparing numbers (e.g. 1/3 == 1/3, but 0.333 != 0.3333 while both of those might be represented as the same fraction internally). Given that, sqrt itself is not producing a formatted representation of the decimal itself, but rather a calculated output, maybe |
Fair point. In this regard it absolutely makes sense to keep it in. I see the reasoning behind allowing any Consider the following example, where let my_number: DynaFraction<u32> = DynaFraction::from_str("123456/654321").unwrap();
// 0.1886780341758861476247896674567987272302
println!("{my_number:.40}");
let root_a = my_number.sqrt_with_accuracy(40);
// The user expects this to be equal to `my_number` to 40 d.p.
let root_a_squared = &root_a * &root_a;
// 0.1886780341758861476247896674567988508748
// That's only 33 d.p.!
println!("{root_a_squared:.40}");
// Here's what the user should have done:
let root_b = my_number.sqrt_with_accuracy(BigUint::from(10_u8).pow(40));
// They could also have done `my_number.sqrt(40)` with the current API.
let root_b_squared = &root_b * &root_b;
// 0.1886780341758861476247896674567987272302
// 40 d.p., as expected.
println!("{root_b_squared:.40}"); (In the above, Of course it would be possible to change the API such that *I don't like this strange behaviour – it would be much better if the accuracy referred to the actual square root value rather than the square of the square root. That is, It's probably worth mentioning that there are possible uses for non-power-of-10 let x: DynaFraction<u32> = DynaFraction::from_str("123456/654321").unwrap();
// 0.0011000001001101001101000010001001000010
print_in_binary_to_40(&x);
let s = x.sqrt_with_accuracy(&SqrtAccuracy {
multiplier: BigUint::from(2_u8).pow(40),
});
let sq = &s * &s;
// 0.0011000001001101001101000010001001000010
// Matches to >= 40 digits beyond the binary point.
print_in_binary_to_40(&sq); Although I'm sure most people will only care about decimal, the fact that there is some meaning to using other bases for I somehow managed to miss your previous mention of using At the moment I'm not sure about the idea of making Edit: I've made significant changes to the accuracy stuff; see the commit immediately after this message. Thanks for your clear explanation of the |
Hi @squ1dd13, Thank you for the detailed explanation. Yes, agree, having I also feel that might be important to capture the details from your above post in the documentation for the SqrtAccuracy structure and the sqrt function. |
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.
That is a great patch, thank you for submitting!
I would be keen to have safer handling for the negatives by default. At least the default API method shouldn't panic so we could handle unexpected data safely, and maybe then provide unsafe but faster method for those who know what they are doing.
Also, would be nice to cover all possible values in the tests:
- NaN
- -Inf
- -2
- 0
- 2
- +Inf
Is this not covered by the behaviour of the /// Returns a reference to the multiplier used by `self` to chop off irrelevant digits.
pub fn multiplier(&self) -> &BigUint {
#[cfg(feature = "lazy_static")]
{
lazy_static! {
static ref DP20_MUL: BigUint = BigUint::from(10_u8).pow(20_u32);
static ref DP100_MUL: BigUint = BigUint::from(10_u8).pow(100_u32);
static ref DP500_MUL: BigUint = BigUint::from(10_u8).pow(500_u32);
};
return match self {
Accuracy::Dp20 => &DP20_MUL,
Accuracy::Dp100 => &DP100_MUL,
Accuracy::Dp500 => &DP500_MUL,
Accuracy::Custom { multiplier } => multiplier,
};
}
// -snip-
let Accuracy::Custom { multiplier } = self else {
// -snip-
};
multiplier
} The current
Absolutely. I'll write up full documentation for each of the Footnotes
|
Yes, that looks good. I was writing that comment before reviewing the latest changes to the PR, sorry for the confusion.
Totally happy with that. There are no strong arguments for not having |
I keep forgetting this :P I'll leave the explicit requirement for I think the PR is nearly done. I'll write some more tests so that we have all the special cases covered, and then write up the documentation for all of the methods. Please do say if there's anything that the docs leave unclear – I'm used to the inner workings of the module now so there's a chance I'll miss out bits that I forget might not be obvious. Not directly related to this PR as such but still potentially important is that there are a couple of issues I noticed with (On looking a second time, it would appear that this is not an issue for fractions made using |
That's everything I can think of for now. I've made I plan to open at least one more PR with other approximations, so it would make sense for those to then be their own modules within |
This is true, the negative numbers within the Ratio data (either num or denum) would lead to destructive consequences in many scenarios. Partially, this is neglected because the GenericFraction implementation keeps the sign out as a separate attribute:
|
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.
Thank you, that looks great!
I only have one last concern about the panic
to discuss, but otherwise am really happy with everything.
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.
That looks great, thank you for the contribution @squ1dd13!
That PR looks awesome and I would love to merge it. However, there is a small issue left with "test_raw" taking too long to finish: https://github.com/dnsl48/fraction/actions/runs/6291510982/job/17081838784?pr=84#step:4:660
Looks like it takes over 250s to finish on GitHub (and over 300s on my localhost).
Perhaps we could reduce that time? Ideally the whole approx
module test suite would run within 30s if possible?
If we want to keep the heavy tests for more thorough coverage, perhaps we should move them into a separate feature (e.g. how it is done with src/tests/division/divide_to_string_u8.rs).
That running time had me confused for a minute :D I forgot to say at any point that the algorithm is only remotely "fast" when compiler optimisations are turned on, so I've been testing with This difference is absolutely not ideal, because it's likely to catch users out. RFC 2412 should help with this, because we would be able to force speed optimisation for specifically the In terms of the testing time, would we be able to test with optimisations enabled? All we'd need is this: [profile.test]
opt-level = 3 Alternatively, we could test using For now I'll add a note to the documentation and change the build config to optimise tests, but I'm open to reversing those changes and discussing other options. Footnotes
|
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.
Thank you @squ1dd13,
the PR looks great and the tests performance is okay now (happy to go with opt-level=3
for now).
It looks like we only need to fix benchmarks and it is good to merge
error[E0599]: no method named `sqrt_raw` found for enum `GenericFraction` in the current scope
--> benches/bench_fraction.rs:69:28
|
69 | b.iter(|| num2.sqrt_raw(n));
| ^^^^^^^^ method not found in `GenericFraction<u8>`
error[E0599]: no method named `sqrt_raw` found for enum `GenericFraction` in the current scope
--> benches/bench_fraction.rs:73:33
|
73 | b.iter(|| small_num.sqrt_raw(n));
| ^^^^^^^^ method not found in `GenericFraction<BigUint>`
error[E0599]: no method named `sqrt_raw` found for enum `GenericFraction` in the current scope
--> benches/bench_fraction.rs:77:31
|
77 | b.iter(|| big_num.sqrt_raw(n));
| ^^^^^^^^ method not found in `GenericFraction<BigUint>`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `fraction` due to 3 previous errors
warning: build failed, waiting for other jobs to finish...
Oops, forgot to check that. I've left the benches in but they'll now only run when |
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.
Thank you for the contribution!
Released with |
Great, thanks! |
I recently found myself needing
sqrt
while using this crate, and after reading #60 I decided to have a go at implementing it myself.The implementation is quite long because it's the result of quite a lot of benchmarking to try and get decent performance, so I've added a new
approx
module for it. The API isn't very polished yet but the actual algorithms work fairly well and are able to produce very accurate rational approximations for square roots in relatively little time. I thought I'd just get an extra opinion on it before I spend time perfecting the API, which is why I've only submitted a draft PR.Let me know if you think this is worth finishing off. Cheers :)