Skip to content

Commit 9bae080

Browse files
Merge pull request #229 from ChrisRackauckas-Claude/fix-oop-jacobian-inference
Make the out-of-place jacobian's return type inferrable
2 parents b3d662d + 8ea0814 commit 9bae080

3 files changed

Lines changed: 45 additions & 23 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "FiniteDiff"
22
uuid = "6a86dc24-6348-571c-b903-95158fe2bd41"
3-
version = "2.32.0"
3+
version = "2.32.1"
44

55
[deps]
66
ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"

src/jacobians.jl

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -294,14 +294,17 @@ function finite_difference_jacobian(
294294
copyto!(x1, x)
295295
end
296296

297-
if !(f_in isa Nothing)
298-
vecfx = _vec(f_in)
297+
# Single assignment site: `vecfx` is captured by the `calculate_Ji_*` closures
298+
# below, and a captured variable with several assignment sites is boxed, which
299+
# makes the whole function's return type uninferrable.
300+
vecfx = if !(f_in isa Nothing)
301+
_vec(f_in)
299302
elseif fdtype == Val(:forward)
300-
vecfx = _vec(f(x))
303+
_vec(f(x))
301304
elseif fdtype == Val(:complex) && returntype <: Real
302-
vecfx = real(fx)
305+
real(fx)
303306
else
304-
vecfx = _vec(fx)
307+
_vec(fx)
305308
end
306309
vecx = _vec(x)
307310
J = jac_prototype isa Nothing ?
@@ -310,13 +313,16 @@ function finite_difference_jacobian(
310313
nrows, ncols = size(J)
311314

312315
if !(sparsity isa Nothing)
313-
rows_index, cols_index = ArrayInterface.findstructralnz(sparsity)
314-
rows_index = [rows_index[i] for i in 1:length(rows_index)]
315-
cols_index = [cols_index[i] for i in 1:length(cols_index)]
316+
structural_rows, structural_cols = ArrayInterface.findstructralnz(sparsity)
317+
rows_index = [structural_rows[i] for i in 1:length(structural_rows)]
318+
cols_index = [structural_cols[i] for i in 1:length(structural_cols)]
316319
end
317320

318321
if fdtype == Val(:forward)
322+
# `local` keeps these from aliasing (and thereby boxing) the same-named
323+
# variables of the enclosing function; see the `vecfx` comment above.
319324
function calculate_Ji_forward(i)
325+
local x_save, epsilon, _vecx1, _x1, vecfx1, dx
320326
x_save = ArrayInterface.allowed_getindex(vecx, i)
321327
epsilon = compute_epsilon(Val(:forward), x_save, relstep, absstep, dir)
322328
_vecx1 = setindex(vecx, x_save+epsilon, i)
@@ -336,12 +342,12 @@ function finite_difference_jacobian(
336342
J = J + _make_Ji(J, eltype(x), dx, color_i, nrows, ncols)
337343
else
338344
tmp = norm(vecx .* (colorvec .== color_i))
339-
epsilon = compute_epsilon(
345+
epsilon_c = compute_epsilon(
340346
Val(:forward), sqrt(tmp), relstep, absstep, dir)
341-
_vecx = @. vecx + epsilon * (colorvec == color_i)
347+
_vecx = @. vecx + epsilon_c * (colorvec == color_i)
342348
_x = reshape(_vecx, axes(x))
343349
vecfx1 = _vec(f(_x))
344-
dx = (vecfx1-vecfx)/epsilon
350+
dx = (vecfx1-vecfx)/epsilon_c
345351
Ji = _make_Ji(
346352
J, rows_index, cols_index, dx, colorvec, color_i, nrows, ncols)
347353
J = J + Ji
@@ -354,15 +360,16 @@ function finite_difference_jacobian(
354360
# unsafe — the cache may have been built via `similar(x)` or reused at a
355361
# different x — so we always perturb around `vecx` directly.
356362
function calculate_Ji_central(i)
363+
local x_save, epsilon, _vecx1, _vecx, _x1, _x, vecfx1, vecfx0, dx
357364
x_save = ArrayInterface.allowed_getindex(vecx, i)
358365
epsilon = compute_epsilon(Val(:forward), x_save, relstep, absstep, dir)
359366
_vecx1 = setindex(vecx, x_save+epsilon, i)
360367
_vecx = setindex(vecx, x_save-epsilon, i)
361368
_x1 = reshape(_vecx1, axes(x))
362369
_x = reshape(_vecx, axes(x))
363370
vecfx1 = _vec(f(_x1))
364-
vecfx = _vec(f(_x))
365-
dx = (vecfx1-vecfx)/(2epsilon)
371+
vecfx0 = _vec(f(_x))
372+
dx = (vecfx1-vecfx0)/(2epsilon)
366373
return dx
367374
end
368375

@@ -376,15 +383,15 @@ function finite_difference_jacobian(
376383
J = J + _make_Ji(J, eltype(x), dx, color_i, nrows, ncols)
377384
else
378385
tmp = norm(vecx .* (colorvec .== color_i))
379-
epsilon = compute_epsilon(
386+
epsilon_c = compute_epsilon(
380387
Val(:forward), sqrt(tmp), relstep, absstep, dir)
381-
_vecx1 = @. vecx + epsilon * (colorvec == color_i)
382-
_vecx = @. vecx - epsilon * (colorvec == color_i)
388+
_vecx1 = @. vecx + epsilon_c * (colorvec == color_i)
389+
_vecx = @. vecx - epsilon_c * (colorvec == color_i)
383390
_x1 = reshape(_vecx1, axes(x))
384391
_x = reshape(_vecx, axes(x))
385392
vecfx1 = _vec(f(_x1))
386-
vecfx = _vec(f(_x))
387-
dx = (vecfx1-vecfx)/(2epsilon)
393+
vecfx0 = _vec(f(_x))
394+
dx = (vecfx1-vecfx0)/(2epsilon_c)
388395
Ji = _make_Ji(
389396
J, rows_index, cols_index, dx, colorvec, color_i, nrows, ncols)
390397
J = J + Ji
@@ -395,11 +402,12 @@ function finite_difference_jacobian(
395402
epsilon = eps(eltype(x))
396403

397404
function calculate_Ji_complex(i)
405+
local x_save, _vecx, _x, vecfx_c, dx
398406
x_save = ArrayInterface.allowed_getindex(vecx, i)
399407
_vecx = setindex(complex.(vecx), x_save+im*epsilon, i)
400408
_x = reshape(_vecx, axes(x))
401-
vecfx = _vec(f(_x))
402-
dx = imag(vecfx)/epsilon
409+
vecfx_c = _vec(f(_x))
410+
dx = imag(vecfx_c)/epsilon
403411
return dx
404412
end
405413

@@ -414,8 +422,8 @@ function finite_difference_jacobian(
414422
else
415423
_vecx = @. vecx + im * epsilon * (colorvec == color_i)
416424
_x = reshape(_vecx, axes(x))
417-
vecfx = _vec(f(_x))
418-
dx = imag(vecfx)/epsilon
425+
vecfx_c = _vec(f(_x))
426+
dx = imag(vecfx_c)/epsilon
419427
Ji = _make_Ji(
420428
J, rows_index, cols_index, dx, colorvec, color_i, nrows, ncols)
421429
J = J + Ji

test/out_of_place_tests.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,17 @@ J = FiniteDiff.finite_difference_jacobian(f, x, Val{:central}, eltype(x))
5555
J = FiniteDiff.finite_difference_jacobian(f, x, Val{:complex}, eltype(x))
5656
@test J fill(1.0, 2, 1)
5757
@test J isa SMatrix{2,1}
58+
59+
# The per-color closures used to assign variables that also live in the enclosing
60+
# function's scope, which boxed the captures and made the return type `Any`.
61+
# Static-array inputs stay uninferrable here: the `mapreduce(_, hcat, _)` accumulator
62+
# grows one `SMatrix` column per color, so its type depends on `maximum(colorvec)`.
63+
@testset "Type stability of the dense out-of-place jacobian" begin
64+
g(x) = x .^ 2 .- 2
65+
x = [1.0, 2.0, 3.0]
66+
@testset "$difftype" for difftype in (:forward, :central, :complex)
67+
cache = FiniteDiff.JacobianCache(x, Val{difftype}, eltype(x))
68+
@test (@inferred FiniteDiff.finite_difference_jacobian(g, x, cache))
69+
Diagonal(2x)
70+
end
71+
end

0 commit comments

Comments
 (0)