Skip to content

Commit e8b422e

Browse files
authored
Merge branch 'master' into Require-Julia-LTS
2 parents a9af1a1 + d9afcbe commit e8b422e

File tree

14 files changed

+161
-62
lines changed

14 files changed

+161
-62
lines changed

.github/workflows/ci.yml

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: CI
2+
23
on:
34
pull_request:
45
branches:
@@ -8,52 +9,50 @@ on:
89
- master
910
tags: '*'
1011
workflow_dispatch:
12+
13+
# needed to allow julia-actions/cache to delete old caches that it has created
14+
permissions:
15+
actions: write
16+
contents: read
17+
1118
jobs:
1219
test:
13-
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
20+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ github.event_name }}
1421
runs-on: ${{ matrix.os }}
15-
continue-on-error: ${{ matrix.version == 'nightly' }}
1622
strategy:
1723
fail-fast: false
1824
matrix:
1925
version:
26+
- 'min'
2027
- 'lts'
2128
- '1'
22-
# - 'nightly'
2329
os:
2430
- ubuntu-latest
25-
arch:
26-
- x64
31+
- windows-latest
32+
- macOS-latest
2733
steps:
28-
- uses: actions/checkout@v4
34+
- uses: actions/checkout@v5
2935
- uses: julia-actions/setup-julia@v2
3036
with:
3137
version: ${{ matrix.version }}
32-
arch: ${{ matrix.arch }}
33-
- uses: actions/cache@v4
34-
env:
35-
cache-name: cache-artifacts
36-
with:
37-
path: ~/.julia/artifacts
38-
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
39-
restore-keys: |
40-
${{ runner.os }}-test-${{ env.cache-name }}-
41-
${{ runner.os }}-test-
42-
${{ runner.os }}-
38+
- uses: julia-actions/cache@v2
4339
- uses: julia-actions/julia-buildpkg@v1
4440
- uses: julia-actions/julia-runtest@v1
4541
- uses: julia-actions/julia-processcoverage@v1
46-
- uses: codecov/codecov-action@v4
42+
- uses: codecov/codecov-action@v5
4743
with:
48-
file: lcov.info
44+
files: lcov.info
45+
token: ${{ secrets.CODECOV_TOKEN }}
46+
fail_ci_if_error: true
4947
docs:
5048
name: Documentation
5149
runs-on: ubuntu-latest
5250
steps:
53-
- uses: actions/checkout@v4
51+
- uses: actions/checkout@v5
5452
- uses: julia-actions/setup-julia@v2
5553
with:
5654
version: '1'
55+
- uses: julia-actions/cache@v2
5756
- run: |
5857
julia --project=docs -e '
5958
using Pkg

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "ForwardDiff"
22
uuid = "f6369f11-7733-5829-9624-2563aa707210"
3-
version = "0.11.0-DEV"
3+
version = "1.0.1"
44

55
[deps]
66
CommonSubexpressions = "bbf7d656-a473-5ed7-a52c-81e309532950"

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
1-
[![CI](https://github.com/JuliaDiff/ForwardDiff.jl/workflows/CI/badge.svg)](https://github.com/JuliaDiff/ForwardDiff.jl/actions/workflows/ci.yml)
2-
[![Coverage Status](https://coveralls.io/repos/JuliaDiff/ForwardDiff.jl/badge.svg?branch=master&service=github)](https://coveralls.io/github/JuliaDiff/ForwardDiff.jl?branch=master)
1+
[![CI](https://github.com/JuliaDiff/ForwardDiff.jl/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/JuliaDiff/ForwardDiff.jl/actions/workflows/ci.yml?query=branch%3Amaster)
2+
[![codecov](https://codecov.io/gh/JuliaDiff/ForwardDiff.jl/graph/badge.svg?token=SzHfhyoxUa)](https://codecov.io/gh/JuliaDiff/ForwardDiff.jl)
33

44
[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://juliadiff.org/ForwardDiff.jl/stable)
55
[![](https://img.shields.io/badge/docs-dev-blue.svg)](https://juliadiff.org/ForwardDiff.jl/dev)
66

77
# ForwardDiff.jl
88

9+
## Upgrading to ForwardDiff.jl 1.0
10+
11+
Equality (`==`) on `Dual` numbers now requires both the real and dual part to match (https://github.com/JuliaDiff/ForwardDiff.jl/pull/481).
12+
This removes a large number of bugs where the "structure" of e.g, non-zero values in an array was inspected, leading to erroneous derivatives.
13+
This might cause slightly different behavior in programs but should in general be more correct than previously.
14+
15+
---------------------------
16+
917
ForwardDiff implements methods to take **derivatives**, **gradients**, **Jacobians**, **Hessians**, and higher-order derivatives of native Julia functions (or any callable object, really) using **forward mode automatic differentiation (AD)**.
1018

1119
While performance can vary depending on the functions you evaluate, the algorithms implemented by ForwardDiff generally outperform non-AD algorithms (such as finite-differencing) in both speed and accuracy.

docs/src/dev/how_it_works.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,9 @@ learn about [hyper-dual numbers](https://arc.aiaa.org/doi/abs/10.2514/6.2011-886
5656
which extend dual numbers to higher orders by introducing extra ``\epsilon`` terms that can
5757
cross-multiply. ForwardDiff's `Dual` number implementation naturally supports hyper-dual
5858
numbers without additional code by allowing instances of the `Dual` type to nest within each
59-
other. For example, a second-order hyper-dual number has the type `Dual{T,Dual{S,V,M},N}`, a
60-
third-order hyper-dual number has the type `Dual{T,Dual{S,Dual{R,V,K},M},N}`, and so on.
59+
other. For example, a second-order hyper-dual number has the type `Dual{T,Dual{S,V,M},N}`,
60+
and can be formed as follows: `Dual(Dual(x, one(eltype(x))), one(eltype(x)))`. A third-order
61+
hyper-dual number has the type `Dual{T,Dual{S,Dual{R,V,K},M},N}`, and so on.
6162

6263
## ForwardDiff's API
6364

docs/src/user/limitations.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ function being differentiated):
77

88
- **The target function can only be composed of generic Julia functions.** ForwardDiff cannot propagate derivative information through non-Julia code. Thus, your function may not work if it makes calls to external, non-Julia programs, e.g. uses explicit BLAS calls instead of `Ax_mul_Bx`-style functions.
99

10+
- **The target function must be a composition of differentiable functions.** ForwardDiff can have issues to compute derivatives of functions, which are composed of at least one function, which is not differentiable in the point the derivative should be evaluated, even if the target function itself is differentiable. A simple example is `f(x) = norm(x)^2`, where `ForwardDiff.gradient(f, zeros(2))` returns a vector of `NaN`s since the Euclidean norm is not differentiable in zero. A possible solution to this issue is to, e.g., define `f(x) = sum(abs2, x)` instead. In situations, where rewriting the target function only as a composition of differentiable functions is more complicated (e.g. `f(x) = (1 + norm(x))exp(-norm(x))`)), one would need to define a custom derivative rule (see [this comment](https://github.com/JuliaDiff/ForwardDiff.jl/issues/303#issuecomment-2977990425)).
11+
1012
- **The target function must be unary (i.e., only accept a single argument).** [`ForwardDiff.jacobian`](@ref) is an exception to this rule.
1113

1214
- **The target function must be written generically enough to accept numbers of type `T<:Real` as input (or arrays of these numbers).** The function doesn't require a specific type signature, as long as the type signature is generic enough to avoid breaking this rule. This also means that any storage assigned used within the function must be generic as well (see [this comment](https://github.com/JuliaDiff/ForwardDiff.jl/issues/136#issuecomment-237941790) for an example).

ext/ForwardDiffStaticArraysExt.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,16 +103,16 @@ end
103103
T = typeof(Tag(f, eltype(x)))
104104
ydual = static_dual_eval(T, f, x)
105105
result = DiffResults.jacobian!(result, extract_jacobian(T, ydual, x))
106-
result = DiffResults.value!(d -> value(T,d), result, ydual)
106+
result = DiffResults.value!(Base.Fix1(value, T), result, ydual)
107107
return result
108108
end
109109

110110
# Hessian
111-
ForwardDiff.hessian(f::F, x::StaticArray) where {F} = jacobian(y -> gradient(f, y), x)
111+
ForwardDiff.hessian(f::F, x::StaticArray) where {F} = jacobian(Base.Fix1(gradient, f), x)
112112
ForwardDiff.hessian(f::F, x::StaticArray, cfg::HessianConfig) where {F} = hessian(f, x)
113113
ForwardDiff.hessian(f::F, x::StaticArray, cfg::HessianConfig, ::Val) where {F} = hessian(f, x)
114114

115-
ForwardDiff.hessian!(result::AbstractArray, f::F, x::StaticArray) where {F} = jacobian!(result, y -> gradient(f, y), x)
115+
ForwardDiff.hessian!(result::AbstractArray, f::F, x::StaticArray) where {F} = jacobian!(result, Base.Fix1(gradient, f), x)
116116

117117
ForwardDiff.hessian!(result::MutableDiffResult, f::F, x::StaticArray) where {F} = hessian!(result, f, x, HessianConfig(f, result, x))
118118

src/apiutils.jl

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,32 +40,68 @@ end
4040
return Expr(:tuple, [:(single_seed(Partials{N,V}, Val{$i}())) for i in 1:N]...)
4141
end
4242

43+
# Only seed indices that are structurally non-zero
44+
structural_eachindex(x::AbstractArray) = structural_eachindex(x, x)
45+
function structural_eachindex(x::AbstractArray, y::AbstractArray)
46+
require_one_based_indexing(x, y)
47+
eachindex(x, y)
48+
end
49+
function structural_eachindex(x::UpperTriangular, y::AbstractArray)
50+
require_one_based_indexing(x, y)
51+
if size(x) != size(y)
52+
throw(DimensionMismatch())
53+
end
54+
n = size(x, 1)
55+
return (CartesianIndex(i, j) for j in 1:n for i in 1:j)
56+
end
57+
function structural_eachindex(x::LowerTriangular, y::AbstractArray)
58+
require_one_based_indexing(x, y)
59+
if size(x) != size(y)
60+
throw(DimensionMismatch())
61+
end
62+
n = size(x, 1)
63+
return (CartesianIndex(i, j) for j in 1:n for i in j:n)
64+
end
65+
function structural_eachindex(x::Diagonal, y::AbstractArray)
66+
require_one_based_indexing(x, y)
67+
if size(x) != size(y)
68+
throw(DimensionMismatch())
69+
end
70+
return diagind(x)
71+
end
72+
4373
function seed!(duals::AbstractArray{Dual{T,V,N}}, x,
4474
seed::Partials{N,V} = zero(Partials{N,V})) where {T,V,N}
45-
duals .= Dual{T,V,N}.(x, Ref(seed))
75+
for idx in structural_eachindex(duals, x)
76+
duals[idx] = Dual{T,V,N}(x[idx], seed)
77+
end
4678
return duals
4779
end
4880

4981
function seed!(duals::AbstractArray{Dual{T,V,N}}, x,
5082
seeds::NTuple{N,Partials{N,V}}) where {T,V,N}
51-
dual_inds = 1:N
52-
duals[dual_inds] .= Dual{T,V,N}.(view(x,dual_inds), seeds)
83+
for (i, idx) in zip(1:N, structural_eachindex(duals, x))
84+
duals[idx] = Dual{T,V,N}(x[idx], seeds[i])
85+
end
5386
return duals
5487
end
5588

5689
function seed!(duals::AbstractArray{Dual{T,V,N}}, x, index,
5790
seed::Partials{N,V} = zero(Partials{N,V})) where {T,V,N}
5891
offset = index - 1
59-
dual_inds = (1:N) .+ offset
60-
duals[dual_inds] .= Dual{T,V,N}.(view(x, dual_inds), Ref(seed))
92+
idxs = Iterators.drop(structural_eachindex(duals, x), offset)
93+
for idx in idxs
94+
duals[idx] = Dual{T,V,N}(x[idx], seed)
95+
end
6196
return duals
6297
end
6398

6499
function seed!(duals::AbstractArray{Dual{T,V,N}}, x, index,
65100
seeds::NTuple{N,Partials{N,V}}, chunksize = N) where {T,V,N}
66101
offset = index - 1
67-
seed_inds = 1:chunksize
68-
dual_inds = seed_inds .+ offset
69-
duals[dual_inds] .= Dual{T,V,N}.(view(x, dual_inds), getindex.(Ref(seeds), seed_inds))
102+
idxs = Iterators.drop(structural_eachindex(duals, x), offset)
103+
for (i, idx) in zip(1:chunksize, idxs)
104+
duals[idx] = Dual{T,V,N}(x[idx], seeds[i])
105+
end
70106
return duals
71107
end

src/config.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,10 @@ struct DerivativeConfig{T,D} <: AbstractConfig{1}
6464
end
6565

6666
"""
67-
ForwardDiff.DerivativeConfig(f!, y::AbstractArray, x::AbstractArray)
67+
ForwardDiff.DerivativeConfig(f!, y::AbstractArray, x::Real)
6868
6969
Return a `DerivativeConfig` instance based on the type of `f!`, and the types/shapes of the
70-
output vector `y` and the input vector `x`.
70+
output vector `y` and the input value `x`.
7171
7272
The returned `DerivativeConfig` instance contains all the work buffers required by
7373
`ForwardDiff.derivative` and `ForwardDiff.derivative!` when the target function takes the form

src/gradient.jl

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Set `check` to `Val{false}()` to disable tag checking. This can lead to perturba
1616
function gradient(f::F, x::AbstractArray, cfg::GradientConfig{T} = GradientConfig(f, x), ::Val{CHK}=Val{true}()) where {F, T, CHK}
1717
require_one_based_indexing(x)
1818
CHK && checktag(T, f, x)
19-
if chunksize(cfg) == length(x)
19+
if chunksize(cfg) == structural_length(x)
2020
return vector_mode_gradient(f, x, cfg)
2121
else
2222
return chunk_mode_gradient(f, x, cfg)
@@ -35,7 +35,7 @@ This method assumes that `isa(f(x), Real)`.
3535
function gradient!(result::Union{AbstractArray,DiffResult}, f::F, x::AbstractArray, cfg::GradientConfig{T} = GradientConfig(f, x), ::Val{CHK}=Val{true}()) where {T, CHK, F}
3636
result isa DiffResult ? require_one_based_indexing(x) : require_one_based_indexing(result, x)
3737
CHK && checktag(T, f, x)
38-
if chunksize(cfg) == length(x)
38+
if chunksize(cfg) == structural_length(x)
3939
vector_mode_gradient!(result, f, x, cfg)
4040
else
4141
chunk_mode_gradient!(result, f, x, cfg)
@@ -63,12 +63,19 @@ function extract_gradient!(::Type{T}, result::DiffResult, dual::Dual) where {T}
6363
end
6464

6565
extract_gradient!(::Type{T}, result::AbstractArray, y::Real) where {T} = fill!(result, zero(y))
66-
extract_gradient!(::Type{T}, result::AbstractArray, dual::Dual) where {T}= copyto!(result, partials(T, dual))
66+
function extract_gradient!(::Type{T}, result::AbstractArray, dual::Dual) where {T}
67+
idxs = structural_eachindex(result)
68+
for (i, idx) in zip(1:npartials(dual), idxs)
69+
result[idx] = partials(T, dual, i)
70+
end
71+
return result
72+
end
6773

6874
function extract_gradient_chunk!(::Type{T}, result, dual, index, chunksize) where {T}
6975
offset = index - 1
70-
for i in 1:chunksize
71-
result[i + offset] = partials(T, dual, i)
76+
idxs = Iterators.drop(structural_eachindex(result), offset)
77+
for (i, idx) in zip(1:chunksize, idxs)
78+
result[idx] = partials(T, dual, i)
7279
end
7380
return result
7481
end
@@ -106,10 +113,10 @@ end
106113

107114
function chunk_mode_gradient_expr(result_definition::Expr)
108115
return quote
109-
@assert length(x) >= N "chunk size cannot be greater than length(x) ($(N) > $(length(x)))"
116+
@assert structural_length(x) >= N "chunk size cannot be greater than ForwardDiff.structural_length(x) ($(N) > $(structural_length(x)))"
110117

111118
# precalculate loop bounds
112-
xlen = length(x)
119+
xlen = structural_length(x)
113120
remainder = xlen % N
114121
lastchunksize = ifelse(remainder == 0, N, remainder)
115122
lastchunkindex = xlen - lastchunksize + 1

src/jacobian.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Set `check` to `Val{false}()` to disable tag checking. This can lead to perturba
1818
function jacobian(f::F, x::AbstractArray, cfg::JacobianConfig{T} = JacobianConfig(f, x), ::Val{CHK}=Val{true}()) where {F,T,CHK}
1919
require_one_based_indexing(x)
2020
CHK && checktag(T, f, x)
21-
if chunksize(cfg) == length(x)
21+
if chunksize(cfg) == structural_length(x)
2222
return vector_mode_jacobian(f, x, cfg)
2323
else
2424
return chunk_mode_jacobian(f, x, cfg)
@@ -36,7 +36,7 @@ Set `check` to `Val{false}()` to disable tag checking. This can lead to perturba
3636
function jacobian(f!::F, y::AbstractArray, x::AbstractArray, cfg::JacobianConfig{T} = JacobianConfig(f!, y, x), ::Val{CHK}=Val{true}()) where {F,T, CHK}
3737
require_one_based_indexing(y, x)
3838
CHK && checktag(T, f!, x)
39-
if chunksize(cfg) == length(x)
39+
if chunksize(cfg) == structural_length(x)
4040
return vector_mode_jacobian(f!, y, x, cfg)
4141
else
4242
return chunk_mode_jacobian(f!, y, x, cfg)
@@ -57,7 +57,7 @@ Set `check` to `Val{false}()` to disable tag checking. This can lead to perturba
5757
function jacobian!(result::Union{AbstractArray,DiffResult}, f::F, x::AbstractArray, cfg::JacobianConfig{T} = JacobianConfig(f, x), ::Val{CHK}=Val{true}()) where {F,T, CHK}
5858
result isa DiffResult ? require_one_based_indexing(x) : require_one_based_indexing(result, x)
5959
CHK && checktag(T, f, x)
60-
if chunksize(cfg) == length(x)
60+
if chunksize(cfg) == structural_length(x)
6161
vector_mode_jacobian!(result, f, x, cfg)
6262
else
6363
chunk_mode_jacobian!(result, f, x, cfg)
@@ -78,7 +78,7 @@ Set `check` to `Val{false}()` to disable tag checking. This can lead to perturba
7878
function jacobian!(result::Union{AbstractArray,DiffResult}, f!::F, y::AbstractArray, x::AbstractArray, cfg::JacobianConfig{T} = JacobianConfig(f!, y, x), ::Val{CHK}=Val{true}()) where {F,T,CHK}
7979
result isa DiffResult ? require_one_based_indexing(y, x) : require_one_based_indexing(result, y, x)
8080
CHK && checktag(T, f!, x)
81-
if chunksize(cfg) == length(x)
81+
if chunksize(cfg) == structural_length(x)
8282
vector_mode_jacobian!(result, f!, y, x, cfg)
8383
else
8484
chunk_mode_jacobian!(result, f!, y, x, cfg)
@@ -169,10 +169,10 @@ const JACOBIAN_ERROR = DimensionMismatch("jacobian(f, x) expects that f(x) is an
169169
function jacobian_chunk_mode_expr(work_array_definition::Expr, compute_ydual::Expr,
170170
result_definition::Expr, y_definition::Expr)
171171
return quote
172-
@assert length(x) >= N "chunk size cannot be greater than length(x) ($(N) > $(length(x)))"
172+
@assert structural_length(x) >= N "chunk size cannot be greater than ForwardDiff.structural_length(x) ($(N) > $(structural_length(x)))"
173173

174174
# precalculate loop bounds
175-
xlen = length(x)
175+
xlen = structural_length(x)
176176
remainder = xlen % N
177177
lastchunksize = ifelse(remainder == 0, N, remainder)
178178
lastchunkindex = xlen - lastchunksize + 1

0 commit comments

Comments
 (0)