diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index 5c82418..7d72ae0 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -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) @@ -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}, @@ -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( @@ -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( @@ -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 @@ -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( @@ -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) @@ -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}, @@ -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}, @@ -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( @@ -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( @@ -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 diff --git a/src/ParametricOptInterface.jl b/src/ParametricOptInterface.jl index 08d2936..de7f087 100644 --- a/src/ParametricOptInterface.jl +++ b/src/ParametricOptInterface.jl @@ -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 @@ -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 diff --git a/src/cubic_parser.jl b/src/cubic_parser.jl index 5b61109..93c108b 100644 --- a/src/cubic_parser.jl +++ b/src/cubic_parser.jl @@ -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). """ @@ -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. @@ -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. """ @@ -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). diff --git a/src/duals.jl b/src/duals.jl index c70c0db..b9444b7 100644 --- a/src/duals.jl +++ b/src/duals.jl @@ -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 @@ -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] @@ -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 = @@ -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 = @@ -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}, @@ -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) @@ -200,6 +260,13 @@ 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 @@ -207,6 +274,12 @@ function _is_additive(model::Optimizer, cp::MOI.ConstraintIndex) 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 = diff --git a/src/parametric_cubic_function.jl b/src/parametric_cubic_function.jl index 25669b3..d675990 100644 --- a/src/parametric_cubic_function.jl +++ b/src/parametric_cubic_function.jl @@ -130,12 +130,46 @@ function ParametricCubicFunction(parsed::_ParsedCubicExpression{T}) where {T} ) end -# Accessors for cubic terms by type (direct field access) +""" + cubic_affine_parameter_terms(f::ParametricCubicFunction) + +Return the affine parameter terms (`p`) of `f`. +""" cubic_affine_parameter_terms(f::ParametricCubicFunction) = f.p + +""" + cubic_parameter_variable_terms(f::ParametricCubicFunction) + +Return the parameter × variable quadratic terms (`pv`) of `f`. +""" cubic_parameter_variable_terms(f::ParametricCubicFunction) = f.pv + +""" + cubic_parameter_parameter_terms(f::ParametricCubicFunction) + +Return the parameter × parameter quadratic terms (`pp`) of `f`. +""" cubic_parameter_parameter_terms(f::ParametricCubicFunction) = f.pp + +""" + cubic_parameter_variable_variable_terms(f::ParametricCubicFunction) + +Return the parameter × variable × variable cubic terms (`pvv`) of `f`. +""" cubic_parameter_variable_variable_terms(f::ParametricCubicFunction) = f.pvv + +""" + cubic_parameter_parameter_variable_terms(f::ParametricCubicFunction) + +Return the parameter × parameter × variable cubic terms (`ppv`) of `f`. +""" cubic_parameter_parameter_variable_terms(f::ParametricCubicFunction) = f.ppv + +""" + cubic_parameter_parameter_parameter_terms(f::ParametricCubicFunction) + +Return the parameter × parameter × parameter cubic terms (`ppp`) of `f`. +""" cubic_parameter_parameter_parameter_terms(f::ParametricCubicFunction) = f.ppp """ diff --git a/src/parametric_functions.jl b/src/parametric_functions.jl index 4c90151..3634c38 100644 --- a/src/parametric_functions.jl +++ b/src/parametric_functions.jl @@ -5,6 +5,13 @@ abstract type ParametricFunction{T} end +""" + _cache_set_constant!(f, s) + +Store the bound constant of set `s` into `f.set_constant` for fast incremental +updates. No-op for set types that do not carry a scalar constant (e.g. `Interval` +is handled via a separate `_set_with_new_constant` path). +""" function _cache_set_constant!( f::ParametricFunction{T}, s::Union{MOI.LessThan{T},MOI.GreaterThan{T},MOI.EqualTo{T}}, @@ -20,6 +27,19 @@ function _cache_set_constant!( return end +""" + ParametricQuadraticFunction{T} + +Internal representation of a scalar quadratic function that may contain +parameters. Terms are split at construction time into: +- `pv`: parameter × variable quadratic terms (become linear after substitution) +- `pp`: parameter × parameter quadratic terms (become constant after substitution) +- `vv`: variable × variable quadratic terms (passed through unchanged) +- `p` / `v`: affine parameter / variable terms +- `affine_data`: precomputed per-variable sums of `p*v` coefficients (variables that appear in `pv`) +- `affine_data_np`: affine coefficients for variables that do not appear in any `pv` term +- `current_constant` / `current_terms_with_p`: cached values currently set in the inner solver +""" mutable struct ParametricQuadraticFunction{T} <: ParametricFunction{T} # helper to efficiently update affine terms affine_data::Dict{MOI.VariableIndex,T} @@ -88,26 +108,58 @@ function ParametricQuadraticFunction( ) end +""" + affine_parameter_terms(f) + +Return the affine terms of `f` whose variable index is a parameter. +""" function affine_parameter_terms(f::ParametricQuadraticFunction) return f.p end +""" + affine_variable_terms(f) + +Return the affine terms of `f` whose variable index is a true variable. +""" function affine_variable_terms(f::ParametricQuadraticFunction) return f.v end +""" + quadratic_parameter_variable_terms(f) + +Return the `p*v` quadratic terms of `f` (parameter is normalized to `variable_1`). +""" function quadratic_parameter_variable_terms(f::ParametricQuadraticFunction) return f.pv end +""" + quadratic_parameter_parameter_terms(f) + +Return the `p*p` quadratic terms of `f`. +""" function quadratic_parameter_parameter_terms(f::ParametricQuadraticFunction) return f.pp end +""" + quadratic_variable_variable_terms(f) + +Return the `v*v` quadratic terms of `f`. +""" function quadratic_variable_variable_terms(f::ParametricQuadraticFunction) return f.vv end +""" + _split_quadratic_terms(terms) + +Partition scalar quadratic terms into `(pv, pp, vv)` vectors where `p` indices +are parameters and `v` indices are variables. `pv` terms are normalized so the +parameter is always `variable_1`. +""" function _split_quadratic_terms( terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} @@ -144,6 +196,12 @@ function _split_quadratic_terms( return pv, pp, vv end +""" + _count_scalar_quadratic_terms_types(terms) + +Return `(num_vv, num_pp, num_pv)` counts for a vector of scalar quadratic terms. +Used to pre-allocate the output vectors of `_split_quadratic_terms`. +""" function _count_scalar_quadratic_terms_types( terms::Vector{MOI.ScalarQuadraticTerm{T}}, ) where {T} @@ -168,6 +226,12 @@ function _count_scalar_quadratic_terms_types( return num_vv, num_pp, num_pv end +""" + _original_function(f) + +Reconstruct the original MOI function from a parametric function, with +parameter indices treated as ordinary variable indices. +""" function _original_function(f::ParametricQuadraticFunction{T}) where {T} return MOI.ScalarQuadraticFunction{T}( vcat( @@ -180,6 +244,12 @@ function _original_function(f::ParametricQuadraticFunction{T}) where {T} ) end +""" + _current_function(f) + +Build the MOI function with all parameter values substituted and cached +affine coefficients applied. Used when setting the function in the inner solver. +""" function _current_function(f::ParametricQuadraticFunction{T}) where {T} affine = MOI.ScalarAffineTerm{T}[] sizehint!(affine, length(f.current_terms_with_p) + length(f.affine_data_np)) @@ -196,6 +266,12 @@ function _current_function(f::ParametricQuadraticFunction{T}) where {T} ) end +""" + _parametric_constant(model, f) + +Evaluate the scalar (or vector) constant of `f` after substituting the current +parameter values. Does not include the set constant. +""" function _parametric_constant( model, f::ParametricQuadraticFunction{T}, @@ -218,6 +294,12 @@ function _parametric_constant( return param_constant end +""" + _delta_parametric_constant(model, f) + +Compute the change in the parametric constant of `f` due to pending parameter +updates in `model.updated_parameters`. Returns zero when no parameters changed. +""" function _delta_parametric_constant( model, f::ParametricQuadraticFunction{T}, @@ -252,6 +334,12 @@ function _delta_parametric_constant( return delta_constant end +""" + _parametric_affine_terms(model, f) + +Return a dict mapping each variable to its total affine coefficient after +substituting current parameter values into all `p*v` quadratic terms. +""" function _parametric_affine_terms( model, f::ParametricQuadraticFunction{T}, @@ -271,6 +359,12 @@ function _parametric_affine_terms( return param_terms_dict end +""" + _delta_parametric_affine_terms(model, f) + +Return a dict of coefficient changes for each variable in `p*v` terms, based +on pending parameter updates. Empty dict when no parameters changed. +""" function _delta_parametric_affine_terms( model, f::ParametricQuadraticFunction{T}, @@ -290,12 +384,27 @@ function _delta_parametric_affine_terms( return delta_terms_dict end +""" + _update_cache!(f, model) + +Recompute and store `f.current_constant` (and `f.current_terms_with_p` for +quadratic types) from the current parameter values. Called when a constraint +is first added to the inner solver. +""" function _update_cache!(f::ParametricQuadraticFunction{T}, model) where {T} f.current_constant = _parametric_constant(model, f) f.current_terms_with_p = _parametric_affine_terms(model, f) return nothing end +""" + ParametricAffineFunction{T} + +Internal representation of a scalar affine function that may contain +parameters. Stores parameter terms (`p`) and variable terms (`v`) separately. +`current_constant` caches the evaluated constant (including parameter contributions) +currently set in the inner solver. +""" mutable struct ParametricAffineFunction{T} <: ParametricFunction{T} # constant * parameter p::Vector{MOI.ScalarAffineTerm{T}} @@ -336,6 +445,12 @@ function affine_variable_terms(f::ParametricAffineFunction) return f.v end +""" + _split_affine_terms(terms) + +Partition scalar affine terms into `(v, p)` where `v` are pure variable terms +and `p` are parameter terms. +""" function _split_affine_terms(terms::Vector{MOI.ScalarAffineTerm{T}}) where {T} num_v, num_p = _count_scalar_affine_terms_types(terms) v = Vector{MOI.ScalarAffineTerm{T}}(undef, num_v) @@ -354,6 +469,12 @@ function _split_affine_terms(terms::Vector{MOI.ScalarAffineTerm{T}}) where {T} return v, p end +""" + _count_scalar_affine_terms_types(terms) + +Return `(num_vars, num_params)` counts for a vector of scalar affine terms. +Used to pre-allocate the output vectors of `_split_affine_terms`. +""" function _count_scalar_affine_terms_types( terms::Vector{MOI.ScalarAffineTerm{T}}, ) where {T} @@ -413,6 +534,14 @@ function _update_cache!(f::ParametricAffineFunction{T}, model) where {T} return nothing end +""" + ParametricVectorAffineFunction{T} + +Internal representation of a vector affine function that may contain +parameters. Stores parameter terms (`p`) and variable terms (`v`) separately. +`current_constant` caches the evaluated constant vector currently set in the +inner solver. +""" mutable struct ParametricVectorAffineFunction{T} # constant * parameter p::Vector{MOI.VectorAffineTerm{T}} @@ -439,14 +568,30 @@ function ParametricVectorAffineFunction( ) end +""" + vector_affine_parameter_terms(f) + +Return the affine terms of `f` whose variable index is a parameter. +""" function vector_affine_parameter_terms(f::ParametricVectorAffineFunction) return f.p end +""" + vector_affine_variable_terms(f) + +Return the affine terms of `f` whose variable index is a true variable. +""" function vector_affine_variable_terms(f::ParametricVectorAffineFunction) return f.v end +""" + _split_vector_affine_terms(terms) + +Partition vector affine terms into `(v, p)` where `v` are pure variable terms +and `p` are parameter terms. +""" function _split_vector_affine_terms( terms::Vector{MOI.VectorAffineTerm{T}}, ) where {T} @@ -467,6 +612,12 @@ function _split_vector_affine_terms( return v, p end +""" + _count_vector_affine_terms_types(terms) + +Return `(num_vars, num_params)` counts for a vector of vector affine terms. +Used to pre-allocate the output vectors of `_split_vector_affine_terms`. +""" function _count_vector_affine_terms_types( terms::Vector{MOI.VectorAffineTerm{T}}, ) where {T} @@ -531,6 +682,14 @@ function _update_cache!(f::ParametricVectorAffineFunction{T}, model) where {T} return nothing end +""" + ParametricVectorQuadraticFunction{T} + +Internal representation of a vector quadratic function that may contain +parameters. Mirrors `ParametricQuadraticFunction` but for vector-valued +constraints; `affine_data` keys are `(variable, output_index)` tuples because +the same variable can appear in different output rows. +""" mutable struct ParametricVectorQuadraticFunction{T} # helper to efficiently update affine terms affine_data::Dict{Tuple{MOI.VariableIndex,Int},T} @@ -605,18 +764,33 @@ function ParametricVectorQuadraticFunction( ) end +""" + vector_quadratic_parameter_variable_terms(f) + +Return the `p*v` quadratic terms of `f` (parameter is normalized to `variable_1`). +""" function vector_quadratic_parameter_variable_terms( f::ParametricVectorQuadraticFunction, ) return f.pv end +""" + vector_quadratic_parameter_parameter_terms(f) + +Return the `p*p` quadratic terms of `f`. +""" function vector_quadratic_parameter_parameter_terms( f::ParametricVectorQuadraticFunction, ) return f.pp end +""" + vector_quadratic_variable_variable_terms(f) + +Return the `v*v` quadratic terms of `f`. +""" function vector_quadratic_variable_variable_terms( f::ParametricVectorQuadraticFunction, ) @@ -631,6 +805,12 @@ function vector_affine_variable_terms(f::ParametricVectorQuadraticFunction) return f.v end +""" + _split_vector_quadratic_terms(terms) + +Partition vector quadratic terms into `(pv, pp, vv)`. `pv` terms are +normalized so the parameter index is always `variable_1`. +""" function _split_vector_quadratic_terms( terms::Vector{MOI.VectorQuadraticTerm{T}}, ) where {T} diff --git a/src/update_parameters.jl b/src/update_parameters.jl index 2188add..4764364 100644 --- a/src/update_parameters.jl +++ b/src/update_parameters.jl @@ -3,6 +3,12 @@ # 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. +""" + _set_with_new_constant(s, val) + +Return a new set of the same type as `s` with its bound(s) shifted by `-val`. +Used to absorb the evaluated parametric constant into the constraint's RHS. +""" function _set_with_new_constant(s::MOI.LessThan{T}, val::T) where {T} return MOI.LessThan{T}(s.upper - val) end @@ -19,9 +25,15 @@ function _set_with_new_constant(s::MOI.Interval{T}, val::T) where {T} return MOI.Interval{T}(s.lower - val, s.upper - val) end -# Affine -# change to use only inner_ci all around so tha tupdates are faster -# modifications should not be used any ways, afterall we have param all around +""" + _update_affine_constraints!(model) + +Push parameter value changes to all scalar affine constraints in the inner +optimizer by updating each constraint's set RHS incrementally. +Iterates over `(F, S)` type keys as a type-instability barrier. +""" +# change to use only inner_ci all around so that updates are faster +# modifications should not be used anyway, afterall we have param all around function _update_affine_constraints!(model::Optimizer) for (F, S) in keys(model.affine_constraint_cache.dict) affine_constraint_cache_inner = model.affine_constraint_cache[F, S] @@ -83,6 +95,12 @@ function _update_affine_constraints!( return end +""" + _update_vector_affine_constraints!(model) + +Push parameter value changes to all vector affine constraints in the inner +optimizer by updating each constraint's constant vector incrementally. +""" function _update_vector_affine_constraints!(model::Optimizer) for (F, S) in keys(model.vector_affine_constraint_cache.dict) vector_affine_constraint_cache_inner = @@ -116,6 +134,12 @@ function _update_vector_affine_constraints!( return end +""" + _update_quadratic_constraints!(model) + +Push parameter value changes to all quadratic constraints in the inner +optimizer, updating both the set RHS and linear coefficients from `p*v` terms. +""" function _update_quadratic_constraints!(model::Optimizer) for (F, S) in keys(model.quadratic_constraint_cache.dict) quadratic_constraint_cache_inner = @@ -134,6 +158,12 @@ function _update_quadratic_constraints!(model::Optimizer) return end +""" + _affine_build_change_and_up_param_func(pf, delta_terms) + +Apply `delta_terms` to `pf.current_terms_with_p` and return a vector of MOI +coefficient-change objects ready to pass to `MOI.modify`. Mutates `pf`. +""" function _affine_build_change_and_up_param_func( pf::ParametricQuadraticFunction{T}, delta_terms, @@ -225,6 +255,12 @@ function _update_quadratic_constraints!( return end +""" + _update_affine_objective!(model) + +Update the constant term of the affine objective in the inner optimizer to +reflect the current parameter values. +""" function _update_affine_objective!(model::Optimizer{T}) where {T} if model.affine_objective_cache === nothing return @@ -243,6 +279,12 @@ function _update_affine_objective!(model::Optimizer{T}) where {T} return end +""" + _update_quadratic_objective!(model) + +Update the constant term and linear coefficients of the quadratic objective +in the inner optimizer to reflect the current parameter values. +""" function _update_quadratic_objective!(model::Optimizer{T}) where {T} if model.quadratic_objective_cache === nothing return @@ -267,6 +309,17 @@ function _update_quadratic_objective!(model::Optimizer{T}) where {T} return end +""" + update_parameters!(model::Optimizer) + +Propagate all pending parameter value changes to the inner optimizer and +commit them. After this call, `model.parameters` reflects the new values and +`model.updated_parameters` is reset to `NaN` (indicating no pending update). + +Must be called after modifying parameter values via +`MOI.set(model, MOI.ConstraintSet(), cp, MOI.Parameter(new_value))` and +before the next `MOI.optimize!` for the changes to take effect. +""" function update_parameters!(model::Optimizer) _update_affine_constraints!(model) _update_vector_affine_constraints!(model) @@ -288,6 +341,12 @@ function update_parameters!(model::Optimizer) return end +""" + _update_vector_quadratic_constraints!(model) + +Push parameter value changes to all vector quadratic constraints in the inner +optimizer, updating both the constant vector and linear coefficients. +""" function _update_vector_quadratic_constraints!(model::Optimizer) for (F, S) in keys(model.vector_quadratic_constraint_cache.dict) vector_quadratic_constraint_cache_inner =