Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,27 @@
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

_is_variable(v::MOI.VariableIndex) = !_is_parameter(v)
"""
_is_parameter(v::MOI.VariableIndex)::Bool

Return `true` if `v` encodes a parameter (index above `PARAMETER_INDEX_THRESHOLD`).
"""
function _is_parameter(v::MOI.VariableIndex)
return PARAMETER_INDEX_THRESHOLD < v.value <= PARAMETER_INDEX_THRESHOLD_MAX
end

"""
_is_variable(v::MOI.VariableIndex)::Bool

Return `true` if `v` is a true decision variable (not a parameter).
"""
_is_variable(v::MOI.VariableIndex) = !_is_parameter(v)

"""
_has_parameters(f)::Bool

Return `true` if any term in `f` contains a parameter index.
"""
function _has_parameters(f::MOI.ScalarAffineFunction{T}) where {T}
for term in f.terms
if _is_parameter(term.variable)
Expand Down Expand Up @@ -67,6 +82,13 @@ function _has_parameters(f::MOI.VectorQuadraticFunction)
return false
end

"""
_cache_multiplicative_params!(model, f)

Record all parameters that appear in `p*v` (or higher) product terms into
`model.multiplicative_parameters_pv`. This is used to reject dual queries for
parameters whose dual is not well-defined.
"""
function _cache_multiplicative_params!(
model::Optimizer{T},
f::ParametricQuadraticFunction{T},
Expand Down Expand Up @@ -265,6 +287,13 @@ function MOI.get(model::Optimizer, tp::Type{MOI.VariableIndex}, attr::String)
return MOI.get(model.optimizer, tp, attr)
end

"""
_add_variable(model, inner_vi)

Validate that `inner_vi` is not a parameter index and return it unchanged.
Raises an error if the inner solver accidentally created a variable index in
the parameter range.
"""
function _add_variable(model::Optimizer, inner_vi)
if _is_parameter(inner_vi)
error(
Expand Down Expand Up @@ -307,6 +336,11 @@ function MOI.supports_add_constrained_variables(
return MOI.supports_add_constrained_variables(model.optimizer, S)
end

"""
_assert_parameter_is_finite(set::MOI.Parameter)

Throw an `AssertionError` if `set.value` is not finite.
"""
function _assert_parameter_is_finite(set::MOI.Parameter{T}) where {T}
if !isfinite(set.value)
throw(
Expand Down Expand Up @@ -356,6 +390,12 @@ function MOI.add_constrained_variables(
return _add_variable.(model, inner_vis), inner_ci
end

"""
_add_to_constraint_map!(model, ci)

Register `ci` in `model.constraint_outer_to_inner` (mapping outer to inner
index). Specialized methods also increment the affine or quadratic counter.
"""
function _add_to_constraint_map!(model::Optimizer, ci)
model.constraint_outer_to_inner[ci] = ci
return
Expand Down Expand Up @@ -472,6 +512,13 @@ function MOI.delete(model::Optimizer, v::MOI.VariableIndex)
return
end

"""
_delete_variable_index_constraint(model, d, F, S, v)

Remove stale entries from constraint dict `d` after variable `v` is deleted.
Specialized for `VectorOfVariables` (prunes invalidated constraints) and
`VariableIndex` (removes the entry keyed by `v`). The default method is a no-op.
"""
_delete_variable_index_constraint(model, d, F, S, v) = nothing

function _delete_variable_index_constraint(
Expand Down Expand Up @@ -843,6 +890,12 @@ function MOI.modify(
return
end

"""
_add_constraint_direct_and_cache_map!(model, f, set)

Add constraint `(f, set)` directly to the inner optimizer and register it in
`model.constraint_outer_to_inner`. Used for constraints with no parameters.
"""
function _add_constraint_direct_and_cache_map!(model::Optimizer, f, set)
ci = MOI.add_constraint(model.optimizer, f, set)
_add_to_constraint_map!(model, ci)
Expand All @@ -862,6 +915,13 @@ function MOI.add_constraint(
return _add_constraint_direct_and_cache_map!(model, f, set)
end

"""
_add_constraint_with_parameters_on_function(model, f, set)

Add a constraint whose function `f` contains at least one parameter.
Constructs the appropriate `Parametric*Function`, caches it, and registers the
outer-to-inner constraint index mapping. Dispatches on `f` type.
"""
function _add_constraint_with_parameters_on_function(
model::Optimizer,
f::MOI.ScalarAffineFunction{T},
Expand Down Expand Up @@ -913,6 +973,13 @@ function MOI.add_constraint(
return outer_ci
end

"""
_add_vi_constraint(model, pf, set)

Add a parametric affine constraint as a variable bound in the inner optimizer
(i.e. `VariableIndex`-in-set form). Used when `ConstraintsInterpretation` allows
converting single-variable constraints with a unit coefficient into bounds.
"""
function _add_vi_constraint(
model::Optimizer,
pf::ParametricAffineFunction{T},
Expand Down Expand Up @@ -1026,6 +1093,11 @@ function _add_constraint_with_parameters_on_function(
return outer_ci
end

"""
_is_affine(f::MOI.ScalarQuadraticFunction)::Bool

Return `true` if `f` has no quadratic terms (i.e., reduces to an affine function).
"""
_is_affine(f::MOI.ScalarQuadraticFunction) = isempty(f.quadratic_terms)

function MOI.add_constraint(
Expand Down Expand Up @@ -1062,6 +1134,11 @@ function MOI.add_constraint(
return _add_constraint_with_parameters_on_function(model, f, set)
end

"""
_is_vector_affine(f::MOI.VectorQuadraticFunction)::Bool

Return `true` if `f` has no quadratic terms (i.e., reduces to a vector affine function).
"""
_is_vector_affine(f::MOI.VectorQuadraticFunction) = isempty(f.quadratic_terms)

function _add_constraint_with_parameters_on_function(
Expand Down Expand Up @@ -1283,6 +1360,12 @@ function MOI.get(model::Optimizer, attr::MOI.ObjectiveFunction)
return MOI.get(model.original_objective_cache, attr)
end

"""
_empty_objective_function_caches!(model::Optimizer)

Clear all cached objective function data and reset `original_objective_cache`.
Called before setting a new objective to avoid stale cached terms.
"""
function _empty_objective_function_caches!(model::Optimizer{T}) where {T}
model.affine_objective_cache = nothing
model.quadratic_objective_cache = nothing
Expand Down
24 changes: 24 additions & 0 deletions src/ParametricOptInterface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,33 @@ struct ParameterIndex
index::Int64
end

"""
p_idx(vi::MOI.VariableIndex)::ParameterIndex

Convert a `VariableIndex` that represents a parameter into a `ParameterIndex`
by stripping the `PARAMETER_INDEX_THRESHOLD` offset.
"""
function p_idx(vi::MOI.VariableIndex)::ParameterIndex
return ParameterIndex(vi.value - PARAMETER_INDEX_THRESHOLD)
end

"""
v_idx(pi::ParameterIndex)::MOI.VariableIndex

Convert a `ParameterIndex` back into a `VariableIndex` by adding the
`PARAMETER_INDEX_THRESHOLD` offset.
"""
function v_idx(pi::ParameterIndex)::MOI.VariableIndex
return MOI.VariableIndex(pi.index + PARAMETER_INDEX_THRESHOLD)
end

"""
p_val(vi::MOI.VariableIndex)::Int64
p_val(ci::MOI.ConstraintIndex)::Int64

Return the integer parameter index (1-based position in `model.parameters`)
corresponding to the given variable or constraint index.
"""
function p_val(vi::MOI.VariableIndex)::Int64
return vi.value - PARAMETER_INDEX_THRESHOLD
end
Expand Down Expand Up @@ -299,6 +318,11 @@ function Optimizer{T}(
return Optimizer{T}(inner; kwargs...)
end

"""
_parameter_in_model(model, v)

Return `true` if `v` is a parameter index and that parameter exists in `model`.
"""
function _parameter_in_model(model::Optimizer, v::MOI.VariableIndex)
return _is_parameter(v) && haskey(model.parameters, p_idx(v))
end
Expand Down
8 changes: 4 additions & 4 deletions src/cubic_parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function _Monomial{T}(coefficient::T, var::MOI.VariableIndex) where {T}
end

"""
_monomial_degree(m::_Monomial) -> Int
_monomial_degree(m::_Monomial)::Int

Total degree of a monomial (number of variable/parameter factors).
"""
Expand Down Expand Up @@ -72,7 +72,7 @@ struct _ParsedCubicExpression{T}
end

"""
_expand_to_monomials(arg, ::Type{T}) where {T} -> Union{Vector{_Monomial{T}}, Nothing}
_expand_to_monomials(arg, ::Type{T}) where {T}::Union{Vector{_Monomial{T}}, Nothing}

Expand an expression argument to a list of monomials.
Returns `nothing` if the expression is not a valid polynomial.
Expand Down Expand Up @@ -389,7 +389,7 @@ function _combine_like_monomials(monomials::Vector{_Monomial{T}}) where {T}
end

"""
_classify_monomial(m::_Monomial) -> Symbol
_classify_monomial(m::_Monomial)::Symbol

Classify a monomial by its structure.
"""
Expand Down Expand Up @@ -423,7 +423,7 @@ function _classify_monomial(m::_Monomial)
end

"""
_parse_cubic_expression(f::MOI.ScalarNonlinearFunction, ::Type{T}) where {T} -> Union{_ParsedCubicExpression{T}, Nothing}
_parse_cubic_expression(f::MOI.ScalarNonlinearFunction, ::Type{T}) where {T}::Union{_ParsedCubicExpression{T}, Nothing}

Parse a ScalarNonlinearFunction and return a _ParsedCubicExpression if it represents
a valid cubic polynomial (with parameters multiplying at most quadratic variable terms).
Expand Down
73 changes: 73 additions & 0 deletions src/duals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
# Use of this source code is governed by an MIT-style license that can be found
# in the LICENSE.md file or at https://opensource.org/licenses/MIT.

"""
_compute_dual_of_parameters!(model::Optimizer)

Populate `model.dual_value_of_parameters` with the sensitivity of the optimal
objective with respect to each parameter.

The dual of parameter `p` is computed as `∂obj*/∂p`, accumulated from:
- Each constraint containing `p`: `dual_λ * ∂f/∂p` (negated, since the
parameter appears in the constraint's RHS shift)
- The parametric objective: `±∂obj/∂p` (sign depends on `MIN_SENSE`)

For `pp` quadratic terms the product rule gives two symmetric contributions;
diagonal terms (`p_i == p_j`) are halved to avoid double-counting.
"""
function _compute_dual_of_parameters!(model::Optimizer{T}) where {T}
n = model.number_of_parameters_in_model
if length(model.dual_value_of_parameters) != n
Expand All @@ -26,6 +40,14 @@ function _compute_dual_of_parameters!(model::Optimizer{T}) where {T}
return
end

"""
_update_duals_from_affine_constraints!(model::Optimizer)

Iterate over all scalar affine constraint types and accumulate parameter dual
contributions from each into `model.dual_value_of_parameters`.
The inner-dict call is a type-instability barrier so Julia specializes
`_compute_parameters_in_ci!` on the concrete `(F, S)` types.
"""
function _update_duals_from_affine_constraints!(model::Optimizer)
for (F, S) in keys(model.affine_constraint_cache.dict)
affine_constraint_cache_inner = model.affine_constraint_cache[F, S]
Expand All @@ -35,6 +57,12 @@ function _update_duals_from_affine_constraints!(model::Optimizer)
return
end

"""
_update_duals_from_vector_affine_constraints!(model::Optimizer)

Iterate over all vector affine constraint types and accumulate parameter dual
contributions from each into `model.dual_value_of_parameters`.
"""
function _update_duals_from_vector_affine_constraints!(model::Optimizer)
for (F, S) in keys(model.vector_affine_constraint_cache.dict)
vector_affine_constraint_cache_inner =
Expand All @@ -45,6 +73,12 @@ function _update_duals_from_vector_affine_constraints!(model::Optimizer)
return
end

"""
_update_duals_from_quadratic_constraints!(model::Optimizer)

Iterate over all scalar quadratic constraint types and accumulate parameter
dual contributions from each into `model.dual_value_of_parameters`.
"""
function _update_duals_from_quadratic_constraints!(model::Optimizer)
for (F, S) in keys(model.quadratic_constraint_cache.dict)
quadratic_constraint_cache_inner =
Expand All @@ -55,6 +89,22 @@ function _update_duals_from_quadratic_constraints!(model::Optimizer)
return
end

"""
_compute_parameters_in_ci!(model, constraint_cache_inner)
_compute_parameters_in_ci!(model, pf, ci)

Accumulate the dual contribution of parameters appearing in constraint `ci`
into `model.dual_value_of_parameters`.

For affine terms the contribution is `-λ * c` where `λ` is the constraint
dual and `c` is the parameter coefficient. For `pp` quadratic terms the product
rule applies: each parameter receives `-λ * c * value_of_other_parameter`;
diagonal terms (`p_i == p_j`) are halved to avoid double-counting the
symmetric representation.

The `DoubleDictInner` overload is a function barrier; the concrete `pf`
overloads are where the computation happens.
"""
function _compute_parameters_in_ci!(
model::Optimizer,
constraint_cache_inner::DoubleDicts.DoubleDictInner{F,S,V},
Expand Down Expand Up @@ -114,6 +164,16 @@ function _compute_parameters_in_ci!(
return
end

"""
_update_duals_from_objective!(model, pf)

Accumulate the sensitivity of the parametric objective with respect to each
parameter into `model.dual_value_of_parameters`.

The sign convention matches the objective sense: `+` for minimization, `-` for
maximization. Specialized methods exist for `ParametricQuadraticFunction` and
`ParametricCubicFunction` to handle higher-order terms via the product rule.
"""
function _update_duals_from_objective!(model::Optimizer{T}, pf) where {T}
is_min = MOI.get(model.optimizer, MOI.ObjectiveSense()) == MOI.MIN_SENSE
for param in affine_parameter_terms(pf)
Expand Down Expand Up @@ -200,13 +260,26 @@ function MOI.get(
return model.dual_value_of_parameters[p_val(cp)]
end

"""
_is_additive(model, cp)

Return `true` if parameter `cp` appears only in additive (affine/constant)
positions. Returns `false` if it appears in any `p*v` product term, in which
case its dual is not well-defined and `ConstraintDual` will error.
"""
function _is_additive(model::Optimizer, cp::MOI.ConstraintIndex)
if cp.value in model.multiplicative_parameters_pv
return false
end
return true
end

"""
_update_duals_from_vector_quadratic_constraints!(model::Optimizer)

Iterate over all vector quadratic constraint types and accumulate parameter
dual contributions from each into `model.dual_value_of_parameters`.
"""
function _update_duals_from_vector_quadratic_constraints!(model::Optimizer)
for (F, S) in keys(model.vector_quadratic_constraint_cache.dict)
vector_quadratic_constraint_cache_inner =
Expand Down
Loading
Loading