Skip to content

Type Stability of IpoptProblem#532

Open
hfytr wants to merge 4 commits intojump-dev:masterfrom
hfytr:master
Open

Type Stability of IpoptProblem#532
hfytr wants to merge 4 commits intojump-dev:masterfrom
hfytr:master

Conversation

@hfytr
Copy link
Copy Markdown

@hfytr hfytr commented Apr 2, 2026

I have been trying to use JuliaC.jl with Ipopt.jl. This requires fixing #518. (JuliaC also doesn't work with #519).

@odow
Copy link
Copy Markdown
Member

odow commented Apr 2, 2026

The current suggestion is to use PackageCompiler, not JuliaC:

We test it with JuMP and Ipopt: https://github.com/jump-dev/JuMP.jl/blob/master/test/PackageCompiler/build_app.jl

There are a number of trade-offs to consider here, including whether it is worth making a breaking change.

@hfytr
Copy link
Copy Markdown
Author

hfytr commented Apr 2, 2026

Thanks @odow. I'd like to take advantage of juliac trimming capability. If I make the pr backward compatible can it be merged? Also, what is the breaking change?

@odow
Copy link
Copy Markdown
Member

odow commented Apr 2, 2026

I understand the motivation for wanting to use JuliaC, but proper support is likely years rather than months away.

Adding new type parameters can be breaking if anyone dispatched on Type{IpoptProblem}. There's also a cost to highly parameterised types in error messages and stacktraces and precompilation.

Ideally we'd also merge something like this only if there are also tests that run JuliaC so we can confirm it works.

I guess the fact that there are multiple open PRs shows that there's not a simple quick fix 😄

Comment thread src/C_wrapper.jl

mutable struct IpoptProblem
mutable struct IntermediateCallbackWrapper
f::Function
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this one okay? Is it just that you didn't test JuliaC with an intermediate callback?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This should be fixed in latest commit.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I still don't understand: why is the non-type-stable f::Function okay here, but it wasn't okay in IpoptProblem for eval_f::Function etc?

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 2, 2026

Codecov Report

❌ Patch coverage is 87.50000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 99.75%. Comparing base (1f6a40c) to head (a2fc4e1).

Files with missing lines Patch % Lines
src/C_wrapper.jl 87.50% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##            master     #532      +/-   ##
===========================================
- Coverage   100.00%   99.75%   -0.25%     
===========================================
  Files            5        5              
  Lines         1191     1200       +9     
===========================================
+ Hits          1191     1197       +6     
- Misses           0        3       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@odow
Copy link
Copy Markdown
Member

odow commented Apr 2, 2026

I'd need to double check, but I think the other consequence of this is that precompilation would now be specific to the model:

PrecompileTools.@setup_workload begin
PrecompileTools.@compile_workload begin
model = MOI.Utilities.CachingOptimizer(
MOI.Utilities.UniversalFallback(MOI.Utilities.Model{Float64}()),
MOI.instantiate(Optimizer; with_bridge_type = Float64),
)
# We don't want to advertise this option, but it's required so that
# we don't print the banner during precompilation.
MOI.set(model, MOI.RawOptimizerAttribute("sb"), "yes")
MOI.set(model, MOI.Silent(), true)
x = MOI.add_variables(model, 3)
MOI.supports(model, MOI.VariableName(), typeof(x[1]))
MOI.set(model, MOI.VariableName(), x[1], "x1")
MOI.set(model, MOI.VariablePrimalStart(), x[1], 0.0)
for F in (MOI.VariableIndex, MOI.ScalarAffineFunction{Float64})
MOI.supports_constraint(model, F, MOI.GreaterThan{Float64})
MOI.supports_constraint(model, F, MOI.LessThan{Float64})
MOI.supports_constraint(model, F, MOI.EqualTo{Float64})
# These return false, but it doesn't matter
MOI.supports_constraint(model, F, MOI.ZeroOne)
MOI.supports_constraint(model, F, MOI.Integer)
end
MOI.add_constraint(model, x[1], MOI.GreaterThan(0.0))
MOI.add_constraint(model, x[2], MOI.LessThan(0.0))
MOI.add_constraint(model, x[3], MOI.EqualTo(0.0))
f = 1.0 * x[1] + x[2] + x[3]
c1 = MOI.add_constraint(model, f, MOI.GreaterThan(0.0))
MOI.set(model, MOI.ConstraintName(), c1, "c1")
MOI.supports(model, MOI.ConstraintName(), typeof(c1))
MOI.add_constraint(model, f, MOI.LessThan(0.0))
MOI.add_constraint(model, f, MOI.EqualTo(0.0))
y, _ = MOI.add_constrained_variables(model, MOI.Nonnegatives(2))
MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE)
f = MOI.ScalarNonlinearFunction(
:+,
Any[MOI.ScalarNonlinearFunction(:sin, Any[x[i]]) for i in 1:3],
)
MOI.supports(model, MOI.ObjectiveFunction{typeof(f)}())
MOI.set(model, MOI.ObjectiveFunction{typeof(f)}(), f)
MOI.add_constraint(model, f, MOI.EqualTo(0.0))
MOI.optimize!(model)
MOI.get(model, MOI.TerminationStatus())
MOI.get(model, MOI.PrimalStatus())
MOI.get(model, MOI.DualStatus())
MOI.get(model, MOI.VariablePrimal(), x)
# We put these after `optimize!` so that the error is thrown on add,
# not on optimize!
try
MOI.add_constraint(model, x[1], MOI.ZeroOne())
catch
end
try
MOI.add_constraint(model, x[1], MOI.Integer())
catch
end
end
end

So this would hurt the startup times of all users not using JuliaC.

@odow
Copy link
Copy Markdown
Member

odow commented Apr 2, 2026

Precompilation doesn't seem to be affected:

Before

(tmp) pkg> st
Status `/private/tmp/Project.toml`
  [b6b21f68] Ipopt v1.14.1
  [4076af6c] JuMP v1.30.0

julia> using JuMP, Ipopt

julia> @time begin
           model = Model(Ipopt.Optimizer)
           set_silent(model)
           @variable(model, x)
           @variable(model, y)
           @objective(model, Min, (1 - x)^2 + 100 * (y - x^2)^2)
           optimize!(model)
           assert_is_solved_and_feasible(model)
       end

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

  2.261278 seconds (11.55 M allocations: 556.102 MiB, 6.35% gc time, 99.85% compilation time: 1% of which was recompilation)

After

(ip2) pkg> st
Status `/private/tmp/ip2/Project.toml`
  [b6b21f68] Ipopt v1.14.1 `~/git/jump-dev/Ipopt`
  [4076af6c] JuMP v1.30.0

julia> using JuMP, Ipopt

julia> @time begin
           model = Model(Ipopt.Optimizer)
           set_silent(model)
           @variable(model, x)
           @variable(model, y)
           @objective(model, Min, (1 - x)^2 + 100 * (y - x^2)^2)
           optimize!(model)
           assert_is_solved_and_feasible(model)
       end

******************************************************************************
This program contains Ipopt, a library for large-scale nonlinear optimization.
 Ipopt is released as open source code under the Eclipse Public License (EPL).
         For more information visit https://github.com/coin-or/Ipopt
******************************************************************************

  2.262386 seconds (11.55 M allocations: 556.119 MiB, 7.09% gc time, 99.85% compilation time: 1% of which was recompilation)

@odow
Copy link
Copy Markdown
Member

odow commented Apr 2, 2026

If we are to consider making a breaking change by adding type parameters, I wonder if we're better off doing something like:

abstract type AbstractOracle end

struct HS071 <: AbstractOracle end

function Ipopt.eval_f(::HS071, x::Vector{Float64})
    return x[1] * x[4] * (x[1] + x[2] + x[3]) + x[3]
end

Ipopt.eval_grad_f()
Ipopt.eval_g()
Ipopt.eval_jac_g()
Ipopt.has_eval_h(::HS071) = true
Ipopt.eval_h()

prob = Ipopt.CreateIpoptProblem(n, x_L, x_U, m, g_L, g_U, 8, 10, HS071())

struct IpoptProblem{O<:AbstractOracle}
    oracle::O
end

This would be much more "Julian" in its approach. It would be completely type stable. It avoids the intermediate callback issue, because you could have has_intermediate_cb(::HS071). The oracles are more testable in isolation, and it also solves #514 because the data could be part of the oracle.

@odow
Copy link
Copy Markdown
Member

odow commented Apr 3, 2026

Take a look at the alternative in #535.

There's now a merge conflict because I made some changes inspired by some of your diffs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants