From b886ae336246941b2621d7d4c2397383f5e9fbec Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Thu, 12 Feb 2026 12:50:13 -0500 Subject: [PATCH 1/5] fix parametric obj --- src/NonLinearProgram/nlp_utilities.jl | 8 ++++--- test/nlp_program.jl | 32 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index edcbe6e8..8f3a79b1 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -489,9 +489,11 @@ function _compute_sensitivity(model::Model; tol = 1e-6) # Dual bounds upper ∂s[((num_w+num_cons+num_lower+1):end), :] *= -_sense_multiplier - # dual wrt parameter + grad = _compute_gradient(model) primal_idx = [i.value for i in model.cache.primal_vars] - df_dx = _compute_gradient(model)[primal_idx] - df_dp = df_dx'∂s[1:num_vars, :] + params_idx = [i.value for i in model.cache.params] + df_dx = grad[primal_idx] + df_dp_direct = grad[params_idx] + df_dp = df_dx'∂s[1:num_vars, :] + df_dp_direct' return ∂s, df_dp end diff --git a/test/nlp_program.jl b/test/nlp_program.jl index c266c715..e47819a8 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -740,6 +740,38 @@ function test_ObjectiveSensitivity_model2() @test isapprox(dp, -1.5; atol = 1e-4) end +function test_ObjectiveSensitivity_direct_param_contrib() + model = DiffOpt.nonlinear_diff_model(Ipopt.Optimizer) + set_silent(model) + + p_val = 3.0 + @variable(model, p ∈ MOI.Parameter(p_val)) + @variable(model, x ≥ 1) + @objective(model, Min, p^2 * x^2) + + optimize!(model) + @assert is_solved_and_feasible(model) + + Δp = 0.1 + DiffOpt.set_forward_parameter(model, p, Δp) + DiffOpt.forward_differentiate!(model) + + df_dp = MOI.get(model, DiffOpt.ForwardObjectiveSensitivity()) + @test isapprox(df_dp, 2 * p_val * Δp, atol=1e-8) # ≈ 0.6 for p=3 + + ε = 1e-6 + df_dp_fd = (begin + set_parameter_value(p, p_val + ε) + optimize!(model) + Δp * objective_value(model) + end - begin + set_parameter_value(p, p_val - ε) + optimize!(model) + Δp * objective_value(model) + end) / (2ε) + + @test isapprox(df_dp, df_dp_fd, atol=1e-4) +end function test_ObjectiveSensitivity_subset_parameters() # Model with 10 parameters, differentiate only w.r.t. 3rd and 7th model = Model(() -> DiffOpt.diff_optimizer(Ipopt.Optimizer)) From e72beedaabaa9e2c2aca8bd358dbf8d3bb64e503 Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Thu, 12 Feb 2026 13:03:39 -0500 Subject: [PATCH 2/5] format --- test/nlp_program.jl | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test/nlp_program.jl b/test/nlp_program.jl index e47819a8..34a41b8d 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -757,20 +757,23 @@ function test_ObjectiveSensitivity_direct_param_contrib() DiffOpt.forward_differentiate!(model) df_dp = MOI.get(model, DiffOpt.ForwardObjectiveSensitivity()) - @test isapprox(df_dp, 2 * p_val * Δp, atol=1e-8) # ≈ 0.6 for p=3 + @test isapprox(df_dp, 2 * p_val * Δp, atol = 1e-8) # ≈ 0.6 for p=3 ε = 1e-6 - df_dp_fd = (begin - set_parameter_value(p, p_val + ε) - optimize!(model) - Δp * objective_value(model) - end - begin - set_parameter_value(p, p_val - ε) - optimize!(model) - Δp * objective_value(model) - end) / (2ε) - - @test isapprox(df_dp, df_dp_fd, atol=1e-4) + df_dp_fd = + ( + begin + set_parameter_value(p, p_val + ε) + optimize!(model) + Δp * objective_value(model) + end - begin + set_parameter_value(p, p_val - ε) + optimize!(model) + Δp * objective_value(model) + end + ) / (2ε) + + @test isapprox(df_dp, df_dp_fd, atol = 1e-4) end function test_ObjectiveSensitivity_subset_parameters() # Model with 10 parameters, differentiate only w.r.t. 3rd and 7th From 8e618ead512c58724b86da252d45dfa22d44724b Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Sun, 15 Feb 2026 15:50:44 -0500 Subject: [PATCH 3/5] split fd test --- test/nlp_program.jl | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/test/nlp_program.jl b/test/nlp_program.jl index 34a41b8d..bb2bd4e2 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -760,18 +760,17 @@ function test_ObjectiveSensitivity_direct_param_contrib() @test isapprox(df_dp, 2 * p_val * Δp, atol = 1e-8) # ≈ 0.6 for p=3 ε = 1e-6 - df_dp_fd = - ( - begin - set_parameter_value(p, p_val + ε) - optimize!(model) - Δp * objective_value(model) - end - begin - set_parameter_value(p, p_val - ε) - optimize!(model) - Δp * objective_value(model) - end - ) / (2ε) + df_dp_fdpos = begin + set_parameter_value(p, p_val + ε) + optimize!(model) + Δp * objective_value(model) + end + df_dp_fdneg = begin + set_parameter_value(p, p_val - ε) + optimize!(model) + Δp * objective_value(model) + end + df_dp_fd = (df_dp_fdpos - df_dp_fdneg) / (2ϵ) @test isapprox(df_dp, df_dp_fd, atol = 1e-4) end From 971f34aeadf2750af71715aaeaa2aaf164aeaeda Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Sun, 15 Feb 2026 16:01:04 -0500 Subject: [PATCH 4/5] add math --- src/NonLinearProgram/nlp_utilities.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/NonLinearProgram/nlp_utilities.jl b/src/NonLinearProgram/nlp_utilities.jl index 8f3a79b1..12e639fa 100644 --- a/src/NonLinearProgram/nlp_utilities.jl +++ b/src/NonLinearProgram/nlp_utilities.jl @@ -490,10 +490,15 @@ function _compute_sensitivity(model::Model; tol = 1e-6) ∂s[((num_w+num_cons+num_lower+1):end), :] *= -_sense_multiplier grad = _compute_gradient(model) + # `grad` = [∇ₓf(x,p); ∇ₚf(x,p)] where `x` is the primal vars, `p` is the params, + # and `f(x,p)` is the objective function. we extract the components + # so we can form `∇ₚfᵒ(x,p) = ∇ₓf(x,p) * ∇ₚxᵒ(p) + ∇ₚf(x,p) * ∇ₚpᵒ(p)` + # where `ᵒ` denotes "optimal". note that parameters are fixed, so + # pᵒ(p) = p and ∇ₚpᵒ(p) = 𝐈ₚ. primal_idx = [i.value for i in model.cache.primal_vars] params_idx = [i.value for i in model.cache.params] - df_dx = grad[primal_idx] - df_dp_direct = grad[params_idx] - df_dp = df_dx'∂s[1:num_vars, :] + df_dp_direct' + df_dx = grad[primal_idx] # ∇ₓf(x,p) + df_dp_direct = grad[params_idx] # ∇ₚf(x,p) + df_dp = df_dx'∂s[1:num_vars, :] + df_dp_direct' # ∇ₚfᵒ(x,p) = ∇ₓf(x,p) * ∇ₚxᵒ(p) + ∇ₚf(x,p) * 𝐈ₚ return ∂s, df_dp end From 240869ec00f999503971f041ef2c20be9d6b8ded Mon Sep 17 00:00:00 2001 From: "Klamkin, Michael" Date: Sun, 15 Feb 2026 20:59:15 -0500 Subject: [PATCH 5/5] typo --- test/nlp_program.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/nlp_program.jl b/test/nlp_program.jl index bb2bd4e2..53fd9c12 100644 --- a/test/nlp_program.jl +++ b/test/nlp_program.jl @@ -770,7 +770,7 @@ function test_ObjectiveSensitivity_direct_param_contrib() optimize!(model) Δp * objective_value(model) end - df_dp_fd = (df_dp_fdpos - df_dp_fdneg) / (2ϵ) + df_dp_fd = (df_dp_fdpos - df_dp_fdneg) / (2ε) @test isapprox(df_dp, df_dp_fd, atol = 1e-4) end