* bump abseil to 20250814 * bump protobuf to v32.0 * cmake: add ccache auto support * backport flatzinc, math_opt and sat update
1234 lines
52 KiB
Python
1234 lines
52 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright 2010-2025 Google LLC
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import datetime
|
|
import math
|
|
|
|
from absl.testing import absltest
|
|
from ortools.pdlp import solve_log_pb2
|
|
from ortools.math_opt import result_pb2
|
|
from ortools.math_opt import solution_pb2
|
|
from ortools.math_opt import sparse_containers_pb2
|
|
from ortools.math_opt.python import model
|
|
from ortools.math_opt.python import result
|
|
from ortools.math_opt.python import solution
|
|
from ortools.math_opt.python.testing import compare_proto
|
|
from ortools.math_opt.solvers import osqp_pb2
|
|
from ortools.math_opt.solvers.gscip import gscip_pb2
|
|
|
|
|
|
class TerminationTest(compare_proto.MathOptProtoAssertions, absltest.TestCase):
|
|
|
|
def test_termination_unspecified(self) -> None:
|
|
termination_proto = result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_UNSPECIFIED
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "Termination.*UNSPECIFIED"):
|
|
result.parse_termination(termination_proto)
|
|
|
|
def test_termination_limit_but_not_limit_reason(self) -> None:
|
|
termination_proto = result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_OPTIMAL,
|
|
limit=result_pb2.LIMIT_OTHER,
|
|
)
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Termination limit.*FEASIBLE or NO_SOLUTION_FOUND"
|
|
):
|
|
result.parse_termination(termination_proto)
|
|
|
|
def test_termination_limit_reason_but_no_limit(self) -> None:
|
|
termination_proto = result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND,
|
|
limit=result_pb2.LIMIT_UNSPECIFIED,
|
|
)
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Termination limit.*FEASIBLE or NO_SOLUTION_FOUND"
|
|
):
|
|
result.parse_termination(termination_proto)
|
|
|
|
def test_termination_ok_proto_round_trip(self) -> None:
|
|
termination = result.Termination(
|
|
reason=result.TerminationReason.NO_SOLUTION_FOUND,
|
|
limit=result.Limit.OTHER,
|
|
detail="detail",
|
|
problem_status=result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.FEASIBLE,
|
|
dual_status=result.FeasibilityStatus.INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result.ObjectiveBounds(primal_bound=10, dual_bound=20),
|
|
)
|
|
|
|
termination_proto = result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND,
|
|
limit=result_pb2.LIMIT_OTHER,
|
|
detail="detail",
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result_pb2.ObjectiveBoundsProto(
|
|
primal_bound=10, dual_bound=20
|
|
),
|
|
)
|
|
|
|
# Test proto-> Termination
|
|
self.assertEqual(result.parse_termination(termination_proto), termination)
|
|
|
|
# Test Termination -> proto
|
|
self.assert_protos_equiv(termination.to_proto(), termination_proto)
|
|
|
|
|
|
class ParseProblemStatus(compare_proto.MathOptProtoAssertions, absltest.TestCase):
|
|
|
|
def test_problem_status_round_trip(self) -> None:
|
|
problem_status = result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.FEASIBLE,
|
|
dual_status=result.FeasibilityStatus.INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
)
|
|
problem_status_proto = problem_status.to_proto()
|
|
expected_proto = result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
)
|
|
self.assert_protos_equiv(expected_proto, problem_status_proto)
|
|
round_trip_status = result.parse_problem_status(problem_status_proto)
|
|
self.assertEqual(problem_status, round_trip_status)
|
|
|
|
def test_problem_status_unspecified_primal_status(self) -> None:
|
|
proto = result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_UNSPECIFIED,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
)
|
|
with self.assertRaisesRegex(
|
|
ValueError, "Primal feasibility status.*UNSPECIFIED"
|
|
):
|
|
result.parse_problem_status(proto)
|
|
|
|
def test_problem_status_unspecified_dual_status(self) -> None:
|
|
proto = result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_UNSPECIFIED,
|
|
primal_or_dual_infeasible=False,
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "Dual feasibility status.*UNSPECIFIED"):
|
|
result.parse_problem_status(proto)
|
|
|
|
|
|
class ParseObjectiveBounds(compare_proto.MathOptProtoAssertions, absltest.TestCase):
|
|
|
|
def test_objective_bounds_round_trip(self) -> None:
|
|
objective_bounds = result.ObjectiveBounds(primal_bound=10, dual_bound=20)
|
|
objective_bounds_proto = objective_bounds.to_proto()
|
|
expected_proto = result_pb2.ObjectiveBoundsProto(primal_bound=10, dual_bound=20)
|
|
self.assert_protos_equiv(expected_proto, objective_bounds_proto)
|
|
round_trip_objective_bounds = result.parse_objective_bounds(
|
|
objective_bounds_proto
|
|
)
|
|
self.assertEqual(objective_bounds, round_trip_objective_bounds)
|
|
|
|
|
|
class ParseSolveStats(compare_proto.MathOptProtoAssertions, absltest.TestCase):
|
|
|
|
def test_problem_status_round_trip(self) -> None:
|
|
solve_stats = result.SolveStats(
|
|
solve_time=datetime.timedelta(seconds=10),
|
|
simplex_iterations=10,
|
|
barrier_iterations=20,
|
|
first_order_iterations=30,
|
|
node_count=40,
|
|
)
|
|
solve_stats_proto = solve_stats.to_proto()
|
|
expected_proto = result_pb2.SolveStatsProto()
|
|
expected_proto.solve_time.seconds = 10
|
|
expected_proto.simplex_iterations = 10
|
|
expected_proto.barrier_iterations = 20
|
|
expected_proto.first_order_iterations = 30
|
|
expected_proto.node_count = 40
|
|
self.assert_protos_equiv(expected_proto, solve_stats_proto)
|
|
round_trip_solve_stats = result.parse_solve_stats(solve_stats_proto)
|
|
self.assertEqual(solve_stats, round_trip_solve_stats)
|
|
|
|
|
|
class SolveResultAuxiliaryFunctionsTest(absltest.TestCase):
|
|
|
|
def test_solve_time(self) -> None:
|
|
res = result.SolveResult(
|
|
solve_stats=result.SolveStats(solve_time=datetime.timedelta(seconds=10))
|
|
)
|
|
self.assertEqual(res.solve_time(), datetime.timedelta(seconds=10))
|
|
|
|
def test_best_objective_bound(self) -> None:
|
|
res = result.SolveResult(
|
|
termination=result.Termination(
|
|
objective_bounds=result.ObjectiveBounds(dual_bound=10.0)
|
|
)
|
|
)
|
|
self.assertEqual(res.best_objective_bound(), 10.0)
|
|
|
|
def test_primal_solution_has_feasible(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
other_mod = model.Model(name="other_test_model")
|
|
other_x = other_mod.add_binary_variable(name="other_x")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
primal_solution=solution.PrimalSolution(
|
|
variable_values={x: 2.0, y: 1.0},
|
|
objective_value=3.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
)
|
|
self.assertTrue(res.has_primal_feasible_solution())
|
|
self.assertEqual(res.objective_value(), 3.0)
|
|
self.assertDictEqual(res.variable_values(), {x: 2.0, y: 1.0})
|
|
self.assertEqual(res.variable_values()[x], 2.0)
|
|
self.assertEqual(res.variable_values([]), [])
|
|
self.assertEqual(res.variable_values([y, x]), [1.0, 2.0])
|
|
self.assertEqual(res.variable_values(y), 1.0)
|
|
with self.assertRaisesRegex(KeyError, ".*other_x"):
|
|
res.variable_values(other_x)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.variable_values([y, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.variable_values(20)
|
|
|
|
def test_primal_solution_no_feasible(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
primal_solution=solution.PrimalSolution(
|
|
variable_values={
|
|
x: 2.0,
|
|
},
|
|
objective_value=3.0,
|
|
feasibility_status=solution.SolutionStatus.UNDETERMINED,
|
|
)
|
|
)
|
|
)
|
|
self.assertFalse(res.has_primal_feasible_solution())
|
|
with self.assertRaisesRegex(ValueError, "No primal feasible.*"):
|
|
res.objective_value()
|
|
with self.assertRaisesRegex(ValueError, "No primal feasible.*"):
|
|
res.variable_values()
|
|
|
|
def test_primal_solution_no_primal(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
dual_solution=solution.DualSolution(
|
|
dual_values={c: 3.0},
|
|
reduced_costs={x: 1.0},
|
|
objective_value=2.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
)
|
|
self.assertFalse(res.has_primal_feasible_solution())
|
|
with self.assertRaisesRegex(ValueError, "No primal feasible.*"):
|
|
res.objective_value()
|
|
with self.assertRaisesRegex(ValueError, "No primal feasible.*"):
|
|
res.variable_values()
|
|
|
|
def test_primal_solution_no_solution(self) -> None:
|
|
res = result.SolveResult()
|
|
self.assertFalse(res.has_primal_feasible_solution())
|
|
with self.assertRaisesRegex(ValueError, "No primal feasible.*"):
|
|
res.objective_value()
|
|
with self.assertRaisesRegex(ValueError, "No primal feasible.*"):
|
|
res.variable_values()
|
|
|
|
def test_dual_solution_has_feasible(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d")
|
|
other_mod = model.Model(name="other_test_model")
|
|
other_x = other_mod.add_binary_variable(name="other_x")
|
|
other_c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="other_c")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
dual_solution=solution.DualSolution(
|
|
dual_values={c: 3.0, d: 4.0},
|
|
reduced_costs={x: 1.0, y: -2.0},
|
|
objective_value=2.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
)
|
|
self.assertTrue(res.has_dual_feasible_solution())
|
|
# Reduced costs.
|
|
self.assertDictEqual(res.reduced_costs(), {x: 1.0, y: -2.0})
|
|
self.assertEqual(res.reduced_costs()[x], 1.0)
|
|
self.assertEqual(res.reduced_costs([]), [])
|
|
self.assertEqual(res.reduced_costs([y, x]), [-2.0, 1.0])
|
|
self.assertEqual(res.reduced_costs(y), -2.0)
|
|
with self.assertRaisesRegex(KeyError, ".*other_x"):
|
|
res.reduced_costs(other_x)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.reduced_costs([y, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.reduced_costs(20)
|
|
# Dual values.
|
|
self.assertDictEqual(res.dual_values(), {c: 3.0, d: 4.0})
|
|
self.assertEqual(res.dual_values()[c], 3.0)
|
|
self.assertEqual(res.dual_values([]), [])
|
|
self.assertEqual(res.dual_values([d, c]), [4.0, 3.0])
|
|
self.assertEqual(res.dual_values(c), 3.0)
|
|
with self.assertRaisesRegex(KeyError, ".*other_c"):
|
|
res.dual_values(other_c)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.dual_values([d, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.dual_values(20)
|
|
|
|
def test_dual_solution_no_feasible(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
dual_solution=solution.DualSolution(
|
|
dual_values={c: 3.0},
|
|
reduced_costs={
|
|
x: 1.0,
|
|
},
|
|
objective_value=2.0,
|
|
feasibility_status=solution.SolutionStatus.UNDETERMINED,
|
|
)
|
|
)
|
|
)
|
|
self.assertFalse(res.has_dual_feasible_solution())
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"):
|
|
res.reduced_costs()
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"):
|
|
res.dual_values()
|
|
|
|
def test_dual_solution_no_dual_in_best_solution(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
primal_solution=solution.PrimalSolution(
|
|
variable_values={
|
|
x: 2.0,
|
|
},
|
|
objective_value=3.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
)
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
dual_solution=solution.DualSolution(
|
|
dual_values={c: 3.0},
|
|
reduced_costs={
|
|
x: 1.0,
|
|
},
|
|
objective_value=2.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
)
|
|
self.assertFalse(res.has_dual_feasible_solution())
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"):
|
|
res.reduced_costs()
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"):
|
|
res.dual_values()
|
|
|
|
def test_dual_solution_no_solution(self) -> None:
|
|
res = result.SolveResult()
|
|
self.assertFalse(res.has_dual_feasible_solution())
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"):
|
|
res.reduced_costs()
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"):
|
|
res.dual_values()
|
|
|
|
def test_primal_ray_has_ray(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
other_mod = model.Model(name="other_test_model")
|
|
other_x = other_mod.add_binary_variable(name="other_x")
|
|
res = result.SolveResult()
|
|
res.primal_rays.append(solution.PrimalRay(variable_values={x: 2.0, y: 1.0}))
|
|
self.assertTrue(res.has_ray())
|
|
self.assertDictEqual(res.ray_variable_values(), {x: 2.0, y: 1.0})
|
|
self.assertEqual(res.ray_variable_values()[x], 2.0)
|
|
self.assertEqual(res.ray_variable_values([]), [])
|
|
self.assertEqual(res.ray_variable_values([y, x]), [1.0, 2.0])
|
|
self.assertEqual(res.ray_variable_values(y), 1.0)
|
|
with self.assertRaisesRegex(KeyError, ".*other_x"):
|
|
res.ray_variable_values(other_x)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.ray_variable_values([y, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.ray_variable_values(20)
|
|
|
|
def test_primal_ray_no_ray(self) -> None:
|
|
res = result.SolveResult()
|
|
self.assertFalse(res.has_ray())
|
|
with self.assertRaisesRegex(ValueError, ".*primal ray.*"):
|
|
res.ray_variable_values()
|
|
|
|
def test_dual_ray_has_ray(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
other_mod = model.Model(name="other_test_model")
|
|
other_x = other_mod.add_binary_variable(name="other_x")
|
|
other_c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="other_c")
|
|
res = result.SolveResult()
|
|
res.dual_rays.append(
|
|
solution.DualRay(
|
|
dual_values={c: 3.0, d: 4.0}, reduced_costs={x: 1.0, y: -2.0}
|
|
)
|
|
)
|
|
self.assertTrue(res.has_dual_ray())
|
|
self.assertDictEqual(res.ray_reduced_costs(), {x: 1.0, y: -2.0})
|
|
# Reduced costs.
|
|
self.assertEqual(res.ray_reduced_costs()[x], 1.0)
|
|
self.assertEqual(res.ray_reduced_costs([]), [])
|
|
self.assertEqual(res.ray_reduced_costs([y, x]), [-2.0, 1.0])
|
|
self.assertEqual(res.ray_reduced_costs(y), -2.0)
|
|
with self.assertRaisesRegex(KeyError, ".*other_x"):
|
|
res.ray_reduced_costs(other_x)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.ray_reduced_costs([y, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.ray_reduced_costs(20)
|
|
# Dual values.
|
|
self.assertDictEqual(res.ray_dual_values(), {c: 3.0, d: 4.0})
|
|
self.assertEqual(res.ray_dual_values()[c], 3.0)
|
|
self.assertEqual(res.ray_dual_values([]), [])
|
|
self.assertEqual(res.ray_dual_values([d, c]), [4.0, 3.0])
|
|
self.assertEqual(res.ray_dual_values(c), 3.0)
|
|
with self.assertRaisesRegex(KeyError, ".*other_c"):
|
|
res.ray_dual_values(other_c)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.ray_dual_values([d, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.ray_dual_values(20)
|
|
|
|
def test_dual_ray_no_ray(self) -> None:
|
|
res = result.SolveResult()
|
|
self.assertFalse(res.has_dual_ray())
|
|
with self.assertRaisesRegex(ValueError, ".*dual ray.*"):
|
|
res.ray_dual_values()
|
|
with self.assertRaisesRegex(ValueError, ".*dual ray.*"):
|
|
res.ray_reduced_costs()
|
|
|
|
def test_basis_has_basis(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d")
|
|
other_mod = model.Model(name="other_test_model")
|
|
other_x = other_mod.add_binary_variable(name="other_x")
|
|
other_c = other_mod.add_linear_constraint(name="other_c")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
basis=solution.Basis(
|
|
variable_status={
|
|
x: solution.BasisStatus.AT_LOWER_BOUND,
|
|
y: solution.BasisStatus.AT_UPPER_BOUND,
|
|
},
|
|
constraint_status={
|
|
c: solution.BasisStatus.BASIC,
|
|
d: solution.BasisStatus.FIXED_VALUE,
|
|
},
|
|
)
|
|
)
|
|
)
|
|
self.assertTrue(res.has_basis())
|
|
# Variable status
|
|
self.assertDictEqual(
|
|
res.variable_status(),
|
|
{
|
|
x: solution.BasisStatus.AT_LOWER_BOUND,
|
|
y: solution.BasisStatus.AT_UPPER_BOUND,
|
|
},
|
|
)
|
|
self.assertEqual(res.variable_status()[x], solution.BasisStatus.AT_LOWER_BOUND)
|
|
self.assertEqual(res.variable_status([]), [])
|
|
self.assertEqual(
|
|
res.variable_status([y, x]),
|
|
[
|
|
solution.BasisStatus.AT_UPPER_BOUND,
|
|
solution.BasisStatus.AT_LOWER_BOUND,
|
|
],
|
|
)
|
|
self.assertEqual(res.variable_status(y), solution.BasisStatus.AT_UPPER_BOUND)
|
|
with self.assertRaisesRegex(KeyError, ".*other_x"):
|
|
res.variable_status(other_x)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.variable_status([y, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.variable_status(20)
|
|
# Constraint status
|
|
self.assertDictEqual(
|
|
res.constraint_status(),
|
|
{c: solution.BasisStatus.BASIC, d: solution.BasisStatus.FIXED_VALUE},
|
|
)
|
|
self.assertEqual(res.constraint_status()[c], solution.BasisStatus.BASIC)
|
|
self.assertEqual(res.constraint_status([]), [])
|
|
self.assertEqual(
|
|
res.constraint_status([d, c]),
|
|
[solution.BasisStatus.FIXED_VALUE, solution.BasisStatus.BASIC],
|
|
)
|
|
self.assertEqual(res.constraint_status(c), solution.BasisStatus.BASIC)
|
|
with self.assertRaisesRegex(KeyError, ".*other_c"):
|
|
res.constraint_status(other_c)
|
|
with self.assertRaisesRegex(KeyError, ".*string"):
|
|
res.constraint_status([d, "string"])
|
|
with self.assertRaisesRegex(TypeError, ".*int"):
|
|
res.constraint_status(20)
|
|
|
|
def test_basis_no_basis_in_best_solution(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
res = result.SolveResult()
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
primal_solution=solution.PrimalSolution(
|
|
variable_values={x: 2.0, y: 1.0},
|
|
objective_value=3.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
)
|
|
res.solutions.append(
|
|
solution.Solution(
|
|
basis=solution.Basis(
|
|
variable_status={
|
|
x: solution.BasisStatus.AT_LOWER_BOUND,
|
|
y: solution.BasisStatus.AT_UPPER_BOUND,
|
|
},
|
|
constraint_status={c: solution.BasisStatus.BASIC},
|
|
)
|
|
)
|
|
)
|
|
self.assertFalse(res.has_basis())
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"):
|
|
res.variable_status()
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"):
|
|
res.constraint_status()
|
|
|
|
def test_basis_no_solution(self) -> None:
|
|
res = result.SolveResult()
|
|
self.assertFalse(res.has_basis())
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"):
|
|
res.variable_status()
|
|
with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"):
|
|
res.constraint_status()
|
|
|
|
def test_bounded(self) -> None:
|
|
res = result.SolveResult(
|
|
termination=result.Termination(
|
|
reason=result.TerminationReason.NO_SOLUTION_FOUND,
|
|
problem_status=result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.FEASIBLE,
|
|
dual_status=result.FeasibilityStatus.FEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result.ObjectiveBounds(
|
|
primal_bound=math.inf,
|
|
dual_bound=-math.inf,
|
|
),
|
|
),
|
|
)
|
|
self.assertTrue(res.bounded())
|
|
|
|
def test_not_bounded_primal_infeasible(self) -> None:
|
|
res = result.SolveResult(
|
|
termination=result.Termination(
|
|
reason=result.TerminationReason.NO_SOLUTION_FOUND,
|
|
problem_status=result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.INFEASIBLE,
|
|
dual_status=result.FeasibilityStatus.FEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result.ObjectiveBounds(
|
|
primal_bound=math.inf,
|
|
dual_bound=-math.inf,
|
|
),
|
|
),
|
|
)
|
|
self.assertFalse(res.bounded())
|
|
|
|
def test_not_bounded_dual_infeasible(self) -> None:
|
|
res = result.SolveResult(
|
|
termination=result.Termination(
|
|
reason=result.TerminationReason.NO_SOLUTION_FOUND,
|
|
problem_status=result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.FEASIBLE,
|
|
dual_status=result.FeasibilityStatus.INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result.ObjectiveBounds(
|
|
primal_bound=math.inf,
|
|
dual_bound=-math.inf,
|
|
),
|
|
),
|
|
)
|
|
self.assertFalse(res.bounded())
|
|
|
|
|
|
def _make_undetermined_result_proto() -> result_pb2.SolveResultProto:
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND,
|
|
limit=result_pb2.LIMIT_TIME,
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result_pb2.ObjectiveBoundsProto(
|
|
primal_bound=math.inf,
|
|
dual_bound=-math.inf,
|
|
),
|
|
)
|
|
)
|
|
proto.solve_stats.solve_time.FromTimedelta(datetime.timedelta(minutes=2))
|
|
return proto
|
|
|
|
|
|
def _make_undetermined_solve_result() -> result.SolveResult:
|
|
return result.SolveResult(
|
|
termination=result.Termination(
|
|
reason=result.TerminationReason.NO_SOLUTION_FOUND,
|
|
limit=result.Limit.TIME,
|
|
problem_status=result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.UNDETERMINED,
|
|
dual_status=result.FeasibilityStatus.UNDETERMINED,
|
|
),
|
|
objective_bounds=result.ObjectiveBounds(
|
|
primal_bound=math.inf, dual_bound=-math.inf
|
|
),
|
|
),
|
|
solve_stats=result.SolveStats(solve_time=datetime.timedelta(minutes=2)),
|
|
)
|
|
|
|
|
|
class SolveResultTest(compare_proto.MathOptProtoAssertions, absltest.TestCase):
|
|
|
|
def test_solve_result_gscip_output(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
res = _make_undetermined_solve_result()
|
|
res.gscip_specific_output = gscip_pb2.GScipOutput(status_detail="gscip_detail")
|
|
|
|
proto = _make_undetermined_result_proto()
|
|
proto.gscip_output.status_detail = "gscip_detail"
|
|
|
|
# proto -> result
|
|
actual_res = result.parse_solve_result(proto, mod)
|
|
self.assertIsNotNone(actual_res.gscip_specific_output)
|
|
assert actual_res.gscip_specific_output is not None
|
|
self.assertEqual("gscip_detail", actual_res.gscip_specific_output.status_detail)
|
|
self.assertIsNone(actual_res.pdlp_specific_output)
|
|
self.assertIsNone(actual_res.osqp_specific_output)
|
|
|
|
# result -> proto
|
|
self.assert_protos_equiv(res.to_proto(), proto)
|
|
|
|
def test_solve_result_osqp_output(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
res = _make_undetermined_solve_result()
|
|
res.osqp_specific_output = osqp_pb2.OsqpOutput(
|
|
initialized_underlying_solver=True
|
|
)
|
|
|
|
proto = _make_undetermined_result_proto()
|
|
proto.osqp_output.initialized_underlying_solver = True
|
|
|
|
# proto -> result
|
|
actual_res = result.parse_solve_result(proto, mod)
|
|
self.assertIsNotNone(actual_res.osqp_specific_output)
|
|
assert actual_res.osqp_specific_output is not None
|
|
self.assertTrue(actual_res.osqp_specific_output.initialized_underlying_solver)
|
|
self.assertIsNone(actual_res.pdlp_specific_output)
|
|
self.assertIsNone(actual_res.gscip_specific_output)
|
|
|
|
# result -> proto
|
|
self.assert_protos_equiv(res.to_proto(), proto)
|
|
|
|
def test_solve_result_pdlp_output(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
res = _make_undetermined_solve_result()
|
|
res.pdlp_specific_output = result_pb2.SolveResultProto.PdlpOutput(
|
|
convergence_information=solve_log_pb2.ConvergenceInformation(
|
|
primal_objective=1.0
|
|
)
|
|
)
|
|
|
|
proto = _make_undetermined_result_proto()
|
|
proto.pdlp_output.convergence_information.primal_objective = 1.0
|
|
|
|
# proto -> result
|
|
actual_res = result.parse_solve_result(proto, mod)
|
|
self.assertIsNotNone(actual_res.pdlp_specific_output)
|
|
assert actual_res.pdlp_specific_output is not None
|
|
self.assertEqual(
|
|
actual_res.pdlp_specific_output.convergence_information.primal_objective,
|
|
1.0,
|
|
)
|
|
self.assertIsNone(actual_res.osqp_specific_output)
|
|
self.assertIsNone(actual_res.gscip_specific_output)
|
|
|
|
# result -> proto
|
|
self.assert_protos_equiv(res.to_proto(), proto)
|
|
|
|
def test_multiple_solver_specific_outputs_error(self) -> None:
|
|
res = _make_undetermined_solve_result()
|
|
res.gscip_specific_output = gscip_pb2.GScipOutput(status_detail="gscip_detail")
|
|
res.osqp_specific_output = osqp_pb2.OsqpOutput(
|
|
initialized_underlying_solver=False
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "solver specific output"):
|
|
res.to_proto()
|
|
|
|
def test_solve_result_from_proto_missing_bounds_in_termination(
|
|
self,
|
|
) -> None:
|
|
mod = model.Model(name="test_model")
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_INFEASIBLE,
|
|
detail="",
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
),
|
|
solve_stats=result_pb2.SolveStatsProto(
|
|
best_primal_bound=10.0,
|
|
best_dual_bound=20.0,
|
|
),
|
|
)
|
|
res = result.parse_solve_result(proto, mod)
|
|
self.assertEqual(10.0, res.termination.objective_bounds.primal_bound)
|
|
self.assertEqual(20.0, res.termination.objective_bounds.dual_bound)
|
|
self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible)
|
|
|
|
def test_solve_result_from_proto_missing_status_in_termination(
|
|
self,
|
|
) -> None:
|
|
mod = model.Model(name="test_model")
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_INFEASIBLE,
|
|
detail="",
|
|
objective_bounds=result_pb2.ObjectiveBoundsProto(
|
|
primal_bound=10.0, dual_bound=20.0
|
|
),
|
|
),
|
|
solve_stats=result_pb2.SolveStatsProto(
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
),
|
|
)
|
|
res = result.parse_solve_result(proto, mod)
|
|
self.assertEqual(
|
|
result.FeasibilityStatus.INFEASIBLE,
|
|
res.termination.problem_status.primal_status,
|
|
)
|
|
self.assertEqual(
|
|
result.FeasibilityStatus.FEASIBLE,
|
|
res.termination.problem_status.dual_status,
|
|
)
|
|
|
|
def test_solve_result_from_proto_double_infeasible_multiple_rays(
|
|
self,
|
|
) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_INFEASIBLE,
|
|
detail="",
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
primal_or_dual_infeasible=False,
|
|
),
|
|
objective_bounds=result_pb2.ObjectiveBoundsProto(
|
|
primal_bound=math.inf, dual_bound=-math.inf
|
|
),
|
|
),
|
|
primal_rays=[
|
|
solution_pb2.PrimalRayProto(
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[2.0, 1.0]
|
|
)
|
|
),
|
|
solution_pb2.PrimalRayProto(
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[3.0, 2.0]
|
|
)
|
|
),
|
|
],
|
|
dual_rays=[
|
|
solution_pb2.DualRayProto(
|
|
dual_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[4.0]
|
|
),
|
|
reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[10.0, 11.0]
|
|
),
|
|
),
|
|
solution_pb2.DualRayProto(
|
|
dual_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[5.0]
|
|
),
|
|
reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[11.0, 12.0]
|
|
),
|
|
),
|
|
],
|
|
)
|
|
proto.solve_stats.node_count = 10
|
|
proto.solve_stats.problem_status.primal_status = (
|
|
result_pb2.FEASIBILITY_STATUS_INFEASIBLE
|
|
)
|
|
proto.solve_stats.problem_status.dual_status = (
|
|
result_pb2.FEASIBILITY_STATUS_INFEASIBLE
|
|
)
|
|
proto.solve_stats.problem_status.primal_or_dual_infeasible = False
|
|
proto.solve_stats.best_primal_bound = math.inf
|
|
proto.solve_stats.best_dual_bound = -math.inf
|
|
res = result.parse_solve_result(proto, mod)
|
|
|
|
self.assertEqual(result.TerminationReason.INFEASIBLE, res.termination.reason)
|
|
self.assertEqual("", res.termination.detail)
|
|
self.assertIsNone(res.termination.limit)
|
|
self.assertEmpty(res.solutions)
|
|
self.assertLen(res.primal_rays, 2)
|
|
self.assertLen(res.dual_rays, 2)
|
|
self.assertDictEqual({x: 2.0, y: 1.0}, res.primal_rays[0].variable_values)
|
|
self.assertDictEqual({x: 10.0, y: 11.0}, res.dual_rays[0].reduced_costs)
|
|
self.assertDictEqual({c: 4.0}, res.dual_rays[0].dual_values)
|
|
self.assertDictEqual({x: 3.0, y: 2.0}, res.primal_rays[1].variable_values)
|
|
self.assertDictEqual({x: 11.0, y: 12.0}, res.dual_rays[1].reduced_costs)
|
|
self.assertDictEqual({c: 5.0}, res.dual_rays[1].dual_values)
|
|
|
|
# solve_stats
|
|
self.assertEqual(10, res.solve_stats.node_count)
|
|
self.assertEqual(
|
|
result.FeasibilityStatus.INFEASIBLE,
|
|
res.termination.problem_status.primal_status,
|
|
)
|
|
self.assertEqual(
|
|
result.FeasibilityStatus.INFEASIBLE,
|
|
res.termination.problem_status.dual_status,
|
|
)
|
|
self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible)
|
|
self.assertEqual(math.inf, res.termination.objective_bounds.primal_bound)
|
|
self.assertEqual(-math.inf, res.termination.objective_bounds.dual_bound)
|
|
self.assertIsNone(res.gscip_specific_output)
|
|
|
|
def test_solve_result_from_feasible_multiple_solutions(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
y = mod.add_binary_variable(name="y")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_OPTIMAL,
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
),
|
|
objective_bounds=result_pb2.ObjectiveBoundsProto(
|
|
primal_bound=10.0, dual_bound=20.0
|
|
),
|
|
),
|
|
solutions=[
|
|
solution_pb2.SolutionProto(
|
|
primal_solution=solution_pb2.PrimalSolutionProto(
|
|
objective_value=2.0,
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[2.0, 1.0]
|
|
),
|
|
feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE,
|
|
),
|
|
dual_solution=solution_pb2.DualSolutionProto(
|
|
objective_value=2.0,
|
|
dual_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[4.0]
|
|
),
|
|
reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[10.0, 11.0]
|
|
),
|
|
feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE,
|
|
),
|
|
basis=solution_pb2.BasisProto(
|
|
constraint_status=solution_pb2.SparseBasisStatusVector(
|
|
ids=[0],
|
|
values=[solution_pb2.BASIS_STATUS_AT_UPPER_BOUND],
|
|
),
|
|
variable_status=solution_pb2.SparseBasisStatusVector(
|
|
ids=[0, 1],
|
|
values=[
|
|
solution_pb2.BASIS_STATUS_BASIC,
|
|
solution_pb2.BASIS_STATUS_AT_LOWER_BOUND,
|
|
],
|
|
),
|
|
basic_dual_feasibility=solution_pb2.SOLUTION_STATUS_FEASIBLE,
|
|
),
|
|
),
|
|
solution_pb2.SolutionProto(
|
|
primal_solution=solution_pb2.PrimalSolutionProto(
|
|
objective_value=3.0,
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[3.0, 2.0]
|
|
),
|
|
feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE,
|
|
)
|
|
),
|
|
solution_pb2.SolutionProto(
|
|
dual_solution=solution_pb2.DualSolutionProto(
|
|
objective_value=3.0,
|
|
dual_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[5.0]
|
|
),
|
|
reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0, 1], values=[11.0, 12.0]
|
|
),
|
|
feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE,
|
|
)
|
|
),
|
|
solution_pb2.SolutionProto(
|
|
basis=solution_pb2.BasisProto(
|
|
constraint_status=solution_pb2.SparseBasisStatusVector(
|
|
ids=[0], values=[solution_pb2.BASIS_STATUS_BASIC]
|
|
),
|
|
variable_status=solution_pb2.SparseBasisStatusVector(
|
|
ids=[0, 1],
|
|
values=[
|
|
solution_pb2.BASIS_STATUS_AT_LOWER_BOUND,
|
|
solution_pb2.BASIS_STATUS_AT_UPPER_BOUND,
|
|
],
|
|
),
|
|
basic_dual_feasibility=solution_pb2.SOLUTION_STATUS_INFEASIBLE,
|
|
)
|
|
),
|
|
],
|
|
)
|
|
|
|
proto.solve_stats.node_count = 10
|
|
proto.solve_stats.problem_status.primal_status = (
|
|
result_pb2.FEASIBILITY_STATUS_FEASIBLE
|
|
)
|
|
proto.solve_stats.problem_status.dual_status = (
|
|
result_pb2.FEASIBILITY_STATUS_FEASIBLE
|
|
)
|
|
proto.solve_stats.problem_status.primal_or_dual_infeasible = False
|
|
proto.solve_stats.best_primal_bound = 10
|
|
proto.solve_stats.best_dual_bound = 10
|
|
res = result.parse_solve_result(proto, mod)
|
|
|
|
self.assertEqual(result.TerminationReason.OPTIMAL, res.termination.reason)
|
|
self.assertEqual("", res.termination.detail)
|
|
self.assertIsNone(res.termination.limit)
|
|
self.assertLen(res.solutions, 4)
|
|
self.assertEmpty(res.primal_rays)
|
|
self.assertEmpty(res.dual_rays)
|
|
|
|
# Solution 0
|
|
assert (
|
|
res.solutions[0].primal_solution is not None
|
|
and res.solutions[0].dual_solution is not None
|
|
and res.solutions[0].basis is not None
|
|
)
|
|
self.assertEqual(2.0, res.solutions[0].primal_solution.objective_value)
|
|
self.assertDictEqual(
|
|
{x: 2.0, y: 1.0}, res.solutions[0].primal_solution.variable_values
|
|
)
|
|
self.assertEqual(
|
|
solution.SolutionStatus.FEASIBLE,
|
|
res.solutions[0].primal_solution.feasibility_status,
|
|
)
|
|
self.assertEqual(2.0, res.solutions[0].dual_solution.objective_value)
|
|
self.assertDictEqual(
|
|
{x: 10.0, y: 11.0}, res.solutions[0].dual_solution.reduced_costs
|
|
)
|
|
self.assertDictEqual({c: 4.0}, res.solutions[0].dual_solution.dual_values)
|
|
self.assertEqual(
|
|
solution.SolutionStatus.FEASIBLE,
|
|
res.solutions[0].dual_solution.feasibility_status,
|
|
)
|
|
self.assertDictEqual(
|
|
{x: solution.BasisStatus.BASIC, y: solution.BasisStatus.AT_LOWER_BOUND},
|
|
res.solutions[0].basis.variable_status,
|
|
)
|
|
self.assertDictEqual(
|
|
{c: solution.BasisStatus.AT_UPPER_BOUND},
|
|
res.solutions[0].basis.constraint_status,
|
|
)
|
|
self.assertEqual(
|
|
solution.SolutionStatus.FEASIBLE,
|
|
res.solutions[0].basis.basic_dual_feasibility,
|
|
)
|
|
|
|
# Solution 1
|
|
assert res.solutions[1].primal_solution is not None
|
|
self.assertEqual(3.0, res.solutions[1].primal_solution.objective_value)
|
|
self.assertDictEqual(
|
|
{x: 3.0, y: 2.0}, res.solutions[1].primal_solution.variable_values
|
|
)
|
|
self.assertEqual(
|
|
solution.SolutionStatus.INFEASIBLE,
|
|
res.solutions[1].primal_solution.feasibility_status,
|
|
)
|
|
self.assertIsNone(res.solutions[1].dual_solution)
|
|
self.assertIsNone(res.solutions[1].basis)
|
|
|
|
# Solution 2
|
|
assert res.solutions[2].dual_solution is not None
|
|
self.assertIsNone(res.solutions[2].primal_solution)
|
|
self.assertEqual(3.0, res.solutions[2].dual_solution.objective_value)
|
|
self.assertDictEqual(
|
|
{x: 11.0, y: 12.0}, res.solutions[2].dual_solution.reduced_costs
|
|
)
|
|
self.assertDictEqual({c: 5.0}, res.solutions[2].dual_solution.dual_values)
|
|
self.assertEqual(
|
|
solution.SolutionStatus.INFEASIBLE,
|
|
res.solutions[2].dual_solution.feasibility_status,
|
|
)
|
|
self.assertIsNone(res.solutions[2].basis)
|
|
|
|
# Solution 3
|
|
assert res.solutions[3].basis is not None
|
|
self.assertIsNone(res.solutions[3].primal_solution)
|
|
self.assertIsNone(res.solutions[3].dual_solution)
|
|
self.assertDictEqual(
|
|
{
|
|
x: solution.BasisStatus.AT_LOWER_BOUND,
|
|
y: solution.BasisStatus.AT_UPPER_BOUND,
|
|
},
|
|
res.solutions[3].basis.variable_status,
|
|
)
|
|
self.assertDictEqual(
|
|
{c: solution.BasisStatus.BASIC},
|
|
res.solutions[3].basis.constraint_status,
|
|
)
|
|
self.assertEqual(
|
|
solution.SolutionStatus.INFEASIBLE,
|
|
res.solutions[3].basis.basic_dual_feasibility,
|
|
)
|
|
|
|
# solve_stats
|
|
self.assertEqual(10, res.solve_stats.node_count)
|
|
self.assertEqual(
|
|
result.FeasibilityStatus.FEASIBLE,
|
|
res.termination.problem_status.primal_status,
|
|
)
|
|
self.assertEqual(
|
|
result.FeasibilityStatus.FEASIBLE,
|
|
res.termination.problem_status.dual_status,
|
|
)
|
|
self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible)
|
|
self.assertEqual(10, res.termination.objective_bounds.primal_bound)
|
|
self.assertEqual(20, res.termination.objective_bounds.dual_bound)
|
|
self.assertIsNone(res.gscip_specific_output)
|
|
|
|
def test_to_proto_round_trip(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
x = mod.add_binary_variable(name="x")
|
|
c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c")
|
|
|
|
s = solution.Solution(
|
|
primal_solution=solution.PrimalSolution(
|
|
variable_values={x: 1.0},
|
|
objective_value=2.0,
|
|
feasibility_status=solution.SolutionStatus.FEASIBLE,
|
|
)
|
|
)
|
|
r = result.SolveResult(
|
|
termination=result.Termination(
|
|
reason=result.TerminationReason.FEASIBLE,
|
|
limit=result.Limit.TIME,
|
|
problem_status=result.ProblemStatus(
|
|
primal_status=result.FeasibilityStatus.FEASIBLE,
|
|
dual_status=result.FeasibilityStatus.UNDETERMINED,
|
|
),
|
|
),
|
|
solve_stats=result.SolveStats(
|
|
node_count=3, solve_time=datetime.timedelta(seconds=4)
|
|
),
|
|
solutions=[s],
|
|
primal_rays=[solution.PrimalRay(variable_values={x: 4.0})],
|
|
dual_rays=[solution.DualRay(reduced_costs={x: 5.0}, dual_values={c: 6.0})],
|
|
)
|
|
|
|
s_proto = solution_pb2.SolutionProto(
|
|
primal_solution=solution_pb2.PrimalSolutionProto(
|
|
objective_value=2.0,
|
|
feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE,
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[1.0]
|
|
),
|
|
)
|
|
)
|
|
r_proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_FEASIBLE,
|
|
limit=result_pb2.LIMIT_TIME,
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED,
|
|
),
|
|
),
|
|
solve_stats=result_pb2.SolveStatsProto(node_count=3),
|
|
solutions=[s_proto],
|
|
primal_rays=[
|
|
solution_pb2.PrimalRayProto(
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[4.0]
|
|
)
|
|
)
|
|
],
|
|
dual_rays=[
|
|
solution_pb2.DualRayProto(
|
|
reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[5.0]
|
|
),
|
|
dual_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[0], values=[6.0]
|
|
),
|
|
)
|
|
],
|
|
)
|
|
r_proto.solve_stats.solve_time.FromTimedelta(datetime.timedelta(seconds=4))
|
|
|
|
self.assert_protos_equiv(r.to_proto(), r_proto)
|
|
self.assertEqual(result.parse_solve_result(r_proto, mod), r)
|
|
|
|
def test_solution_validation(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_OPTIMAL,
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
),
|
|
),
|
|
solutions=[
|
|
solution_pb2.SolutionProto(
|
|
primal_solution=solution_pb2.PrimalSolutionProto(
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[4.0]
|
|
),
|
|
feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE,
|
|
)
|
|
)
|
|
],
|
|
)
|
|
res = result.parse_solve_result(proto, mod, validate=False)
|
|
bad_var = mod.get_variable(2, validate=False)
|
|
self.assertLen(res.solutions, 1)
|
|
# TODO: b/215588365 - make a local variable so pytype is happy
|
|
primal = res.solutions[0].primal_solution
|
|
self.assertIsNotNone(primal)
|
|
self.assertDictEqual(primal.variable_values, {bad_var: 4.0})
|
|
with self.assertRaises(KeyError):
|
|
result.parse_solve_result(proto, mod, validate=True)
|
|
|
|
def test_primal_ray_validation(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_UNBOUNDED,
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
),
|
|
),
|
|
primal_rays=[
|
|
solution_pb2.PrimalRayProto(
|
|
variable_values=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[4.0]
|
|
)
|
|
)
|
|
],
|
|
)
|
|
res = result.parse_solve_result(proto, mod, validate=False)
|
|
bad_var = mod.get_variable(2, validate=False)
|
|
self.assertLen(res.primal_rays, 1)
|
|
self.assertDictEqual(res.primal_rays[0].variable_values, {bad_var: 4.0})
|
|
with self.assertRaises(KeyError):
|
|
result.parse_solve_result(proto, mod, validate=True)
|
|
|
|
def test_dual_ray_validation(self) -> None:
|
|
mod = model.Model(name="test_model")
|
|
proto = result_pb2.SolveResultProto(
|
|
termination=result_pb2.TerminationProto(
|
|
reason=result_pb2.TERMINATION_REASON_INFEASIBLE,
|
|
problem_status=result_pb2.ProblemStatusProto(
|
|
primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE,
|
|
dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE,
|
|
),
|
|
),
|
|
dual_rays=[
|
|
solution_pb2.DualRayProto(
|
|
reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto(
|
|
ids=[2], values=[4.0]
|
|
)
|
|
)
|
|
],
|
|
)
|
|
res = result.parse_solve_result(proto, mod, validate=False)
|
|
bad_var = mod.get_variable(2, validate=False)
|
|
self.assertLen(res.dual_rays, 1)
|
|
self.assertDictEqual(res.dual_rays[0].reduced_costs, {bad_var: 4.0})
|
|
with self.assertRaises(KeyError):
|
|
result.parse_solve_result(proto, mod, validate=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
absltest.main()
|