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

OPs for a=0, b=0, c=-1 case and generic lowering methods #18

Merged
merged 63 commits into from
May 26, 2021
Merged

Conversation

TSGut
Copy link
Member

@TSGut TSGut commented Feb 15, 2021

The plan is to generate a=0, b=0, c=-1 case directly from Legendre(). This can later also be expanded to any negative integer c but that will be more complicated.

@TSGut
Copy link
Member Author

TSGut commented Feb 15, 2021

B*[P_0(x),P_1(x),_P_2(x),...] with bidiagonal B gives the polynomials orthogonal wrt 1/(t-x). For now I'm setting it up so that it will be relatively easy to implement B in a cached way, though as we discussed I'm not sure if that's exactly what we want, so I definitely welcome any implementation discussion, @dlfivefifty.

For now, here's how we can use this to compute finite dimensional B, with easy extension to infinite cached arrays:

julia> N = 20
20

julia> t = 1.1
1.1

julia> α = zeros(N)'
1×20 LinearAlgebra.Adjoint{Float64,Array{Float64,1}}:
 0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0  0.0

julia> α[1] = initialα(t)
0.4430825224938979

julia> αcoefficients!(α,t,2:N)

julia> α
1×20 LinearAlgebra.Adjoint{Float64,Array{Float64,1}}:
 0.443083  0.521542  0.555073  0.573826  0.585848    0.622648  0.623722  0.624682  0.625545  0.626325

julia> B = BandedMatrices._BandedMatrix(Vcat((-1).^(1:N)' .* α,(-1).^(0:N-1)'), N, 0,1)
20×20 BandedMatrix{Float64} with bandwidths (0, 1) with data vcat(1×20 Array{Float64,2}, 1×20 Array{Int64,2}):
 1.0   0.521542                                               
     -1.0       -0.555073                                      
                1.0        0.573826                            
                         -1.0                                 
                                                             
                                                            
                                                             
                                                             
                                                             
                                                             
                                                            
                                                             
                                                             
                                                             
                                                             
                                                            
                                       0.624682               
                                      -1.0       -0.625545     
                                                 1.0        0.626325
                                                          -1.0

edit: swapped order of bands to be correct

@codecov
Copy link

codecov bot commented Feb 15, 2021

Codecov Report

Merging #18 (0bc8501) into master (b574511) will increase coverage by 2.56%.
The diff coverage is 95.90%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #18      +/-   ##
==========================================
+ Coverage   90.15%   92.71%   +2.56%     
==========================================
  Files           3        4       +1     
  Lines         264      508     +244     
==========================================
+ Hits          238      471     +233     
- Misses         26       37      +11     
Impacted Files Coverage Δ
src/SemiclassicalOrthogonalPolynomials.jl 88.64% <90.69%> (+0.38%) ⬆️
src/lowering.jl 97.17% <97.17%> (ø)
src/derivatives.jl 91.22% <0.00%> (-0.08%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update b574511...0bc8501. Read the comment docs.

@MikaelSlevinsky
Copy link
Member

@TSGut
Copy link
Member Author

TSGut commented Feb 15, 2021

The idea is based on Price https://epubs.siam.org/doi/abs/10.1137/0716073, which technically works for more general cases. But as I'm only doing 1/(t-x) right now, I think it is probably equivalent to what Uvarov is doing?

@MikaelSlevinsky
Copy link
Member

What happens when t is large? Is there any instability with the second kind Legendre functions by forward recurrence used to define the connection coefficients?

@dlfivefifty
Copy link
Member

Cool! Busy revamping Normalized. I realised something I should have realised 10 years ago: the normalised OPs have jacobi matrices with entries sqrt(b_k*c_k), so I think I'm going to make SemiclassicalJacobi always normalised.

....in the process I decided I wanted to be able to Tridiagonal, SymTridiagonal, and Bidiagonal with different vector types, which turned into a big rabbit hole... but hopefully this will be done this week, and all the weird _BandedMatrix(...) can be removed.

@TSGut
Copy link
Member Author

TSGut commented Feb 16, 2021

....in the process I decided I wanted to be able to Tridiagonal, SymTridiagonal, and Bidiagonal with different vector types, which turned into a big rabbit hole... but hopefully this will be done this week, and all the weird _BandedMatrix(...) can be removed.

That sounds great, looking forward to it.

@TSGut
Copy link
Member Author

TSGut commented Feb 16, 2021

What happens when t is large? Is there any instability with the second kind Legendre functions by forward recurrence used to define the connection coefficients?

In principle of course one of those terms in the initial condition goes to 1/0 as t approaches infinity but this happens slowly and the good news is that this seems to be nothing BigFloat initial conditions can't overcome. We're lucky in the sense that the coefficients actually change at a snail's pace with n, so that for example having the coefficients at n=1000 you practically won't have an issue also reaching n=4000 or n=10000 or higher even when t is in the order of thousands or millions. In fact, I have no proof of this yet but I am fairly certain that the alpha coefficients just converge to 2*t as n approaches infinity.

I also just pushed an update that simplifies the initial condition, it was needlessly complicated.

@MikaelSlevinsky
Copy link
Member

No I think the failure is catastrophic (through no fault of your own!), and it's even bad for t not-so-far off the support of the orthogonality measure.

julia> # inital value n=0 for α_{n,n-1}(t) coefficients
       initialα(t) = t-2/(log1p(t)-log(t-1))
initialα (generic function with 1 method)

julia> # takes a previously computed vector of α_{n,n-1}(t) that has been increased in size and fills in the missing data guided by indices in inds
       function αcoefficients!(α,t,inds)
           @inbounds for n in inds
               α[n] = (t*(2*n-1)-(n-1)/α[n-1])/n
           end
       end
αcoefficients! (generic function with 1 method)

julia> function αcoefficients(t, n)
           α = zeros(eltype(float(t)), n)
           α[1] = initialα(t)
           αcoefficients!(α, t, 2:n)
           α
       end
αcoefficients (generic function with 1 method)

julia> using PyPlot

julia> n = 1000
1000

julia> t = 2.0 # Far but not really
2.0

julia> setprecision(256) # Default BigFloat precision
256

julia> semilogy(1:n, abs.(Float64.(αcoefficients(BigFloat(t), n)) - αcoefficients(t, n)), ".r")
1-element Array{PyCall.PyObject,1}:
 PyObject <matplotlib.lines.Line2D object at 0x7ff6d0a9fb50>

julia> setprecision(65536) # Increase precision just in case
65536

julia> semilogy(1:n, abs.(Float64.(αcoefficients(BigFloat(t), n)) - αcoefficients(t, n)), ".k")
1-element Array{PyCall.PyObject,1}:
 PyObject <matplotlib.lines.Line2D object at 0x7ff6d0aa4880>

julia> xlabel("\$n\$")
PyObject Text(0.5, 47.04444444444444, '$n$')

julia> ylabel("Absolute error")
PyObject Text(51.84335222032334, 0.5, 'Absolute error')

julia> grid()

recurrence_error

I think the error is so large (and grows so rapidly) that standard BigFloat precision does not suffice because it eventually converges to the same wrong result as the 64-bit floating-point.

P.S. it just so happens that I'm deeply interested in this at the moment.

@TSGut
Copy link
Member Author

TSGut commented Feb 16, 2021

Fascinating! Consistent convergence to the wrong result is one of those things one reads about but I can't say I've encountered them directly in the wild yet before now. Let me just go through and check all of this on my end as well.

@marcusdavidwebb
Copy link
Member

In Gautschi's paper "Repeated modifications of orthogonal polynomials by linear divisors", he states that for large t, a continued fraction can be used for computing the modifications to the recurrence coefficients. I haven't looked into it at all, just suggesting that this might be relevant and could save you time. Cheers.

@TSGut
Copy link
Member Author

TSGut commented Feb 16, 2021

@marcusdavidwebb Yes, I assume the continued fraction form for the n-th term results by just plugging in the previous term's recurrence, at least that certainly does give a continued fraction here. I'll have to have another look at Gautschi's paper to confirm, though, thanks!

@MikaelSlevinsky I think I see where the error is coming from too: The recurrence terms basically all look like alpha_n = (2 - 1/n)*t + f(alpha_n-1), so when f(alpha) degenerates what remains is just (2 - 1/n)*t, which approaches 2*t, producing the error we're seeing for Float64 and standard precision BigFloat - i.e. both consistently converge to the same wrong result. Annoying but I do have to say - also quite cool. Even though it's still quite fast, I'm not a fan of just increasing the precision higher and higher, so I'll try to see if there is another way around that.

@dlfivefifty
Copy link
Member

@snowball13 and I just used higher-and-higher BigFloat precision for disk slices and spherical caps, which was needed for large parameters, in our case c = m for m large. So I think its OK for now as we may need to do it anyways.

@TSGut
Copy link
Member Author

TSGut commented Feb 16, 2021

Okay well if that's alright then yes that works as is and it's definitely still fast-ish, even quite high orders in tens of thousands are taking me just a few seconds on first computation with some ridiculous setprecision values. If we want to be cheeky we could even check if the values are getting closer to the wrong result 2*t and increase precision and recompute if it does. 😜

I still want to check a few things to see if I can get it more stable, though.

@TSGut
Copy link
Member Author

TSGut commented Feb 17, 2021

Since it was also going to be useful for more serious tests, I've now made a direct computation of the coefficients available too.

Comparing the two shows that we're buying stability for computing cost

julia> # goal parameters

julia> t = BigFloat("2.");

julia> n = 3000;

julia> # recurrence version

julia> setprecision(65536);

julia> αr = zeros(eltype(float(t)), n);

julia> αr[1] = initialα(t);

julia> @time αcoefficients!(αr, t, BigInt.(2:n));
  0.782400 seconds (83.98 k allocations: 191.259 MiB, 2.65% gc time)

julia> # explicit version

julia> setprecision(256);

julia> αd = zeros(eltype(float(t)), n);

julia> @time αdirect!(αd,t,BigInt.(1:n));
  4.463976 seconds (99.61 M allocations: 5.183 GiB, 14.96% gc time)

julia> errs = abs.(αr-αd);

julia> maximum(errs)
6.099089145333413279831345120482707841585231842798410926507117790845627952530002e-76

julia> scatter(errs,yscale=:log10,legend=false,markersize=2)

errors

And of course we can check that at standard BigFloat precision the recurrence shows the wrong convergence to 2*t we talked about while the explicit version doesn't

julia> setprecision(256);

julia> αr = zeros(eltype(float(t)), n);

julia> αr[1] = initialα(t);

julia> αcoefficients!(αr, t, BigInt.(2:n));

julia> αr[end]
3.731428791077769921599369234517361314155892288682987165316973210714883071500137

julia> αd[end]
0.267904542249388512670253160328905937104041057150894248970197013940127741415331

And actually depending on how precise we want the coefficients to be, we can save a lot of computing time by using less precision in the direct version while still getting the correct results

julia> # lower prec direct

julia> setprecision(65536);

julia> αr = zeros(eltype(float(t)), n);

julia> αr[1] = initialα(t);

julia> αcoefficients!(αr, t, BigInt.(2:n));

julia> setprecision(64);

julia> αd = zeros(eltype(float(t)), n);

julia> @time αdirect!(αd,t,BigInt.(1:n));
  1.875301 seconds (56.64 M allocations: 2.102 GiB, 19.43% gc time)

julia> errs = abs.(αr-αd);

julia> maximum(errs)
3.32807740918091828711e-18

@MikaelSlevinsky
Copy link
Member

Use the hypergeometric functions as a final condition and recurse backwards?

@TSGut
Copy link
Member Author

TSGut commented Feb 17, 2021

Don't spoil what I'm working on. 😆 I just need to clean it up a bit, add some tests etc.

julia> using SpecialFunctions, HypergeometricFunctions

julia> setprecision(256)
256

julia> t = BigFloat("2.");

julia> n = 10000;

julia> α = zeros(eltype(float(t)), n);

julia> @time backαcoeff!(α,t,BigInt.(2:n))
  0.043297 seconds (334.61 k allocations: 14.433 MiB, 47.63% gc time)

julia> abs(α[1]-initialα(t))
6.47712641632083346903976389710029967833700027332721103876777760262644385237182e-78

julia> t = BigFloat("100.");

julia> n = 10000;

julia> α = zeros(eltype(float(t)), n);

julia> @time backαcoeff!(α,t,BigInt.(2:n))
  0.010631 seconds (223.57 k allocations: 8.502 MiB)

julia> abs(α[1]-initialα(t))
1.740751338909616831176459088151580383395646093770184472125752404895033027568138e-74

@TSGut TSGut changed the title WIP: OPs for a=0, b=0, c=-1 case. OPs for a=0, b=0, c=-1 case via lowering c Mar 30, 2021
@TSGut
Copy link
Member Author

TSGut commented Mar 30, 2021

Yes, I think it should be fine to merge it as is and add those things after. I've changed the name of the PR, you can merge it at your convenience.

@TSGut
Copy link
Member Author

TSGut commented Mar 30, 2021

Oh, and did I misunderstand, I thought you already had a way to lower a and b?

If this isn't the case, I think the approach I used to lower c can be modified to lower a or b as well if we need it. It will look very similar to this c lowering stuff, obviously with the roles of the parameters exchanged in certain places.

@dlfivefifty
Copy link
Member

No if I knew how to do a and b then c would follow.. I don’t have any of them

@TSGut
Copy link
Member Author

TSGut commented Mar 30, 2021

Okay, I'll do a and b as well then, in a separate PR.

src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
src/loweringc.jl Outdated Show resolved Hide resolved
@TSGut
Copy link
Member Author

TSGut commented Mar 31, 2021

Since it went smoother than expected and is close to done now, I guess I'll just add the a and b lowering in here too.

As it stands the lowering approach seems to have a speed bottleneck in the computation of entries of the higher Jacobi matrix which from what I can tell is computed via Lanczos. I'll see what I can do about that but the basic functionality is there.

This also fixes most of the things you mentioned above, e.g. Camel case typing and some other redundancies. I'll go over it one more time to make sure, though.

@TSGut TSGut changed the title OPs for a=0, b=0, c=-1 case via lowering c OPs for a=0, b=0, c=-1 case and generic lowering methods Apr 10, 2021
@TSGut
Copy link
Member Author

TSGut commented Apr 10, 2021

This does all of the lowering things we want now, including iterative lowering, mixed lowering and raising and the special case (0,0,-1) is treated separately with very fast explicit expressions. Comparing with results obtained via raising instead of Lanczos there also doesn't seem to be an issue with stability.

So I think this is getting ready to merge @dlfivefifty, maybe you can have another look.

TSGut added 3 commits April 10, 2021 19:41
There seems to be a bug(?) in both 2F1 and 2F1general2 that is causing loss of full Float64 accuracy for certain high inputs unless the inputs are of BigFloat type. Presumably it's happening because of some intermediate allocations in HypergeometricFunctions.jl. Either way, this provides a workaround for that so that this method remains accurate to machine precision in high ranges of a, b and c.
@dlfivefifty dlfivefifty merged commit 987dcbe into JuliaApproximation:master May 26, 2021
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.

4 participants