Skip to content

Bug(GTPSA): gradient/jacobian/hessian/hvp fail and pushforward returns wrong values when f does not depend on x #1015

Description

@bdrhill

Description

When f does not propagate the input through GTPSA's TPS (e.g. f(x) = 42.0, or any function whose evaluation on a TPS input returns a plain Float64), the AutoGTPSA extension produces incorrect output. Two symptoms are visible, both rooted in the extension assuming f(xt) is always a TPS:

  • gradient, jacobian, hessian, hvp, second_derivative, value_and_gradient raise MethodError.
  • pushforward and value_and_pushforward return the function value as the pushforward, instead of zero.

Native GTPSA.gradient!/jacobian! also error when passed a plain Number/Vector{<:Number} (the wrapping into a TPS is something the DI extension would need to do).

MWE

using DifferentiationInterface
using GTPSA: GTPSA

backend = AutoGTPSA()
x = [1.0, 2.0]
v = [0.5, 0.7]

fc(x) = 42.0          # scalar-output, no dependence on x
fv(x) = [42.0, 7.0]   # vector-output, no dependence on x

# 1. Errors --------------------------------------------------------------
gradient(fc, backend, x)        # MethodError
jacobian(fv, backend, x)        # MethodError
hessian(fc, backend, x)         # MethodError
hvp(fc, backend, x, (v,))       # MethodError
second_derivative(t -> 42.0, backend, 1.5)  # BoundsError

# 2. Silently wrong values -----------------------------------------------
pushforward(fc, backend, x, (v,))[1]            # returns 42.0, expected 0.0
pushforward(fv, backend, x, (v,))[1]            # returns [42.0, 7.0], expected [0.0, 0.0]
value_and_pushforward(fc, backend, x, (v,))     # returns (42.0, (42.0,)), expected (42.0, (0.0,))

Workaround: force TPS propagation, e.g. f(x) = 0*x[1] + 42.0. Then gradient returns [0.0, 0.0] and pushforward returns 0.0, as expected.

Stacktraces
gradient(fc, AutoGTPSA(), [1.0, 2.0])

ERROR: MethodError: no method matching gradient!(::Vector{Float64}, ::Float64; include_params::Bool, unsafe_inbounds::Bool)
The function `gradient!` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  gradient!(::Any, !Matched::GTPSA.TPS; include_params, unsafe_inbounds)
   @ GTPSA ~/.julia/packages/GTPSA/IhdRA/src/getset.jl:362

Stacktrace:
 [1] gradient(::Function, ::DifferentiationInterfaceGTPSAExt.GTPSAOneArgGradientPrep{...}, ::AutoGTPSA{Nothing}, ::Vector{Float64})
   @ DifferentiationInterfaceGTPSAExt ~/dev/DifferentiationInterface.jl/DifferentiationInterface/ext/DifferentiationInterfaceGTPSAExt/onearg.jl:145
 [2] gradient(::var"#fc#fc##0", ::AutoGTPSA{Nothing}, ::Vector{Float64})
   @ DifferentiationInterface ~/dev/DifferentiationInterface.jl/DifferentiationInterface/src/first_order/gradient.jl:63
jacobian(fv, AutoGTPSA(), [1.0, 2.0])

ERROR: MethodError: no method matching jacobian!(::Matrix{Float64}, ::Vector{Float64}; include_params::Bool, unsafe_inbounds::Bool)
The function `jacobian!` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  jacobian!(::Any, !Matched::AbstractArray{<:GTPSA.TPS}; include_params, unsafe_inbounds)
   @ GTPSA ~/.julia/packages/GTPSA/IhdRA/src/getset.jl:417
second_derivative(t -> 42.0, AutoGTPSA(), 1.5)

ERROR: BoundsError

Expected Behavior

For f that does not depend on x:

  • All differentiation operators return zeros (of the appropriate shape and type), and value_and_* returns (f(x), zeros).

Actual Behavior

Operator Behavior
gradient, value_and_gradient MethodError
jacobian MethodError
hessian MethodError
hvp MethodError
second_derivative BoundsError
pushforward (scalar output) returns f(x) instead of 0
pushforward (vector output) returns f(x) instead of zeros
value_and_pushforward tangent equals f(x) instead of zeros
derivative (scalar input) returns f(x) (also incorrect for the same reason)

Root Cause

In ext/DifferentiationInterfaceGTPSAExt/onearg.jl, the pushforward implementation does:

yt = fc(prep.xt)
if yt isa Number
    return yt[1]                  # if yt is Float64, yt[1] == yt
else
    dy = map(t -> t[1], yt)       # if eltype(yt) is Number, t[1] == t
    return dy
end

When f does not propagate the TPS, yt is a plain Float64 (or Vector{Float64}), and yt[1] returns the value rather than the first-order coefficient.

The same path is taken by gradient/jacobian/hessian which then pass the plain Float64/Vector{Float64} to GTPSA.gradient!/GTPSA.jacobian!, neither of which accepts non-TPS input.

A check on whether yt isa TPS (or eltype(yt) <: TPS) and returning zero(...) in the non-TPS case would match the behavior of other backends. ForwardDiff, for instance, returns [0.0, 0.0] for gradient(x -> 42.0, AutoForwardDiff(), [1.0, 2.0]).

Backend

  • Backend: AutoGTPSA()
  • Works with other backends: ForwardDiff, Enzyme, FiniteDiff all return zeros for the same inputs
  • Native API behavior: GTPSA.gradient!(g, fc(xt)) errors with the same MethodError (fc(xt) produces a Float64 rather than a TPS). Wrapping in a TPS first (y_tps = TPS{Float64}(use=d); y_tps[0] = fc(xt)) then calling GTPSA.gradient!(g, y_tps) returns [0.0, 0.0].

This is the same class of issue as #1013 (AutoHyperHessians), but for AutoGTPSA it also affects first-order operators and produces silently-wrong values from pushforward.

Environment

  • Julia 1.12.5
  • DifferentiationInterface v0.7.18 (this branch)
  • GTPSA v1.5.3
  • ADTypes v1.22.0
Full environment
julia> using Pkg; Pkg.status()
Status `~/dev/DifferentiationInterface.jl/DifferentiationInterface/test/Back/GTPSA/Project.toml`
  [47edcb42] ADTypes v1.22.0
  [a0c0ee7d] DifferentiationInterface v0.7.18 `../../..`
  [a82114a7] DifferentiationInterfaceTest v0.11.0 `../../../../DifferentiationInterfaceTest`
  [7d51a73a] ExplicitImports v1.15.0
  [f6369f11] ForwardDiff v1.3.3
  [b27dd330] GTPSA v1.5.3
  [c3a54625] JET v0.11.3
  [9f842d2f] SparseConnectivityTracer v1.2.1
  [0a514795] SparseMatrixColorings v0.4.27
  [8dfed614] Test v1.11.0

julia> versioninfo()
Julia Version 1.12.5
Commit 5fe89b8ddc1 (2026-02-09 16:05 UTC)
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz
  WORD_SIZE: 64
  LLVM: libLLVM-18.1.7 (ORCJIT, sandybridge)
Threads: 1 default, 1 interactive, 1 GC (on 4 virtual cores)

🤖 I am a robot. This is an experiment in agentic bug-catching under the supervision of @adrhill and @gdalle (#1008). Contents may be hallucinated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    botIssue or PR created automatically, wait for human review before interacting

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions