diff --git a/ortools/julia/ORTools.jl/Project.toml b/ortools/julia/ORTools.jl/Project.toml index 72e75523f8e..e5bd7aebfdf 100644 --- a/ortools/julia/ORTools.jl/Project.toml +++ b/ortools/julia/ORTools.jl/Project.toml @@ -1,6 +1,6 @@ name = "ORTools" uuid = "b7d69b34-a827-4671-8cfa-f7e1eec930c7" -version = "0.0.1" +version = "0.1.0" [deps] MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" @@ -19,8 +19,8 @@ ORToolsBinariesExt = "ORToolsBinaries" [compat] julia = "1.9" ORTools_jll = "9.14.0" -ORToolsBinaries = "0.0.1" -ORToolsGenerated = "0.0.2" +ORToolsBinaries = "0.1.0" +ORToolsGenerated = "0.1.0" ProtoBuf = "1.0.15" MathOptInterface = "1.42.0" diff --git a/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl b/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl index 161f0a41d7b..26ed2de0289 100644 --- a/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl +++ b/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl @@ -13,6 +13,9 @@ mutable struct CPSATOptimizer <: MOI.AbstractOptimizer # Set of constraints in variable indices variables_constraints::Set{MOI.ConstraintIndex} linear_expressions_constraints::Set{MOI.ConstraintIndex} + boolean_argument_constraints::Set{MOI.ConstraintIndex} + linear_argument_constraints::Set{MOI.ConstraintIndex} + global_constraints::Set{MOI.ConstraintIndex} constraint_types_present::Set{Tuple{Type,Type}} # This structure is updated by the optimize! function. solve_response::Union{Nothing,CpSolverResponse} @@ -22,9 +25,22 @@ mutable struct CPSATOptimizer <: MOI.AbstractOptimizer parameters = SatParameters() variables_constraints = Set{MOI.ConstraintIndex}() linear_expressions_constraints = Set{MOI.ConstraintIndex}() + boolean_argument_constraints = Set{MOI.ConstraintIndex}() + linear_argument_constraints = Set{MOI.ConstraintIndex}() + global_constraints = Set{MOI.ConstraintIndex}() constraint_types_present = Set{Tuple{Type,Type}}() - new(model, parameters, nothing) + new( + model, + parameters, + variables_constraints, + linear_expressions_constraints, + boolean_argument_constraints, + linear_argument_constraints, + global_constraints, + constraint_types_present, + nothing, + ) end end @@ -41,6 +57,9 @@ function MOI.empty!(optimizer::CPSATOptimizer) optimizer.model = CpModel() optimizer.variables_constraints = Set{MOI.ConstraintIndex}() optimizer.linear_expressions_constraints = Set{MOI.ConstraintIndex}() + optimizer.boolean_argument_constraints = Set{MOI.ConstraintIndex}() + optimizer.linear_argument_constraints = Set{MOI.ConstraintIndex}() + optimizer.global_constraints = Set{MOI.ConstraintIndex}() optimizer.constraint_types_present = Set{Tuple{Type,Type}}() optimizer.solve_response = nothing @@ -258,7 +277,7 @@ function MOI.add_constraint( lower_bound, upper_bound = bounds(c) # TODO: (b/452908268) - Update variable's domain instead of creating a new linear constraint. - linear_constraint = CPSatLinearConstraintProto() + linear_constraint = NewCPSatLinearConstraint() push!(linear_constraint.vars, variable_index) # In this case, we set the coefficient to 1. @@ -268,8 +287,7 @@ function MOI.add_constraint( push!(linear_constraint.domain, upper_bound) constraint = CPSATConstraint() - constraint.name = :linear - constraint.value = linear_constraint + constraint.constraint = (; linear = linear_constraint) push!(optimizer.model.constraints, constraint) @@ -293,15 +311,15 @@ function throw_if_upper_bound_is_already_set( interval_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}(vi.value) equal_to_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}}(vi.value) - if in(less_than_idx, optimizer.variables_constraints) + if in(less_than_idx, optimizer.variables_constraints) && S <: MOI.LessThan throw(MOI.UpperBoundAlreadySet{MOI.LessThan{T},S}(vi)) end - if in(interval_idx, optimizer.variables_constraints) + if in(interval_idx, optimizer.variables_constraints) && S <: MOI.Interval throw(MOI.UpperBoundAlreadySet{MOI.Interval{T},S}(vi)) end - if in(equal_to_idx, optimizer.variables_constraints) + if in(equal_to_idx, optimizer.variables_constraints) && S <: MOI.EqualTo throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{T},S}(vi)) end @@ -319,15 +337,15 @@ function throw_if_lower_bound_is_already_set( interval_idx = MOI.ConstraintIndex{typeof(vi),MOI.Interval{T}}(vi.value) equal_to_idx = MOI.ConstraintIndex{typeof(vi),MOI.EqualTo{T}}(vi.value) - if in(greater_than_idx, optimizer.variables_constraints) + if in(greater_than_idx, optimizer.variables_constraints) && S <: MOI.GreaterThan throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{T},S}(vi)) end - if in(interval_idx, optimizer.variables_constraints) + if in(interval_idx, optimizer.variables_constraints) && S <: MOI.Interval throw(MOI.LowerBoundAlreadySet{MOI.Interval{T},S}(vi)) end - if in(equal_to_idx, optimizer.variables_constraints) + if in(equal_to_idx, optimizer.variables_constraints) && S <: MOI.EqualTo throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{T},S}(vi)) end @@ -406,21 +424,20 @@ function MOI.add_constraint( constraint_index = length(optimizer.model.constraints) + 1 lower_bound, upper_bound = bounds(c) - linear_constraint = CPSatLinearConstraintProto() + linear_constraint = NewCPSatLinearConstraint() # Update the domain push!(linear_constraint.domain, lower_bound) push!(linear_constraint.domain, upper_bound) terms_pairs = get_terms_pairs(terms) - for (variable_index, coefficient) in term_pairs + for (variable_index, coefficient) in terms_pairs push!(linear_constraint.vars, variable_index) push!(linear_constraint.coeffs, coefficient) end constraint = CPSATConstraint() - constraint.name = :linear - constraint.value = linear_constraint + constraint.constraint = (; linear = linear_constraint) push!(optimizer.model.constraints, constraint) @@ -552,3 +569,1487 @@ function MOI.set( return nothing end + +""" + Abstract type for Boolean argument constraints. +""" +abstract type BoolArgumentConstraint <: MOI.AbstractVectorSet end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{BoolArgumentConstraint}, +) + return true +end + +""" + BoolOr(dimension::Int) + +The bool_or constraint forces at least one literal to be true. + +This constraint is a special case of `MOI.CountAtLeast`. It's equivalent to this: + + MOI.CountAtLeast(1, [d], Set([1])) +""" +struct BoolOr <: BoolArgumentConstraint + dimension::Int + + BoolOr(dimension::Int) = new(dimension) +end + +# The tag used to identify the constraint type in CPSAT's ConstraintProto. +constraint_tag(::BoolOr) = :bool_or + +""" + BoolAnd(dimension::Int) + +The bool_and constraint forces all of the literals to be true. +""" +struct BoolAnd <: BoolArgumentConstraint + dimension::Int + + BoolAnd(dimension::Int) = new(dimension) +end + +# The tag used to identify the constraint type in CPSAT's ConstraintProto. +constraint_tag(::BoolAnd) = :bool_and + +""" + BoolXor(dimension::Int) + +The bool_xor constraint forces an odd number of the literals to be true. +""" +struct BoolXor <: BoolArgumentConstraint + dimension::Int + + BoolXor(dimension::Int) = new(dimension) +end + +constraint_tag(::BoolXor) = :bool_xor + +""" + AtMostOne(dimension::Int) + +The at_most_one constraint enforces that no more than one literal is +true at the same time. +""" +struct AtMostOne <: BoolArgumentConstraint + dimension::Int + + AtMostOne(dimension::Int) = new(dimension) +end + +# The tag used to identify the constraint type in CPSAT's ConstraintProto. +constraint_tag(::AtMostOne) = :at_most_one + +""" + ExactlyOne(dimension::Int) + +The exactly_one constraint enforces that exactly one literal is +true at the same time. +""" +struct ExactlyOne <: BoolArgumentConstraint + dimension::Int + + ExactlyOne(dimension::Int) = new(dimension) +end + +# The tag used to identify the constraint type in CPSAT's ConstraintProto. +constraint_tag(::ExactlyOne) = :exactly_one + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::MOI.VectorOfVariables, + c::S, +) where {S<:BoolArgumentConstraint} + if c.dimension != length(vi.variables) + throw( + ArgumentError( + "The dimension of the constraint must match the number of variables.", + ), + ) + end + + constraint_index = length(optimizer.model.constraints) + 1 + + literals = Int32.(map(v -> v.value, vi.variables)) + bool_argument_constraint = BoolArgument(literals) + + constraint = CPSATConstraint() + constraint.constraint = NamedTuple{(constraint_tag(c),)}((bool_argument_constraint,)) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.boolean_argument_constraints, + MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(c)}(constraint_index), + ) + + return MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(c)}(constraint_index) +end + + +""" + Added support for the `LinearArgumentProto` constraint. +""" +abstract type LinearArgumentConstraint <: MOI.AbstractVectorSet end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{<:LinearArgumentConstraint}, +) + return true +end + +# A LinearExpression object can be derived from a ScalarAffineFunction or from +# a VariableIndex. +function build_linear_expression( + f::MOI.ScalarAffineFunction{T}, +)::CPSatLinearExpression where {T<:Int} + MOI.throw_if_scalar_and_constant_not_zero(f, typeof(c)) + + terms = f.terms + terms_pairs = get_terms_pairs(terms) + + linear_expression = CPSatLinearExpression() + + for (variable_index, coefficient) in terms_pairs + push!(linear_expression.vars, variable_index) + push!(linear_expression.coeffs, coefficient) + end + + linear_expression.offset = f.offset + + return linear_expression +end + +function build_linear_expression(vi::MOI.VariableIndex)::CPSatLinearExpression + linear_expression = CPSatLinearExpression() + push!(linear_expression.vars, vi.value) + push!(linear_expression.coeffs, 1) + + return linear_expression +end + +function build_linear_expressions(vi::MOI.VectorOfVariables)::Vector{CPSatLinearExpression} + return build_linear_expression.(vi.variables) +end + +function build_linear_expressions( + vi::MOI.VectorOfVariables, + start_index::Int, + end_index::Int, +)::Vector{CPSatLinearExpression} + return build_linear_expression.(vi.variables[start_index:end_index]) +end + +function build_linear_expressions( + vi::MOI.VectorAffineFunction, +)::Vector{CPSatLinearExpression} + return build_linear_expression.(MOI.scalar_function.(vi, 1:MOI.output_dimension(vi))) +end + +function build_linear_expressions( + vi::MOI.VectorAffineFunction, + start_index::Int, + end_index::Int, +)::Vector{CPSatLinearExpression} + return build_linear_expression.(MOI.scalar_function.(vi, start_index:end_index)) +end + +function get_variable_indices( + vi::MOI.VectorOfVariables, + start_index::Int, + end_index::Int, +)::Vector{Int32} + start_index > end_index && return Vector{Int32}() + return Int32.(map(v -> v.value, vi.variables[start_index:end_index])) +end + +""" + Get the variable indices from a VectorAffineFunction. + This checks if each of the scalar functions have a co-efficient of 1 and a constant of 0. + The number of terms in each scalar function is 1, for example 1*x. + + If any of the ScalarFunctions in the range is not of the form 1*x + 0, then an error is thrown. +""" +function get_variable_indices( + vi::MOI.VectorAffineFunction, + start_index::Int, + end_index::Int, +)::Vector{Int32} + variable_indices = Vector{Int32}(undef, end_index - start_index + 1) + + for i = start_index:end_index + if MOI.scalar_function(vi, i).constant != 0 || + length(MOI.scalar_function(vi, i).terms) != 1 || + MOI.scalar_function(vi, i).terms[1].coefficient != 1 + throw( + ArgumentError( + "The ScalarFunction at index $(i) is not of the form 1*x + 0.", + ), + ) + end + variable_indices[i-start_index+1] = + MOI.scalar_function(vi, i).terms[1].variable.value + end + + return variable_indices +end + +""" + DivisionEquality(dimension::Int) + +The DivisionEquality constraint forces the target to equal exprs[0] / exprs[1]. +""" +struct DivisionEquality <: LinearArgumentConstraint + dimension::Int + target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}} + + # The dimension is always set to 2. + DivisionEquality(target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}}) = + new(2, target) +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::DivisionEquality, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "A 2D vector function comprising of variables or linear/affine expressions is required for the DivisionEquality constraint.", + ), + ) + end + + target_expr = build_linear_expression(c.target) + expr_0, expr_1 = build_linear_expressions(vi) + + int_div_linear_argument = LinearArgument(target_expr, [expr_0, expr_1]) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; int_div = int_div_linear_argument) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.linear_argument_constraints, + MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(c)}(constraint_index), + ) + + return MOI.ConstraintIndex{MOI.VectorOfVariables,typeof(c)}(constraint_index) +end + +""" + ModuloEquality(dimension::Int, target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}}) + +The ModuloEquality constraint constraint forces the target to equal exprs[0] % exprs[1]. +""" +struct ModuloEquality <: LinearArgumentConstraint + dimension::Int + target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}} + + # The dimension is always set to 2. + ModuloEquality(target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}}) = + new(2, target) +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::ModuloEquality, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "A 2D vector function comprising of variables or linear/affine expressions is required for the ModuloEquality constraint.", + ), + ) + end + + target_expr = build_linear_expression(c.target) + expr_0, expr_1 = build_linear_expressions(vi) + + int_mod_linear_argument = LinearArgument(target_expr, [expr_0, expr_1]) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; int_mod = int_mod_linear_argument) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.linear_argument_constraints, + MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index) +end + +""" + ProductEquality(dimension::Int, target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}}) + +The ProductEquality constraint constraint forces the target to equal the product of all +variables. +""" +struct ProductEquality <: LinearArgumentConstraint + dimension::Int + target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}} + + ProductEquality( + dimenson::Int, + target::Union{MOI.VariableIndex,MOI.ScalarAffineFunction{Int}}, + ) = new(dimension, target) +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::ProductEquality, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "Number of variables in the ProductEquality constraint must match the dimension.", + ), + ) + end + + target_expr = build_linear_expression(c.target) + expressions = build_linear_expressions(vi) + + int_product_linear_argument = LinearArgument(target_expr, expressions) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; int_prod = int_product_linear_argument) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.linear_argument_constraints, + MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index) +end + +""" + LinMax(dimension::Int) + +The LinMax constraint constraint forces the target to equal the maximum of all +linear expressions. +""" +struct LinMax <: LinearArgumentConstraint + dimension::Int + + function LinMax(dimenson::Int) + dimension < 2 && throw(ArgumentError("Dimension must be at least 2.")) + new(dimension, target) + end +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::LinMax, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "Number of variables in the LinMax constraint must match the dimension.", + ), + ) + end + + expressions = build_linear_expressions(vi) + target_expr = expressions[1] + + lin_max_linear_argument = LinearArgument(target_expr, expressions[2:end]) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; lin_max = lin_max_linear_argument) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.linear_argument_constraints, + MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index) +end + + +""" + + Global constraints. + +""" +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}}, + ::Type{MOI.AllDifferent}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::MOI.AllDifferent, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "Number of variables in the AllDifferent constraint must match the dimension.", + ), + ) + end + + constraint_index = length(optimizer.model.constraints) + 1 + + linear_expressions = build_linear_expressions(vi) + all_different_constraint = AllDifferentConstraint(exprs = linear_expressions) + + constraint = CPSATConstraint() + constraint.constraint = (; all_diff = all_different_constraint) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index) +end + + +""" + Circuit(edge_set::Vector{Tuple{Int, Int}}) + +The dimension of the Circuit constraint is equal to the number of edges in the +edge_set. +""" +struct Circuit <: MOI.AbstractVectorSet + dimension::Int + edge_set::Vector{Tuple{Int,Int}} + + Circuit(edge_set::Vector{Tuple{Int,Int}}) = new(length(edge_set), edge_set) +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{Circuit}, +) + return true +end + +""" + The VectorOfVariables assumes that all the variables are Binary Variables. + Make sure to apply the MOI.ZeroOne constraint to the variables before adding + them to the VectorOfVariables. + + The length of the VectorOfVariables must match the dimension of the Circuit. + In short, it should be equal to the size of the edge_set in the Circuit. + + TODO: b/493237559 - Add support for MOI.VectorAffineFunction. + TODO: b/493240012 - Add check for MOI.ZeroOne constraint or verify that CP-Sat + errors if MOI.ZeroOne constraint is not specified for variables. +""" +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::MOI.VectorOfVariables, + c::Circuit, +) + if c.dimension != length(vi.variables) + throw( + ArgumentError( + "The dimension of the Circuit constraint must match the number of variables.", + ), + ) + end + + constraint_index = length(optimizer.model.constraints) + 1 + + circuit_constraint = NewCircuitConstraint() + + for i = 1:c.dimension + push!(circuit_constraint.heads, c.edge_set[i][1]) + push!(circuit_constraint.tails, c.edge_set[i][2]) + push!(circuit_constraint.literals, vi.variables[i].value) + end + + constraint = CPSATConstraint() + constraint.constraint = (; circuit = circuit_constraint) + + push!(optimizer.model.constraints, constraint) + + # Update the associated metadata. + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, typeof(c))) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{MOI.VectorOfVariables,Circuit}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),typeof(c)}(constraint_index) +end + +function supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{MOI.Circuit}, +) + return true +end + +""" + The parameter `vi` is a vector of Integer Variables upon which the Circuit + constraint is applied. + + In this case too, the dimension of the Circuit constraint must match the number + of variables in the vector of Integer Variables. It should be specifid as part + of initializing the Circuit object. +""" +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::MOI.VectorOfVariables, + c::MOI.Circuit, +) + if c.dimension != length(vi.variables) + throw( + ArgumentError( + "The dimension of the Circuit constraint must match the number of variables.", + ), + ) + end + + circuit_arc_literals = [] + edge_set = Vector{Tuple{Int,Int}}() + + for i = 1:c.dimension + # Outgoing edge literal for the current node `i`. + outgoing_edge_literals = [] + for j = 1:c.dimension + # Boolean variable to indicate if the arc `i` -> `j` is present in the circuit. + b_ij = MOI.add_constrained_variable(optimizer, MOI.ZeroOne())[1] + push!(outgoing_edge_literals, b_ij) + push!(circuit_arc_literals, b_ij) + push!(edge_set, (i, j)) + + # We add a reified constraint to ensure that if v[i] = j, then b_ij is true. vi.variables[i] + # NOTE: The variable is passed as a ScalarAffineFunction with a co-efficient 1. + # This is a work-around; given that the variable is subjected to multiple EqualTo constraints within the loop, + # dispatching the `add_constraint` to the same VariableIndex(i) will result in `throw_if_upper[lower]_bound_is_already_set` + # error. Working with ScalarAffineFunction makes the constraint index independent of the VariableIndex(i); it is unique for each constraint. + MOI.add_constraint(optimizer, 1 * vi.variables[i], MOI.EqualTo(j)) + # Update the added constraint's `enforcement literal` + push!(optimizer.model.constraints[end].enforcement_literal, b_ij.value) + end + # Only one outgoing edge is allowed. + # TODO: b/493245890 - Remove this constraint enforcement and a bridge between MOI.Circuit and CPSAT's Circuit constraint. + MOI.add_constraint( + optimizer, + MOI.VectorOfVariables(outgoing_edge_literals), + ExactlyOne(c.dimension), + ) + end + + circuit_arc_literals_vector = MOI.VectorOfVariables(circuit_arc_literals) + + return MOI.add_constraint(optimizer, circuit_arc_literals_vector, Circuit(edge_set)) +end + +struct Element <: MOI.AbstractVectorSet + dimension::Int + + Element(dimension::Int) = new(dimension) +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{Element}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vars::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::Element, +) + if c.dimension <= 2 + throw( + ArgumentError( + """ + The expression must have at least 3 variables. The expression to be indexed occupies the first n - 2 variables, where n is the dimension. + Then the index and target variables are the last 2 variables the first variable as the index and the second variable as the target. + """, + ), + ) + end + + if c.dimension != MOI.output_dimension(vars) + throw( + ArgumentError( + "The dimension of the Element constraint must match the number of variables.", + ), + ) + end + + exprs = build_linear_expressions(vars, 1, c.dimension - 2) + linear_index, linear_target = + build_linear_expressions(vars, (c.dimension - 2) + 1, c.dimension) + + element_constraint = ElementConstraint( + linear_index = linear_index, + linear_target = linear_target, + exprs = exprs, + ) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; element = element_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (typeof(exprs), Element)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{typeof(exprs),Element}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(exprs),Element}(constraint_index) +end + +struct Routes <: MOI.AbstractVectorSet + dimension::Int + tails::Vector{Int} + heads::Vector{Int} + # TODO: b/458704567 - Does not include Node Expressions. Will be included in Routing support. + + function Routes(tails::Vector{Int}, heads::Vector{Int}) + if length(tails) != length(heads) + throw(ArgumentError("The number of tails and heads must match.")) + end + new(length(tails), tails, heads) + end +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{Routes}, +) + return true +end + + +""" + The VectorOfVariables should be a vector of Boolean/Binary Variables. +""" +function MOI.add_constraint(optimizer::CPSATOptimizer, vi::MOI.VectorOfVariables, c::Routes) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "The dimension of the Routes constraint must match the number of variables.", + ), + ) + end + + variable_indices = map(v -> v.value, vi.variables) + + routes_constraint = + RoutesConstraint(tails = c.tails, heads = c.heads, literals = variable_indices) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; routes = routes_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, Routes)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{MOI.VectorOfVariables,Routes}(constraint_index), + ) + + return MOI.ConstraintIndex{MOI.VectorOfVariables,Routes}(constraint_index) +end + + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}}, + ::Type{MOI.Table}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::MOI.Table, +) + # Flatten the table to a vector that's Row-Major first. + flattened_table = collect(c.table'[:]) + linear_expressions = build_linear_expressions(vi) + + table_constraint = TableConstraint(values = flattened_table, exprs = linear_expressions) + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; table = table_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (typeof(vi), MOI.Table)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{typeof(vi),MOI.Table}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),MOI.Table}(constraint_index) +end + +""" + Table(table::Matrix{Int}, negated::Bool) + + Contains support for the `negated` field. +""" +struct Table <: MOI.AbstractVectorSet + dimension::Int + table::AbstractMatrix{Int} + negated::Bool + + function Table(table::AbstractMatrix{Int}; negated::Bool = false) + if ndims(table) != 2 + throw(ArgumentError("The table must be a 2D matrix.")) + end + new(size(table)[2], table, negated) + end +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}}, + ::Type{Table}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::Table, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "The number of columns of the Table constraint must match the number of variables.", + ), + ) + end + + # Flatten the table to a vector that's Row-Major first. + flattened_table = collect(c.table'[:]) + linear_expressions = build_linear_expressions(vi) + table_constraint = TableConstraint( + values = flattened_table, + exprs = linear_expressions, + negated = c.negated, + ) + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; table = table_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (typeof(vi), Table)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{typeof(vi),Table}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),Table}(constraint_index) +end + +""" + The `dimension` has to be defined as part of the Automaton object. + It is the length of the input vector of variables. +""" +struct Automaton <: MOI.AbstractVectorSet + dimension::Int + starting_state::Int + final_states::Vector{Int} + + # State transition matrix. + transitions::AbstractMatrix{Int} + + function Automaton( + dimension::Int, + starting_state::Int, + final_states::Vector{Int}, + transitions::AbstractMatrix{Int}, + ) + if ndims(transitions) != 2 + throw(ArgumentError("The transitions matrix must be a 2D matrix.")) + end + + if size(transitions)[2] != 3 + throw( + ArgumentError( + "The transitions matrix must have 3 columns [from_state, to_state, token_value]", + ), + ) + end + + if isempty(final_states) + throw(ArgumentError("The final states must not be empty.")) + end + + new(dimension, starting_state, final_states, transitions) + end +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}}, + ::Type{Automaton}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::Automaton, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "The dimension of the Automaton constraint must match the number of variables.", + ), + ) + end + + automaton_constraint = AutomatonConstraint( + starting_state = c.starting_state, + final_states = c.final_states, + transition_tail = c.transitions[:, 1], + transition_head = c.transitions[:, 2], + transition_label = c.transitions[:, 3], + exprs = build_linear_expressions(vi), + ) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; automaton = automaton_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (typeof(vi), Automaton)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{typeof(vi),Automaton}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),Automaton}(constraint_index) +end + +""" + The `dimension` is the number of variables in the inverse function. + This number should be the same as the dimension of the VectorOfVariables + upon which the Inverse constraint is applied. +""" +struct Inverse <: MOI.AbstractVectorSet + dimension::Int + + function Inverse(dimension::Int) + new(dimension) + end +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VectorOfVariables}, + ::Type{Inverse}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::MOI.VectorOfVariables, + c::Inverse, +) + if c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + "The dimension of the Inverse constraint must match the number of variables.", + ), + ) + end + + if c.dimension % 2 == 1 + throw( + ArgumentError( + """The dimension of the Inverse constraint must be even; the first half should contain the direct function (variables) and + the second half the inverse function (variables). + """, + ), + ) + end + + inverse_constraint = InverseConstraint() + var_size = Int(c.dimension / 2) + f_direct_variable_indices = Vector{Int}(undef, var_size) + f_inverse_variable_indices = Vector{Int}(undef, var_size) + + for i = 1:var_size + f_direct_variable_indices[i] = vi.variables[i].value + f_inverse_variable_indices[i] = vi.variables[i+var_size].value + end + + append!(inverse_constraint.f_direct, f_direct_variable_indices) + append!(inverse_constraint.f_inverse, f_inverse_variable_indices) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; inverse = inverse_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (MOI.VectorOfVariables, Inverse)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{MOI.VectorOfVariables,Inverse}(constraint_index), + ) + + return MOI.ConstraintIndex{MOI.VectorOfVariables,Inverse}(constraint_index) +end + +""" + The `dimension` is the number of variables in the reservoir function. + This number should be the same as the dimension of the VectorOfVariables + upon which the Reservoir constraint is applied. + + When the `active_literals` is empty, the number of variables must be + 2 * dimension in order to include active literals. + + When the `active_literals` is not empty, the number of variables must be + 3 * dimension in order to include active literals. + + TODO: b/493267118 - split constraint into 2; one with active literals and one without. +""" +struct Reservoir <: MOI.AbstractVectorSet + dimension::Int + min_level::Int + max_level::Int + + function Reservoir(dimension::Int, min_level::Int, max_level::Int) + new(dimension, min_level, max_level) + end +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}}, + ::Type{Reservoir}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + c::Reservoir, +) + if 2 * c.dimension != MOI.output_dimension(vi) + if 3 * c.dimension != MOI.output_dimension(vi) + throw( + ArgumentError( + """ + The number of variables must be 2 * dimension = $(2 * c.dimension) or + 3 * dimension = $(3 * c.dimension) in order to include active literals. + The length of the variables passed is $(MOI.output_dimension(vi)). + This does not meet any of the required conditions. + """, + ), + ) + end + end + + exprs = build_linear_expressions(vi, 1, 2 * c.dimension) + + time_exprs = exprs[1:c.dimension] + level_changes = exprs[(c.dimension+1):(2*c.dimension)] + + active_literals = [] + + # Contains active literals. + if 3 * c.dimension == MOI.output_dimension(vi) + try + active_literals = get_variable_indices(vi, 2 * c.dimension + 1, 3 * c.dimension) + catch error + throw(error) + end + end + + reservoir_constraint = ReservoirConstraint( + min_level = c.min_level, + max_level = c.max_level, + time_exprs = time_exprs, + level_changes = level_changes, + active_literals = active_literals, + ) + + constraint_index = length(optimizer.model.constraints) + 1 + + constraint = CPSATConstraint() + constraint.constraint = (; reservoir = reservoir_constraint) + + push!(optimizer.model.constraints, constraint) + + push!(optimizer.constraint_types_present, (typeof(vi), Reservoir)) + push!( + optimizer.global_constraints, + MOI.ConstraintIndex{typeof(vi),Reservoir}(constraint_index), + ) + + return MOI.ConstraintIndex{typeof(vi),Reservoir}(constraint_index) +end + +""" + + Objective Function Support + +""" + +# Like maximize `x` +function MOI.set( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.VariableIndex}, + objective_function::MOI.VariableIndex, +) + cp_objective = CpObjective(vars = [objective_function.value], coeffs = [Int64(1)]) + + optimizer.model.objective = cp_objective + + return nothing +end + +function MOI.get(optimizer::CPSATOptimizer, ::MOI.ObjectiveFunction{MOI.VariableIndex}) + if isnothing(optimizer.model.objective) + return nothing + end + + return MOI.VariableIndex(optimizer.model.objective.vars[1]) +end + +function MOI.supports(::CPSATOptimizer, ::MOI.ObjectiveFunction{MOI.VariableIndex}) + return true +end + +function MOI.set( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, + objective_function::MOI.ScalarAffineFunction{T}, +) where {T<:Integer} + cp_objective = CpObjective() + # Set the floating_point_objective to `nothing`. + optimizer.model.floating_point_objective = nothing + + terms = objective_function.terms + + terms_pairs = get_terms_pairs(terms) + + for term in terms_pairs + push!(cp_objective.vars, term[1]) + push!(cp_objective.coeffs, term[2]) + end + + cp_objective.offset = objective_function.constant + + optimizer.model.objective = cp_objective + + return nothing +end + +function MOI.get( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, +) where {T<:Integer} + if isnothing(optimizer.model.objective) + return nothing + end + + scalar_affine_terms = map( + i -> MOI.ScalarAffineTerm{T}( + optimizer.model.objective.coeffs[i], + MOI.VariableIndex(optimizer.model.objective.vars[i]), + ), + 1:length(optimizer.model.objective.vars), + ) + + return MOI.ScalarAffineFunction{T}( + scalar_affine_terms, + optimizer.model.objective.offset, + ) +end + +function MOI.supports( + ::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, +) where {T<:Real} + return true +end + +function optionally_initialize_objective!(optimizer::CPSATOptimizer)::Nothing + if isnothing(optimizer.model.objective) + optimizer.model.objective = CpObjective() + end + + return nothing +end + +function MOI.set( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveSense, + sense::MOI.OptimizationSense, +) + if sense == MOI.MAX_SENSE + if !isnothing(optimizer.model.objective) + optimizer.model.objective.scaling_factor = Float64(-1) + # For ineger objectives, we also negate the coefficients. + optimizer.model.objective.coeffs .= .-optimizer.model.objective.coeffs + elseif !isnothing(optimizer.model.floating_point_objective) + optimizer.model.floating_point_objective.maximize = true + end + elseif sense == MOI.MIN_SENSE + if !isnothing(optimizer.model.objective) + optimizer.model.objective.scaling_factor = Float64(1) + elseif !isnothing(optimizer.model.floating_point_objective) + optimizer.model.floating_point_objective.maximize = false + end + else + # ObjectiveFunction is not considered. This is a feasibility problem. + optimizer.model.objective = nothing + optimizer.model.floating_point_objective = nothing + end + + return nothing +end + +function MOI.get(optimizer::CPSATOptimizer, ::MOI.ObjectiveSense) + if !isnothing(optimizer.model.objective) + return optimizer.model.objective.scaling_factor == Float64(-1) ? MOI.MAX_SENSE : + MOI.MIN_SENSE + elseif !isnothing(optimizer.model.floating_point_objective) + return optimizer.model.floating_point_objective.maximize ? MOI.MAX_SENSE : + MOI.MIN_SENSE + else + return MOI.FEASIBILITY_SENSE + end +end + +MOI.supports(optimizer::CPSATOptimizer, ::MOI.ObjectiveSense) = true + +function MOI.get(optimizer::CPSATOptimizer, ::MOI.ObjectiveFunctionType) + if !isnothing(optimizer.model.objective) + # Check if the objective function is just a single variable + if optimizer.model.objective.offset == Float64(0) && + length(optimizer.model.objective.coeffs) == 1 && + optimizer.model.objective.coeffs[1] == Int64(1) && + length(optimizer.model.objective.vars) == 1 + + return MOI.VariableIndex + end + + return MOI.ScalarAffineFunction{Integer} + elseif !isnothing(optimizer.model.floating_point_objective) + # If this is set, it's a scalar affine function by default. + return MOI.ScalarAffineFunction{Float64} + else + return nothing + end +end + +""" +This specifies to the solver that it should only look for an objective value in the given domain; +the domain on the sum of the objective terms only. +""" +struct ObjectiveFunctionSumDomain <: MOI.AbstractModelAttribute end +MOI.attribute_value_type(::ObjectiveFunctionSumDomain) = Union{Nothing,Vector{Int64}} + +function MOI.supports(::CPSATOptimizer, ::Type{ObjectiveFunctionSumDomain}) + return true +end + +function MOI.set(optimizer::CPSATOptimizer, ::ObjectiveFunctionSumDomain, vi::Vector{Int64}) + if isnothing(optimizer.model.objective) + throw( + ArgumentError("The objective function must be set before setting the domain."), + ) + end + + optimizer.model.objective.domain = vi + + return nothing +end + +function MOI.get( + optimizer::CPSATOptimizer, + ::ObjectiveFunctionSumDomain, +)::Union{Nothing,Vector{Int64}} + if isnothing(optimizer.model.objective) + return nothing + end + + return optimizer.model.objective.domain +end + +function MOI.set( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, + objective_function::MOI.ScalarAffineFunction{T}, +) where {T<:Real} + # Override the integer objective function + optimizer.model.objective = nothing + + optimizer.model.floating_point_objective = FloatObjective() + + terms = objective_function.terms + + terms_pairs = get_terms_pairs(terms) + + for term in terms_pairs + push!(optimizer.model.floating_point_objective.vars, term[1]) + push!(optimizer.model.floating_point_objective.coeffs, term[2]) + end + + optimizer.model.floating_point_objective.offset = objective_function.constant + + return nothing +end + +function MOI.get( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction{T}}, +) where {T<:Real} + if isnothing(optimizer.model.floating_point_objective) + return nothing + end + + scalar_affine_terms = map( + i -> MOI.ScalarAffineTerm{T}( + optimizer.model.floating_point_objective.coeffs[i], + MOI.VariableIndex(optimizer.model.floating_point_objective.vars[i]), + ), + 1:length(optimizer.model.floating_point_objective.vars), + ) + + return MOI.ScalarAffineFunction{T}( + scalar_affine_terms, + optimizer.model.floating_point_objective.offset, + ) +end + +function MOI.get( + optimizer::CPSATOptimizer, + ::MOI.ObjectiveFunction{MOI.ScalarAffineFunction}, +) + try + return something( + MOI.get(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Int64}}()), + MOI.get(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}()), + ) + catch error + throw(error) + # Argument error thrown when both objectives are not set. + return nothing + end +end + +""" + Define the strategy to follow when the solver needs to take a new decision. + The strategy is only defined on a subset of variables. +""" +struct DecisionStrategy + vars::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction} + variable_selection_strategy::CPSATVariableSelectionStrategy.T + domain_reduction_strategy::CPSATDomainReductionStrategy.T + + function DecisionStrategy( + vars::Union{MOI.VectorOfVariables,MOI.VectorAffineFunction}, + variable_selection_strategy::CPSATVariableSelectionStrategy.T, + domain_reduction_strategy::CPSATDomainReductionStrategy.T, + ) + new(vars, variable_selection_strategy, domain_reduction_strategy) + end +end + +function to_cp_sat_decision_strategy( + decision_stategy::DecisionStrategy, +)::CPSATDecisionStrategy + cp_sat_decision_strategy = CPSATDecisionStrategy() + if isa(decision_stategy.vars, MOI.VectorOfVariables) + cp_sat_decision_strategy.variables = get_variable_indices( + decision_stategy.vars, + 1, + MOI.output_dimension(decision_stategy.vars), + ) + empty!(cp_sat_decision_strategy.exprs) + else + # VectorAffineFunction is converted to a LinearExpression proto. + cp_sat_decision_strategy.exprs = build_linear_expressions(decision_stategy.vars) + empty!(cp_sat_decision_strategy.vars) + end + cp_sat_decision_strategy.variable_selection_strategy = + decision_stategy.variable_selection_strategy + cp_sat_decision_strategy.domain_reduction_strategy = + decision_stategy.domain_reduction_strategy + return cp_sat_decision_strategy +end + +struct DecisionStrategyList <: MOI.AbstractModelAttribute end +MOI.attribute_value_type(::DecisionStrategyList) = Vector{DecisionStrategy} + +function MOI.supports(::CPSATOptimizer, ::Type{DecisionStrategyList}) + return true +end + +function MOI.set( + optimizer::CPSATOptimizer, + ::DecisionStrategyList, + decision_strategy_list::Vector{DecisionStrategy}, +) + optimizer.model.search_strategy = to_cp_sat_decision_strategy.(decision_strategy_list) + return nothing +end + +function MOI.get( + optimizer::CPSATOptimizer, + ::DecisionStrategyList, +)::Vector{DecisionStrategy} + decision_strategies = Vector{DecisionStrategy}() + + for strategy in optimizer.model.search_strategy + vars = [] + if !isempty(strategy.variables) + vars = MOI.VectorOfVariables(map(v -> MOI.VariableIndex(v), strategy.variables)) + elseif !isempty(strategy.exprs) + terms = map( + i -> MOI.VectorAffineTerm( + i, + MOI.ScalarAffineTerm{Int64}( + strategy.exprs.coeffs[i], + MOI.VariableIndex(strategy.exprs.vars[i]), + ), + ), + 1:length(strategy.exprs.vars), + ) + vars = MOI.VectorAffineFunction(terms, map(v -> v.offset, strategy.exprs)) + end + + push!( + decision_strategies, + DecisionStrategy( + vars, + strategy.variable_selection_strategy, + strategy.domain_reduction_strategy, + ), + ) + end + + return decision_strategies +end + +MOI.supports(::CPSATOptimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex}) = true + +function MOI.get(optimizer::CPSATOptimizer, ::MOI.VariablePrimalStart, vi::MOI.VariableIndex) + if isnothing(optimizer.model.solution_hint) + return nothing + end + + idx = findfirst(==(vi.value), optimizer.model.solution_hint.vars) + + isnothing(idx) && return nothing + + return optimizer.model.solution_hint.values[idx] +end + +function MOI.set(optimizer::CPSATOptimizer, ::MOI.VariablePrimalStart, vi::MOI.VariableIndex, value::Int) + if isnothing(optimizer.model.solution_hint) + optimizer.model.solution_hint = NewCPSATPartialVariableAssignment() + end + + push!(optimizer.model.solution_hint.vars, vi.value) + push!(optimizer.model.solution_hint.values, value) + + return nothing +end + +struct Assumptions <: MOI.AbstractModelAttribute end +MOI.attribute_value_type(::Assumptions) = Union{Nothing,MOI.VectorOfVariables} + +function MOI.supports(::CPSATOptimizer, ::Type{Assumptions}) + return true +end + +function MOI.set( + optimizer::CPSATOptimizer, + ::Assumptions, + assumptions::Union{Nothing,MOI.VectorOfVariables}, +) + if isnothing(assumptions) + optimizer.model.assumptions = Vector{Int32}() + else + optimizer.model.assumptions = + get_variable_indices(assumptions, 1, MOI.output_dimension(assumptions)) + end + return nothing +end + +function MOI.get( + optimizer::CPSATOptimizer, + ::Assumptions, +)::Union{Nothing,MOI.VectorOfVariables} + return isempty(optimizer.model.assumptions) ? nothing : + MOI.VectorOfVariables( + map( + i -> MOI.VariableIndex(optimizer.model.assumptions[i]), + 1:length(optimizer.model.assumptions), + ), + ) +end diff --git a/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl b/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl index f0de08a64b1..e6020125155 100644 --- a/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl +++ b/ortools/julia/ORTools.jl/src/moi_wrapper/Type_wrappers.jl @@ -92,7 +92,7 @@ The circuit constraint takes a graph and forces the arcs present (with arc presence indicated by a literal) to form a unique cycle. """ const CircuitConstraintProto = Sat.CircuitConstraintProto -const CircuitConstraint = +const NewCircuitConstraint = () -> CircuitConstraintProto( Vector{Int32}(), # tails Vector{Int32}(), # heads @@ -140,7 +140,6 @@ const SatParameters_ConflictMinimizationAlgorithm = Sat.var"SatParameters.ConflictMinimizationAlgorithm" const SatParameters_BinaryMinizationAlgorithm = Sat.var"SatParameters.BinaryMinizationAlgorithm" -const SatParameters_ClauseProtection = Sat.var"SatParameters.ClauseProtection" const SatParameters_ClauseOrdering = OperationsResearch.sat.var"SatParameters.ClauseOrdering" const SatParameters_RestartAlgorithm = Sat.var"SatParameters.RestartAlgorithm" @@ -875,7 +874,6 @@ mutable struct SatParameters clause_cleanup_period::Int32 clause_cleanup_target::Int32 clause_cleanup_ratio::Float64 - clause_cleanup_protection::SatParameters_ClauseProtection.T clause_cleanup_lbd_bound::Int32 clause_cleanup_ordering::SatParameters_ClauseOrdering.T pb_cleanup_increment::Int32 @@ -1091,12 +1089,11 @@ mutable struct SatParameters initial_variables_activity = Float64(0.0), also_bump_variables_in_conflict_reasons = false, minimization_algorithm = SatParameters_ConflictMinimizationAlgorithm.RECURSIVE, - binary_minimization_algorithm = SatParameters_BinaryMinizationAlgorithm.BINARY_MINIMIZATION_FIRST, + binary_minimization_algorithm = SatParameters_BinaryMinizationAlgorithm.BINARY_MINIMIZATION_FROM_UIP_AND_DECISIONS, subsumption_during_conflict_analysis = true, clause_cleanup_period = Int32(10000), clause_cleanup_target = Int32(0), clause_cleanup_ratio = Float64(0.5), - clause_cleanup_protection = SatParameters_ClauseProtection.PROTECTION_NONE, clause_cleanup_lbd_bound = Int32(5), clause_cleanup_ordering = SatParameters_ClauseOrdering.CLAUSE_ACTIVITY, pb_cleanup_increment = Int32(200), @@ -1317,7 +1314,6 @@ mutable struct SatParameters clause_cleanup_period, clause_cleanup_target, clause_cleanup_ratio, - clause_cleanup_protection, clause_cleanup_lbd_bound, clause_cleanup_ordering, pb_cleanup_increment, @@ -1545,7 +1541,6 @@ function to_proto_struct( sat_parameters.clause_cleanup_period, sat_parameters.clause_cleanup_target, sat_parameters.clause_cleanup_ratio, - sat_parameters.clause_cleanup_protection, sat_parameters.clause_cleanup_lbd_bound, sat_parameters.clause_cleanup_ordering, sat_parameters.pb_cleanup_increment, @@ -2207,12 +2202,15 @@ end Mutable wrapper struct for the LinearArgumentProto struct. """ mutable struct LinearArgument - target::Union{Nothing,LinearExpression} - exprs::Vector{LinearExpression} + target::Union{Nothing,CPSatLinearExpression} + exprs::Vector{CPSatLinearExpression} end function to_proto_struct(linear_argument::LinearArgument)::Sat.LinearArgumentProto - return Sat.LinearArgumentProto(linear_argument.target, linear_argument.exprs) + return Sat.LinearArgumentProto( + to_proto_struct(linear_argument.target), + to_proto_struct.(linear_argument.exprs), + ) end """ @@ -2221,8 +2219,8 @@ end mutable struct AllDifferentConstraint exprs::Vector{CPSatLinearExpression} - function AllDifferentConstraint() - new(Vector{CPSatLinearExpression}()) + function AllDifferentConstraint(; exprs = Vector{CPSatLinearExpression}()) + new(exprs) end end @@ -2252,7 +2250,7 @@ mutable struct ElementConstraint linear_target = nothing, exprs = Vector{CPSatLinearExpression}(), ) - new(nothing, nothing, Vector{CPSatLinearExpression}()) + new(linear_index, linear_target, exprs) end end @@ -2310,8 +2308,13 @@ mutable struct RoutesConstraint literals::Vector{Int32} dimensions::Vector{NodeExpressions} - function RoutesConstraint() - new(Vector{Int32}(), Vector{Int32}(), Vector{Int32}(), Vector{NodeExpressions}()) + function RoutesConstraint(; + tails = Vector{Int32}(), + heads = Vector{Int32}(), + literals = Vector{Int32}(), + dimensions = Vector{NodeExpressions}(), + ) + new(tails, heads, literals, dimensions) end end @@ -2336,12 +2339,15 @@ mutable struct TableConstraint exprs::Vector{CPSatLinearExpression} negated::Bool - function TableConstraint() + function TableConstraint(; + values::Vector{Int64} = Vector{Int64}(), + exprs = Vector{CPSatLinearExpression}(), + negated = false, + ) new( - Vector{Int32}(), # [Deprecated] vars - Vector{Int64}(), - Vector{CPSatLinearExpression}(), - false, # negated is falase by default + values, + exprs, + negated, # negated is false by default ) end end @@ -2350,7 +2356,12 @@ end function to_proto_struct(table_constraint::TableConstraint)::Sat.TableConstraintProto exprs = to_proto_struct.(table_constraint.exprs) - return Sat.TableConstraintProto(vars, exprs, negated) + return Sat.TableConstraintProto( + Vector{Int32}(), # [Deprecated] vars + table_constraint.values, + table_constraint.exprs, + table_constraint.negated, + ) end """ @@ -2368,14 +2379,21 @@ mutable struct AutomatonConstraint transition_label::Vector{Int64} exprs::Vector{CPSatLinearExpression} - function AutomatonConstraint() + function AutomatonConstraint(; + starting_state = zero(Int64), + final_states = Vector{Int64}(), + transition_tail = Vector{Int64}(), + transition_head = Vector{Int64}(), + transition_label = Vector{Int64}(), + exprs = Vector{CPSatLinearExpression}(), + ) new( - zero(Int64), - Vector{Int64}(), - Vector{Int64}(), - Vector{Int64}(), - Vector{Int64}(), - Vector{CPSatLinearExpression}(), + starting_state, + final_states, + transition_tail, + transition_head, + transition_label, + exprs, ) end end @@ -2422,14 +2440,14 @@ mutable struct ReservoirConstraint level_changes::Vector{CPSatLinearExpression} active_literals::Vector{Int32} - function ReservoirConstraint() - new( - zero(Int64), # min_level - zero(Int64), # max_level - Vector{CPSatLinearExpression}(), # time_exprs - Vector{CPSatLinearExpression}(), # level_changes - Vector{Int32}(), # active_literals - ) + function ReservoirConstraint(; + min_level = zero(Int64), + max_level = zero(Int64), + time_exprs = Vector{CPSatLinearExpression}(), + level_changes = Vector{CPSatLinearExpression}(), + active_literals = Vector{Int32}(), + ) + new(min_level, max_level, time_exprs, level_changes, active_literals) end end @@ -2543,50 +2561,14 @@ function to_proto_struct( return Sat.CumulativeConstraintProto(capacity, cumulative_constraint.intervals, demands) end -""" - Alias for the `ListOfVariablesProto` struct. - - This is a list of variables, without any semantics. - - This constraint is not meant to be used and will be rejected by the - solver. It is meant to mark variable when testing the presolve code. -""" -const ListOfVariablesProto = Sat.ListOfVariablesProto -const ListOfVariables = () -> ListOfVariablesProto( - Vector{Int32}(), # vars -) - - """ Mutable wrapper for the `ConstraintProto` in CPSat. """ mutable struct CPSATConstraint name::String enforcement_literal::Vector{Int32} - constraint::Union{ - Nothing, - PB.OneOf{ - <:Union{ - AllDifferentConstraint, - CPSatLinearConstraintProto, - LinearArgument, - BoolArgument, - InverseConstraintProto, - ListOfVariablesProto, - ElementConstraint, - CircuitConstraintProto, - RoutesConstraint, - TableConstraint, - AutomatonConstraint, - ReservoirConstraint, - ReservoirConstraint, - IntervalConstraint, - NoOverlapConstraintProto, - NoOverlap2DConstraintProto, - CumulativeConstraint, - }, - }, - } + constraint::Union{Nothing,NamedTuple} + function CPSATConstraint(; name = "", enforcement_literal = Vector{Int32}(), @@ -2706,13 +2688,13 @@ end Define the strategy to follow when the solver needs to take a new decision. Note that this strategy is only defined on a subset of variables. """ -mutable struct DecisionStrategy +mutable struct CPSATDecisionStrategy variables::Vector{Int32} exprs::Vector{CPSatLinearExpression} variable_selection_strategy::CPSATVariableSelectionStrategy.T domain_reduction_strategy::CPSATDomainReductionStrategy.T - function DecisionStrategy(; + function CPSATDecisionStrategy(; variables = Vector{Int32}(), exprs = Vector{CPSatLinearExpression}(), variable_selection_strategy = CPSATVariableSelectionStrategy.CHOOSE_FIRST, @@ -2722,8 +2704,10 @@ mutable struct DecisionStrategy end end -function to_proto_struct(decision_strategy::DecisionStrategy)::Sat.DecisionStrategyProto - return Sat.DecisionStrategyProto( +function to_proto_struct( + decision_strategy::CPSATDecisionStrategy, +)::Sat.DecisionStrategyProto + return Sat.CPSATDecisionStrategyProto( decision_strategy.variables, to_proto_struct.(decision_strategy.exprs), decision_strategy.variable_selection_strategy, @@ -2742,7 +2726,7 @@ mutable struct CpModel constraints::Vector{CPSATConstraint} objective::Union{Nothing,CpObjective} floating_point_objective::Union{Nothing,FloatObjective} - search_strategy::Vector{DecisionStrategy} + search_strategy::Vector{CPSATDecisionStrategy} solution_hint::Union{Nothing,CPSATPartialVariableAssignment} assumptions::Vector{Int32} symmetry::Union{Nothing,CPSATExperimentalSymmetry} @@ -2753,7 +2737,7 @@ mutable struct CpModel constraints = Vector{CPSATConstraint}(), objective = nothing, floating_point_objective = nothing, - search_strategy = Vector{DecisionStrategy}(), + search_strategy = Vector{CPSATDecisionStrategy}(), solution_hint = nothing, assumptions = Vector{Int32}(), ) diff --git a/ortools/julia/ORToolsGenerated.jl/Project.toml b/ortools/julia/ORToolsGenerated.jl/Project.toml index e86b9b422e9..d21e3183167 100644 --- a/ortools/julia/ORToolsGenerated.jl/Project.toml +++ b/ortools/julia/ORToolsGenerated.jl/Project.toml @@ -1,6 +1,6 @@ name = "ORToolsGenerated" uuid = "6b269722-41d3-11ee-be56-0242ac120002" -version = "0.0.3" +version = "0.1.0" [deps] ORTools_jll = "717719f8-c30c-5086-8f3c-70cd6a1e3a46"