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

Add derivatives for besselix, besseljx, and besselyx #350

Merged
merged 6 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "SpecialFunctions"
uuid = "276daf66-3868-5448-9aa4-cd146d93841b"
version = "1.7.0"
version = "1.8.0"

[deps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
Expand Down
105 changes: 105 additions & 0 deletions src/chainrules.jl
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,108 @@ ChainRulesCore.@scalar_rule(
ChainRulesCore.@scalar_rule(expinti(x), exp(x) / x)
ChainRulesCore.@scalar_rule(sinint(x), sinc(invπ * x))
ChainRulesCore.@scalar_rule(cosint(x), cos(x) / x)

# non-holomorphic functions
function ChainRulesCore.frule((_, Δν, Δx), ::typeof(besselix), ν::Number, x::Number)
# primal
Ω = besselix(ν, x)

# derivative
∂Ω_∂ν = ChainRulesCore.@not_implemented(BESSEL_ORDER_INFO)
∂Ω_∂ν_Δν = ∂Ω_∂ν * Δν
ΔΩ = if ∂Ω_∂ν_Δν isa ChainRulesCore.NotImplemented
∂Ω_∂ν_Δν
else
Copy link
Member Author

Choose a reason for hiding this comment

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

I am not sure if this optimization is useful enough to justify the more verbose and less readable implementation. The optimization is also not performed when the derivative is defined with @scalar_rule.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I don't think it's necessary. Odds are when this happens it will be because a user actually tried to differentiate wrt the order, and they will instantly realize that's not possible. Ideally the compiler would realize a is unused and not compute it, but that doesn't seem to be the case.

Including it doesn't hurt though. You could always include in a comment the simpler expression so it's easier to follow. We do this in a few places in ChainRules.

a = (besselix(ν - 1, x) + besselix(ν + 1, x)) / 2
if Δx isa Real
muladd(muladd(-sign(real(x)), Ω, a), Δx, ∂Ω_∂ν_Δν)
else
muladd(a, Δx, muladd(-sign(real(x)) * Ω, real(Δx), ∂Ω_∂ν_Δν))
end
Copy link
Member Author

@devmotion devmotion Oct 2, 2021

Choose a reason for hiding this comment

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

This can't be tested currently and requires JuliaDiff/ChainRulesCore.jl#477 (currently this branch is not reached with neither the default nor the NoTangent() tangent, and finite differencing yields different values (and errors with complex numbers) if a ZeroTangent is specified since then nu is not ignored in finite differencing). With the CRC PR all definitions are tested and tests pass locally.

end

return Ω, ΔΩ
end
function ChainRulesCore.rrule(::typeof(besselix), ν::Number, x::Number)
Ω = besselix(ν, x)
project_x = ChainRulesCore.ProjectTo(x)
function besselix_pullback(ΔΩ)
ν̄ = ChainRulesCore.@not_implemented(BESSEL_ORDER_INFO)
a = (besselix(ν - 1, x) + besselix(ν + 1, x)) / 2
Copy link
Contributor

Choose a reason for hiding this comment

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

One can use the recurrence relations to write this as (besselix(ν ± 1, x) ± besselix(ν, x)) * ν / x, which allows to reuse Ω. It would just require some special-casing to handle x=0 gracefully.

Copy link
Member Author

Choose a reason for hiding this comment

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

I just reused the derivatives that are used for besseli etc. They were defined in this way in ChainRules originally. When I copied them to SpecialFunctions I wondered about the motivation for choosing https://functions.wolfram.com/Bessel-TypeFunctions/BesselI/20/01/02/0003/ over https://functions.wolfram.com/Bessel-TypeFunctions/BesselI/20/01/02/0001/ or https://functions.wolfram.com/Bessel-TypeFunctions/BesselI/20/01/02/0002/. I assumed the currently used definition is simpler since it does not require to handle x = 0 in a special way.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it might be best to use the same relations for besselix etc. as for besseli etc. and, if desired, change them to a different form in a separate PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's right, and they're the same in DiffRules as well. If you like, you can keep them similar to besseli, etc for now, and a future PR could update all of the rules.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree!

x̄ = project_x(muladd(conj(a), ΔΩ, - sign(real(x)) * real(conj(Ω) * ΔΩ)))
return ChainRulesCore.NoTangent(), ν̄, x̄
end
return Ω, besselix_pullback
end

function ChainRulesCore.frule((_, Δν, Δx), ::typeof(besseljx), ν::Number, x::Number)
# primal
Ω = besseljx(ν, x)

# derivative
∂Ω_∂ν = ChainRulesCore.@not_implemented(BESSEL_ORDER_INFO)
∂Ω_∂ν_Δν = ∂Ω_∂ν * Δν
ΔΩ = if ∂Ω_∂ν_Δν isa ChainRulesCore.NotImplemented
∂Ω_∂ν_Δν
else
a = (besseljx(ν - 1, x) - besseljx(ν + 1, x)) / 2
if Δx isa Real
a * Δx
else
muladd(a, Δx, muladd(-sign(imag(x)) * Ω, imag(Δx), ∂Ω_∂ν_Δν))
end
end

return Ω, ΔΩ
end
function ChainRulesCore.rrule(::typeof(besseljx), ν::Number, x::Number)
Ω = besseljx(ν, x)
project_x = ChainRulesCore.ProjectTo(x)
function besseljx_pullback(ΔΩ)
ν̄ = ChainRulesCore.@not_implemented(BESSEL_ORDER_INFO)
a = (besseljx(ν - 1, x) - besseljx(ν + 1, x)) / 2
x̄ = if x isa Real
project_x(a * ΔΩ)
else
project_x(muladd(conj(a), ΔΩ, - sign(imag(x)) * real(conj(Ω) * ΔΩ) * im))
end
return ChainRulesCore.NoTangent(), ν̄, x̄
end
return Ω, besseljx_pullback
end

function ChainRulesCore.frule((_, Δν, Δx), ::typeof(besselyx), ν::Number, x::Number)
# primal
Ω = besselyx(ν, x)

# derivative
∂Ω_∂ν = ChainRulesCore.@not_implemented(BESSEL_ORDER_INFO)
∂Ω_∂ν_Δν = ∂Ω_∂ν * Δν
ΔΩ = if ∂Ω_∂ν_Δν isa ChainRulesCore.NotImplemented
∂Ω_∂ν_Δν
else
a = (besselyx(ν - 1, x) - besselyx(ν + 1, x)) / 2
if Δx isa Real
a * Δx
else
muladd(a, Δx, muladd(-sign(imag(x)) * Ω, imag(Δx), ∂Ω_∂ν_Δν))
end
end

return Ω, ΔΩ
end
function ChainRulesCore.rrule(::typeof(besselyx), ν::Number, x::Number)
Ω = besselyx(ν, x)
project_x = ChainRulesCore.ProjectTo(x)
function besselyx_pullback(ΔΩ)
ν̄ = ChainRulesCore.@not_implemented(BESSEL_ORDER_INFO)
a = (besselyx(ν - 1, x) - besselyx(ν + 1, x)) / 2
x̄ = if x isa Real
project_x(a * ΔΩ)
else
project_x(muladd(conj(a), ΔΩ, - sign(imag(x)) * real(conj(Ω) * ΔΩ) * im))
end
return ChainRulesCore.NoTangent(), ν̄, x̄
end
return Ω, besselyx_pullback
end
9 changes: 9 additions & 0 deletions test/chainrules.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@
for nu in (-1.5, 2.2, 4.0)
test_frule(besseli, nu, x)
test_rrule(besseli, nu, x)
test_frule(besselix, nu, x) # derivative is `NotImplemented`
test_frule(besselix, nu ⊢ NoTangent(), x) # derivative is a number
test_rrule(besselix, nu, x)

test_frule(besselj, nu, x)
test_rrule(besselj, nu, x)
test_frule(besseljx, nu, x) # derivative is `NotImplemented`
test_frule(besseljx, nu ⊢ NoTangent(), x) # derivative is a number
test_rrule(besseljx, nu, x)

test_frule(besselk, nu, x)
test_rrule(besselk, nu, x)
Expand All @@ -64,6 +70,9 @@

test_frule(bessely, nu, x)
test_rrule(bessely, nu, x)
test_frule(besselyx, nu, x) # derivative is `NotImplemented`
test_frule(besselyx, nu ⊢ NoTangent(), x) # derivative is a number
test_rrule(besselyx, nu, x)

test_frule(hankelh1, nu, x)
test_rrule(hankelh1, nu, x)
Expand Down