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

Various suggestions #19

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Various suggestions #19

wants to merge 11 commits into from

Conversation

lxvm
Copy link

@lxvm lxvm commented Jul 31, 2024

Hi,
This PR does the following:

  • Adds a testset
  • Adds a CI workflow
  • Adds docstrings
  • Adds a TaylorDiff.jl extension
  • Adds a rule cache based on the precision of the nodes
  • Tries to compute the rule if it is not tabulated
  • Removes allocation of a quadrature for domains with different endpoints

The main issue is the accuracy of the monomial rules, which according to my test are not all accurate to within 3 digits of the working precision. A "fix" is to tabulate only the rules that pass the test or otherwise to find a way to make the optimization succeed.

Accuracy test results
Test Summary:            | Pass  Fail  Total  Time
accuracy                 |  700   950   1650  4.2s
  monomial rules (7, 1)  |   28           28  0.1s
  monomial rules (9, 3)  |         72     72  0.7s
  monomial rules (9, 4)  |         90     90  0.3s
  monomial rules (7, 2)  |   42           42  0.0s
  monomial rules (2, 1)  |    8            8  0.0s
  monomial rules (10, 1) |   40           40  0.0s
  monomial rules (2, 2)  |   12           12  0.0s
  monomial rules (10, 2) |         60     60  0.1s
  monomial rules (8, 0)  |   16           16  0.0s
  monomial rules (1, 0)  |    2            2  0.0s
  monomial rules (9, 1)  |   36           36  0.0s
  monomial rules (3, 0)  |    6            6  0.0s
  monomial rules (8, 3)  |         64     64  0.2s
  monomial rules (1, 3)  |    8            8  0.0s
  monomial rules (8, 4)  |         80     80  0.3s
  monomial rules (1, 4)  |   10           10  0.0s
  monomial rules (9, 2)  |         54     54  0.1s
  monomial rules (3, 3)  |   24           24  0.0s
  monomial rules (3, 4)  |   30           30  0.0s
  monomial rules (5, 0)  |   10           10  0.0s
  monomial rules (8, 1)  |   32           32  0.0s
  monomial rules (6, 0)  |   12           12  0.0s
  monomial rules (1, 1)  |    4            4  0.0s
  monomial rules (5, 3)  |   40           40  0.0s
  monomial rules (5, 4)  |         50     50  0.1s
  monomial rules (8, 2)  |         48     48  0.1s
  monomial rules (1, 2)  |    6            6  0.0s
  monomial rules (3, 1)  |   12           12  0.0s
  monomial rules (6, 3)  |         48     48  0.2s
  monomial rules (6, 4)  |         60     60  0.2s
  monomial rules (3, 2)  |   18           18  0.0s
  monomial rules (4, 0)  |    8            8  0.0s
  monomial rules (5, 1)  |   20           20  0.0s
  monomial rules (4, 3)  |   32           32  0.0s
  monomial rules (4, 4)  |   40           40  0.0s
  monomial rules (5, 2)  |   30           30  0.0s
  monomial rules (6, 1)  |   24           24  0.0s
  monomial rules (7, 0)  |   14           14  0.0s
  monomial rules (6, 2)  |   36           36  0.0s
  monomial rules (2, 0)  |    4            4  0.0s
  monomial rules (10, 0) |   11     9     20  0.0s
  monomial rules (4, 1)  |   16           16  0.0s
  monomial rules (7, 3)  |         56     56  0.2s
  monomial rules (7, 4)  |         70     70  0.2s
  monomial rules (4, 2)  |   24           24  0.0s
  monomial rules (2, 3)  |   16           16  0.0s
  monomial rules (10, 3) |         80     80  0.2s
  monomial rules (2, 4)  |   20           20  0.0s
  monomial rules (10, 4) |        100    100  0.3s
  monomial rules (9, 0)  |    9     9     18  0.0s

@lxvm lxvm changed the title [WIP] Various suggestions Various suggestions Aug 1, 2024
Copy link
Owner

@SouthEndMusic SouthEndMusic left a comment

Choose a reason for hiding this comment

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

Thanks a lot for your contributions! I mainly have questions about how it works (:

end # module IntegralsGaussTuranExt
function GaussTuranQuadrature.GaussTuranComputeRule(::Type{T}, n::Int, s::Int; kws...) where {T}
rhs = inv.(T.(1:(2 * (s + 1) * n + 1))) # 1 ./ collect(range(one(T), T(2 * (s + 1) * n + 1)))
ϕ = let v = Val(2s + 1)
Copy link
Owner

Choose a reason for hiding this comment

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

Can you explain what is going on here? I understand what this code is supposed to do but not how it does it 😅 I'm not very familiar with the do and let keywords

Copy link
Author

Choose a reason for hiding this comment

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

The first line converts integers to the appropriate float type and inverts them to compute the analytical integrals. The second line uses a let block to hopefully improve type-stability within the closure, since Val(2s+1) is computed at runtime. The do syntax creates an anonymous function used to evaluate each element of the tuple and in this case also compute derivatives via an iteration. Here I am using a let block since _f gets reassigned in the closure, which can lead to performance issues because of the language. I should still check to see if this is type stable, which would be the main goal as far as performance goes

Copy link
Author

Choose a reason for hiding this comment

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

I may not be very consistent with my usage of let blocks. Usually I would add them if profiling reveals a type instability, but here I just added them pre-emptively

Copy link
Owner

@SouthEndMusic SouthEndMusic Aug 3, 2024

Choose a reason for hiding this comment

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

Thanks for the explanations, I have a few more questions:

  • Why do you need the *1 at the end of line 198?
  • If I understand correctly, _f is the value of the 'current derivative' and __f the value of the 'next derivative'. If so, can you add an explanation of that in the code?

order = f.order === nothing ? Val(n_derivs(I)) :
f.order isa Integer ? Val(f.order) :
f.order
__f = order isa Val{1} ? x -> (_f(x),) : x -> value(derivatives(_f, x, 1, order))
Copy link
Owner

Choose a reason for hiding this comment

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

Can you also explain this part?

Copy link
Author

Choose a reason for hiding this comment

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

Since TaylorDiff.jl expects the order to be a Val(n), the first line infers the order from the rule if the user doesn't provide an order and otherwise uses the user's order. The second line uses the TaylorDiff.jl API to construct the integrand with derivatives, although the no-derivative case requires special treatment.

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks. Some questions:

  • Is this nested/chained use of ternary operators common practice? Then it is just something for me to get used to. Otherwise it would be better to write it out with if else.
  • Would it be an idea to store f.order as a Val?

function GaussTuranRule(n::Integer, s::Integer; domain::Tuple{<:Number, <:Number} = (0, 1))
dom = promote(domain...)
Δx = dom[2] - dom[1]
T = typeof(float(real(one(Δx))))
Copy link
Owner

Choose a reason for hiding this comment

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

Why this convoluted way of determining the type?

Copy link
Author

Choose a reason for hiding this comment

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

TLDR; this is how QuadGK.jl does it.
The issue is that you want a real, unitless, floating-point type for optimization, but the domain could be specified with integer endpoints, or could be a complex segment, so this is the best way to get a type that works.

return evalrule(f, W, X, a, b)
end

function evalrule(f, W, X, a, b)
Copy link
Owner

Choose a reason for hiding this comment

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

Can you also explain this function?

Copy link
Author

Choose a reason for hiding this comment

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

Your previous version of the function used out = zero(eltype(X)), which is wrong if the integrand returns some generic type supporting addition and scalar multiplication. This version of the function correctly initializes out but has to unwrap the first iteration of the loop to do so.

@SouthEndMusic
Copy link
Owner

Regarding the accuracy of the tabulated rules: I hadn't looked into that yet, I just took a part of the parameter space that didn't take too long to compute, and the supplied optimization parameters are not very well informed. It would indeed be nice to only tabulate the rule if it passes an accuracy criterion (and maybe raise an error if the user tries to use such a rule which apparently is hard to derive).
I believe in general the higher precision floats are used the better the end result is, although in the article they claim:

Thus, in order to obtain results of high accuracy, the computations in software package Mathematica with 120 digits operations for generating the Gauss-Turán quadratures have been performed.

and their results aren't very good as you said. It's also unclear whether they mean 120 digits in binary or in decimal. The optimization algorithm can also be performed with BigFloat, but then it becomes very slow.

@lxvm
Copy link
Author

lxvm commented Aug 2, 2024

Thanks for your comments about the tabulated rules. I think it's totally fine to tabulate the rule only when the accuracy criterion is passed because these quadrature rules are rather niche and as long as you have enough rules for your needs then I think the package has already accomplished more than what some of the original literature presents.

As for the accuracy of the endpoints, I can see why doing the calculation at a higher precision and then reducing precision as necessary for subsequent calculations is desirable, however I don't think this should be done automatically for the user, except for the tabulated rules where we can actually guarantee full precision. (Also, digits usually mean decimal places, while bits correspond to binary.)

So, I think I will print a message whenever a quadrature rule is being computed and say that it is up to the user to verify its accuracy. This check could also be done a posteriori as a convenience for the user, but is most likely non-essential as long as we inform the user about which of the tabulated rules they can choose from. For reference, similar packages which obtain quadrature rules from nonlinear optimization will generally error if you request a rule that is not already computed (see https://github.com/JoshuaTetzner/GeneralizedGaussianQuadrature.jl/blob/main/src/quadrature/gqlog.jl) because this is by no means a solved problem.

@SouthEndMusic
Copy link
Owner

Thanks a lot for your answers to my questions, I learned a lot from it (:

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.

2 participants