small cleaning
This commit is contained in:
@@ -51,66 +51,6 @@ py_library(
|
||||
deps = ["//ortools/math_opt/python/elemental"],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "model_storage",
|
||||
srcs = ["model_storage.py"],
|
||||
visibility = ["//ortools/math_opt/python:__subpackages__"],
|
||||
deps = [
|
||||
"//ortools/math_opt:model_py_pb2",
|
||||
"//ortools/math_opt:model_update_py_pb2",
|
||||
],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "hash_model_storage",
|
||||
srcs = ["hash_model_storage.py"],
|
||||
deps = [
|
||||
":model_storage",
|
||||
"//ortools/math_opt:model_py_pb2",
|
||||
"//ortools/math_opt:model_update_py_pb2",
|
||||
"//ortools/math_opt:sparse_containers_py_pb2",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "hash_model_storage_test",
|
||||
size = "small",
|
||||
srcs = ["hash_model_storage_test.py"],
|
||||
deps = [
|
||||
":hash_model_storage",
|
||||
requirement("absl-py"),
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "model_storage_test",
|
||||
size = "small",
|
||||
srcs = ["model_storage_test.py"],
|
||||
deps = [
|
||||
":hash_model_storage",
|
||||
":model_storage",
|
||||
requirement("absl-py"),
|
||||
"//ortools/math_opt:model_py_pb2",
|
||||
"//ortools/math_opt:sparse_containers_py_pb2",
|
||||
"//ortools/math_opt/python/testing:compare_proto",
|
||||
],
|
||||
)
|
||||
|
||||
py_test(
|
||||
name = "model_storage_update_test",
|
||||
size = "small",
|
||||
srcs = ["model_storage_update_test.py"],
|
||||
deps = [
|
||||
":hash_model_storage",
|
||||
":model_storage",
|
||||
requirement("absl-py"),
|
||||
"//ortools/math_opt:model_py_pb2",
|
||||
"//ortools/math_opt:model_update_py_pb2",
|
||||
"//ortools/math_opt:sparse_containers_py_pb2",
|
||||
"//ortools/math_opt/python/testing:compare_proto",
|
||||
],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "model",
|
||||
srcs = ["model.py"],
|
||||
|
||||
@@ -1,843 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""A minimal pure python implementation of model_storage.ModelStorage."""
|
||||
|
||||
from typing import Dict, Iterable, Iterator, Optional, Set, Tuple
|
||||
import weakref
|
||||
|
||||
from ortools.math_opt import model_pb2
|
||||
from ortools.math_opt import model_update_pb2
|
||||
from ortools.math_opt import sparse_containers_pb2
|
||||
from ortools.math_opt.python import model_storage
|
||||
|
||||
_QuadraticKey = model_storage.QuadraticTermIdKey
|
||||
|
||||
|
||||
class _UpdateTracker(model_storage.StorageUpdateTracker):
|
||||
"""Tracks model updates for HashModelStorage."""
|
||||
|
||||
def __init__(self, mod: "HashModelStorage"):
|
||||
self.retired: bool = False
|
||||
self.model: "HashModelStorage" = mod
|
||||
# Changes for variables with id < variables_checkpoint are explicitly
|
||||
# tracked.
|
||||
self.variables_checkpoint: int = self.model._next_var_id
|
||||
# Changes for linear constraints with id < linear_constraints_checkpoint
|
||||
# are explicitly tracked.
|
||||
self.linear_constraints_checkpoint: int = self.model._next_lin_con_id
|
||||
|
||||
self.objective_direction: bool = False
|
||||
self.objective_offset: bool = False
|
||||
|
||||
self.variable_deletes: Set[int] = set()
|
||||
self.variable_lbs: Set[int] = set()
|
||||
self.variable_ubs: Set[int] = set()
|
||||
self.variable_integers: Set[int] = set()
|
||||
|
||||
self.linear_objective_coefficients: Set[int] = set()
|
||||
self.quadratic_objective_coefficients: Set[_QuadraticKey] = set()
|
||||
|
||||
self.linear_constraint_deletes: Set[int] = set()
|
||||
self.linear_constraint_lbs: Set[int] = set()
|
||||
self.linear_constraint_ubs: Set[int] = set()
|
||||
|
||||
self.linear_constraint_matrix: Set[Tuple[int, int]] = set()
|
||||
|
||||
def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]:
|
||||
if self.retired:
|
||||
raise model_storage.UsedUpdateTrackerAfterRemovalError()
|
||||
if (
|
||||
self.variables_checkpoint == self.model.next_variable_id()
|
||||
and (
|
||||
self.linear_constraints_checkpoint
|
||||
== self.model.next_linear_constraint_id()
|
||||
)
|
||||
and not self.objective_direction
|
||||
and not self.objective_offset
|
||||
and not self.variable_deletes
|
||||
and not self.variable_lbs
|
||||
and not self.variable_ubs
|
||||
and not self.variable_integers
|
||||
and not self.linear_objective_coefficients
|
||||
and not self.quadratic_objective_coefficients
|
||||
and not self.linear_constraint_deletes
|
||||
and not self.linear_constraint_lbs
|
||||
and not self.linear_constraint_ubs
|
||||
and not self.linear_constraint_matrix
|
||||
):
|
||||
return None
|
||||
result = model_update_pb2.ModelUpdateProto()
|
||||
result.deleted_variable_ids[:] = sorted(self.variable_deletes)
|
||||
result.deleted_linear_constraint_ids[:] = sorted(self.linear_constraint_deletes)
|
||||
# Variable updates
|
||||
_set_sparse_double_vector(
|
||||
sorted((vid, self.model.get_variable_lb(vid)) for vid in self.variable_lbs),
|
||||
result.variable_updates.lower_bounds,
|
||||
)
|
||||
_set_sparse_double_vector(
|
||||
sorted((vid, self.model.get_variable_ub(vid)) for vid in self.variable_ubs),
|
||||
result.variable_updates.upper_bounds,
|
||||
)
|
||||
_set_sparse_bool_vector(
|
||||
sorted(
|
||||
(vid, self.model.get_variable_is_integer(vid))
|
||||
for vid in self.variable_integers
|
||||
),
|
||||
result.variable_updates.integers,
|
||||
)
|
||||
# Linear constraint updates
|
||||
_set_sparse_double_vector(
|
||||
sorted(
|
||||
(cid, self.model.get_linear_constraint_lb(cid))
|
||||
for cid in self.linear_constraint_lbs
|
||||
),
|
||||
result.linear_constraint_updates.lower_bounds,
|
||||
)
|
||||
_set_sparse_double_vector(
|
||||
sorted(
|
||||
(cid, self.model.get_linear_constraint_ub(cid))
|
||||
for cid in self.linear_constraint_ubs
|
||||
),
|
||||
result.linear_constraint_updates.upper_bounds,
|
||||
)
|
||||
# New variables and constraints
|
||||
new_vars = []
|
||||
for vid in range(self.variables_checkpoint, self.model.next_variable_id()):
|
||||
var = self.model.variables.get(vid)
|
||||
if var is not None:
|
||||
new_vars.append((vid, var))
|
||||
_variables_to_proto(new_vars, result.new_variables)
|
||||
new_lin_cons = []
|
||||
for lin_con_id in range(
|
||||
self.linear_constraints_checkpoint,
|
||||
self.model.next_linear_constraint_id(),
|
||||
):
|
||||
lin_con = self.model.linear_constraints.get(lin_con_id)
|
||||
if lin_con is not None:
|
||||
new_lin_cons.append((lin_con_id, lin_con))
|
||||
_linear_constraints_to_proto(new_lin_cons, result.new_linear_constraints)
|
||||
# Objective update
|
||||
if self.objective_direction:
|
||||
result.objective_updates.direction_update = self.model.get_is_maximize()
|
||||
if self.objective_offset:
|
||||
result.objective_updates.offset_update = self.model.get_objective_offset()
|
||||
_set_sparse_double_vector(
|
||||
sorted(
|
||||
(var, self.model.get_linear_objective_coefficient(var))
|
||||
for var in self.linear_objective_coefficients
|
||||
),
|
||||
result.objective_updates.linear_coefficients,
|
||||
)
|
||||
for new_var in range(self.variables_checkpoint, self.model.next_variable_id()):
|
||||
# NOTE: the value will be 0.0 if either the coefficient is not set or the
|
||||
# variable has been deleted. Calling
|
||||
# model.get_linear_objective_coefficient() throws an exception if the
|
||||
# variable has been deleted.
|
||||
obj_coef = self.model.linear_objective_coefficient.get(new_var, 0.0)
|
||||
if obj_coef:
|
||||
result.objective_updates.linear_coefficients.ids.append(new_var)
|
||||
result.objective_updates.linear_coefficients.values.append(obj_coef)
|
||||
|
||||
quadratic_objective_updates = [
|
||||
(
|
||||
key.id1,
|
||||
key.id2,
|
||||
self.model.get_quadratic_objective_coefficient(key.id1, key.id2),
|
||||
)
|
||||
for key in self.quadratic_objective_coefficients
|
||||
]
|
||||
for new_var in range(self.variables_checkpoint, self.model.next_variable_id()):
|
||||
if self.model.variable_exists(new_var):
|
||||
for other_var in self.model.get_quadratic_objective_adjacent_variables(
|
||||
new_var
|
||||
):
|
||||
key = _QuadraticKey(new_var, other_var)
|
||||
if new_var >= other_var:
|
||||
key = _QuadraticKey(new_var, other_var)
|
||||
quadratic_objective_updates.append(
|
||||
(
|
||||
key.id1,
|
||||
key.id2,
|
||||
self.model.get_quadratic_objective_coefficient(
|
||||
key.id1, key.id2
|
||||
),
|
||||
)
|
||||
)
|
||||
quadratic_objective_updates.sort()
|
||||
if quadratic_objective_updates:
|
||||
first_var_ids, second_var_ids, coefficients = zip(
|
||||
*quadratic_objective_updates
|
||||
)
|
||||
result.objective_updates.quadratic_coefficients.row_ids[:] = first_var_ids
|
||||
result.objective_updates.quadratic_coefficients.column_ids[:] = (
|
||||
second_var_ids
|
||||
)
|
||||
result.objective_updates.quadratic_coefficients.coefficients[:] = (
|
||||
coefficients
|
||||
)
|
||||
# Linear constraint matrix updates
|
||||
matrix_updates = [
|
||||
(l, v, self.model.get_linear_constraint_coefficient(l, v))
|
||||
for (l, v) in self.linear_constraint_matrix
|
||||
]
|
||||
for new_var in range(self.variables_checkpoint, self.model.next_variable_id()):
|
||||
if self.model.variable_exists(new_var):
|
||||
for lin_con in self.model.get_linear_constraints_with_variable(new_var):
|
||||
matrix_updates.append(
|
||||
(
|
||||
lin_con,
|
||||
new_var,
|
||||
self.model.get_linear_constraint_coefficient(
|
||||
lin_con, new_var
|
||||
),
|
||||
)
|
||||
)
|
||||
for new_lin_con in range(
|
||||
self.linear_constraints_checkpoint,
|
||||
self.model.next_linear_constraint_id(),
|
||||
):
|
||||
if self.model.linear_constraint_exists(new_lin_con):
|
||||
for var in self.model.get_variables_for_linear_constraint(new_lin_con):
|
||||
# We have already gotten the new variables above. Note that we do at
|
||||
# most twice as much work as we should from this.
|
||||
if var < self.variables_checkpoint:
|
||||
matrix_updates.append(
|
||||
(
|
||||
new_lin_con,
|
||||
var,
|
||||
self.model.get_linear_constraint_coefficient(
|
||||
new_lin_con, var
|
||||
),
|
||||
)
|
||||
)
|
||||
matrix_updates.sort()
|
||||
if matrix_updates:
|
||||
lin_cons, variables, coefs = zip(*matrix_updates)
|
||||
result.linear_constraint_matrix_updates.row_ids[:] = lin_cons
|
||||
result.linear_constraint_matrix_updates.column_ids[:] = variables
|
||||
result.linear_constraint_matrix_updates.coefficients[:] = coefs
|
||||
return result
|
||||
|
||||
def advance_checkpoint(self) -> None:
|
||||
if self.retired:
|
||||
raise model_storage.UsedUpdateTrackerAfterRemovalError()
|
||||
self.objective_direction = False
|
||||
self.objective_offset = False
|
||||
self.variable_deletes = set()
|
||||
self.variable_lbs = set()
|
||||
self.variable_ubs = set()
|
||||
self.variable_integers = set()
|
||||
self.linear_objective_coefficients = set()
|
||||
self.linear_constraint_deletes = set()
|
||||
self.linear_constraint_lbs = set()
|
||||
self.linear_constraint_ubs = set()
|
||||
self.linear_constraint_matrix = set()
|
||||
|
||||
self.variables_checkpoint = self.model.next_variable_id()
|
||||
self.linear_constraints_checkpoint = self.model.next_linear_constraint_id()
|
||||
|
||||
|
||||
class _VariableStorage:
|
||||
"""Data specific to each decision variable in the optimization problem."""
|
||||
|
||||
def __init__(self, lb: float, ub: float, is_integer: bool, name: str) -> None:
|
||||
self.lower_bound: float = lb
|
||||
self.upper_bound: float = ub
|
||||
self.is_integer: bool = is_integer
|
||||
self.name: str = name
|
||||
self.linear_constraint_nonzeros: Set[int] = set()
|
||||
|
||||
|
||||
class _LinearConstraintStorage:
|
||||
"""Data specific to each linear constraint in the optimization problem."""
|
||||
|
||||
def __init__(self, lb: float, ub: float, name: str) -> None:
|
||||
self.lower_bound: float = lb
|
||||
self.upper_bound: float = ub
|
||||
self.name: str = name
|
||||
self.variable_nonzeros: Set[int] = set()
|
||||
|
||||
|
||||
class _QuadraticTermStorage:
|
||||
"""Data describing quadratic terms with non-zero coefficients."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._coefficients: Dict[_QuadraticKey, float] = {}
|
||||
# For a variable i that does not appear in a quadratic objective term with
|
||||
# a non-zero coefficient, we may have self._adjacent_variable[i] being an
|
||||
# empty set or i not appearing in self._adjacent_variable.keys() (e.g.
|
||||
# depeding on whether the variable previously appeared in a quadratic term).
|
||||
self._adjacent_variables: Dict[int, Set[int]] = {}
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
"""Returns true if and only if there are any quadratic terms with non-zero coefficients."""
|
||||
return bool(self._coefficients)
|
||||
|
||||
def get_adjacent_variables(self, variable_id: int) -> Iterator[int]:
|
||||
"""Yields the variables multiplying a variable in the stored quadratic terms.
|
||||
|
||||
If variable_id is not in the model the function yields the empty set.
|
||||
|
||||
Args:
|
||||
variable_id: Function yields the variables multiplying variable_id in the
|
||||
stored quadratic terms.
|
||||
|
||||
Yields:
|
||||
The variables multiplying variable_id in the stored quadratic terms.
|
||||
"""
|
||||
yield from self._adjacent_variables.get(variable_id, ())
|
||||
|
||||
def keys(self) -> Iterator[_QuadraticKey]:
|
||||
"""Yields the variable-pair keys associated to the stored quadratic terms."""
|
||||
yield from self._coefficients.keys()
|
||||
|
||||
def coefficients(self) -> Iterator[model_storage.QuadraticEntry]:
|
||||
"""Yields the stored quadratic terms as QuadraticEntry."""
|
||||
for key, coef in self._coefficients.items():
|
||||
yield model_storage.QuadraticEntry(id_key=key, coefficient=coef)
|
||||
|
||||
def delete_variable(self, variable_id: int) -> None:
|
||||
"""Updates the data structure to consider variable_id as deleted."""
|
||||
if variable_id not in self._adjacent_variables.keys():
|
||||
return
|
||||
for adjacent_variable_id in self._adjacent_variables[variable_id]:
|
||||
if variable_id != adjacent_variable_id:
|
||||
self._adjacent_variables[adjacent_variable_id].remove(variable_id)
|
||||
del self._coefficients[_QuadraticKey(variable_id, adjacent_variable_id)]
|
||||
self._adjacent_variables[variable_id].clear()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clears the data structure."""
|
||||
self._coefficients.clear()
|
||||
self._adjacent_variables.clear()
|
||||
|
||||
def set_coefficient(
|
||||
self, first_variable_id: int, second_variable_id: int, value: float
|
||||
) -> bool:
|
||||
"""Sets the coefficient for the quadratic term associated to the product between two variables.
|
||||
|
||||
The ordering of the input variables does not matter.
|
||||
|
||||
Args:
|
||||
first_variable_id: The first variable in the product.
|
||||
second_variable_id: The second variable in the product.
|
||||
value: The value of the coefficient.
|
||||
|
||||
Returns:
|
||||
True if the coefficient is updated, False otherwise.
|
||||
"""
|
||||
key = _QuadraticKey(first_variable_id, second_variable_id)
|
||||
if value == self._coefficients.get(key, 0.0):
|
||||
return False
|
||||
if value == 0.0:
|
||||
# Assuming self._coefficients/_adjacent_variables are filled according
|
||||
# to get_coefficient(key) != 0.0.
|
||||
del self._coefficients[key]
|
||||
self._adjacent_variables[first_variable_id].remove(second_variable_id)
|
||||
if first_variable_id != second_variable_id:
|
||||
self._adjacent_variables[second_variable_id].remove(first_variable_id)
|
||||
else:
|
||||
if first_variable_id not in self._adjacent_variables.keys():
|
||||
self._adjacent_variables[first_variable_id] = set()
|
||||
if second_variable_id not in self._adjacent_variables.keys():
|
||||
self._adjacent_variables[second_variable_id] = set()
|
||||
self._coefficients[key] = value
|
||||
self._adjacent_variables[first_variable_id].add(second_variable_id)
|
||||
self._adjacent_variables[second_variable_id].add(first_variable_id)
|
||||
return True
|
||||
|
||||
def get_coefficient(self, first_variable_id: int, second_variable_id: int) -> float:
|
||||
"""Gets the objective coefficient for the quadratic term associated to the product between two variables.
|
||||
|
||||
The ordering of the input variables does not matter.
|
||||
|
||||
Args:
|
||||
first_variable_id: The first variable in the product.
|
||||
second_variable_id: The second variable in the product.
|
||||
|
||||
Returns:
|
||||
The value of the coefficient.
|
||||
"""
|
||||
return self._coefficients.get(
|
||||
_QuadraticKey(first_variable_id, second_variable_id), 0.0
|
||||
)
|
||||
|
||||
|
||||
class HashModelStorage(model_storage.ModelStorage):
|
||||
"""A simple, pure python implementation of ModelStorage.
|
||||
|
||||
Attributes:
|
||||
_linear_constraint_matrix: A dictionary with (linear_constraint_id,
|
||||
variable_id) keys and numeric values, representing the matrix A for the
|
||||
constraints lb_c <= A*x <= ub_c. Invariant: the values have no zeros.
|
||||
linear_objective_coefficient: A dictionary with variable_id keys and
|
||||
numeric values, representing the linear terms in the objective.
|
||||
Invariant: the values have no zeros.
|
||||
_quadratic_objective_coefficients: A data structure containing quadratic
|
||||
terms in the objective.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "") -> None:
|
||||
super().__init__()
|
||||
self._name: str = name
|
||||
self.variables: Dict[int, _VariableStorage] = {}
|
||||
self.linear_constraints: Dict[int, _LinearConstraintStorage] = {}
|
||||
self._linear_constraint_matrix: Dict[Tuple[int, int], float] = {} #
|
||||
self._is_maximize: bool = False
|
||||
self._objective_offset: float = 0.0
|
||||
self.linear_objective_coefficient: Dict[int, float] = {}
|
||||
self._quadratic_objective_coefficients: _QuadraticTermStorage = (
|
||||
_QuadraticTermStorage()
|
||||
)
|
||||
self._next_var_id: int = 0
|
||||
self._next_lin_con_id: int = 0
|
||||
self._update_trackers: weakref.WeakSet[_UpdateTracker] = weakref.WeakSet()
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def add_variable(self, lb: float, ub: float, is_integer: bool, name: str) -> int:
|
||||
var_id = self._next_var_id
|
||||
self._next_var_id += 1
|
||||
self.variables[var_id] = _VariableStorage(lb, ub, is_integer, name)
|
||||
return var_id
|
||||
|
||||
def delete_variable(self, variable_id: int) -> None:
|
||||
self._check_variable_id(variable_id)
|
||||
variable = self.variables[variable_id]
|
||||
# First update the watchers
|
||||
for watcher in self._update_trackers:
|
||||
if variable_id < watcher.variables_checkpoint:
|
||||
watcher.variable_deletes.add(variable_id)
|
||||
watcher.variable_lbs.discard(variable_id)
|
||||
watcher.variable_ubs.discard(variable_id)
|
||||
watcher.variable_integers.discard(variable_id)
|
||||
watcher.linear_objective_coefficients.discard(variable_id)
|
||||
for (
|
||||
other_variable_id
|
||||
) in self._quadratic_objective_coefficients.get_adjacent_variables(
|
||||
variable_id
|
||||
):
|
||||
key = _QuadraticKey(variable_id, other_variable_id)
|
||||
watcher.quadratic_objective_coefficients.discard(key)
|
||||
for lin_con_id in variable.linear_constraint_nonzeros:
|
||||
if lin_con_id < watcher.linear_constraints_checkpoint:
|
||||
watcher.linear_constraint_matrix.discard(
|
||||
(lin_con_id, variable_id)
|
||||
)
|
||||
# Then update self.
|
||||
for lin_con_id in variable.linear_constraint_nonzeros:
|
||||
self.linear_constraints[lin_con_id].variable_nonzeros.remove(variable_id)
|
||||
del self._linear_constraint_matrix[(lin_con_id, variable_id)]
|
||||
del self.variables[variable_id]
|
||||
self.linear_objective_coefficient.pop(variable_id, None)
|
||||
self._quadratic_objective_coefficients.delete_variable(variable_id)
|
||||
|
||||
def variable_exists(self, variable_id: int) -> bool:
|
||||
return variable_id in self.variables
|
||||
|
||||
def next_variable_id(self) -> int:
|
||||
return self._next_var_id
|
||||
|
||||
def set_variable_lb(self, variable_id: int, lb: float) -> None:
|
||||
self._check_variable_id(variable_id)
|
||||
if lb == self.variables[variable_id].lower_bound:
|
||||
return
|
||||
self.variables[variable_id].lower_bound = lb
|
||||
for watcher in self._update_trackers:
|
||||
if variable_id < watcher.variables_checkpoint:
|
||||
watcher.variable_lbs.add(variable_id)
|
||||
|
||||
def set_variable_ub(self, variable_id: int, ub: float) -> None:
|
||||
self._check_variable_id(variable_id)
|
||||
if ub == self.variables[variable_id].upper_bound:
|
||||
return
|
||||
self.variables[variable_id].upper_bound = ub
|
||||
for watcher in self._update_trackers:
|
||||
if variable_id < watcher.variables_checkpoint:
|
||||
watcher.variable_ubs.add(variable_id)
|
||||
|
||||
def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None:
|
||||
self._check_variable_id(variable_id)
|
||||
if is_integer == self.variables[variable_id].is_integer:
|
||||
return
|
||||
self.variables[variable_id].is_integer = is_integer
|
||||
for watcher in self._update_trackers:
|
||||
if variable_id < watcher.variables_checkpoint:
|
||||
watcher.variable_integers.add(variable_id)
|
||||
|
||||
def get_variable_lb(self, variable_id: int) -> float:
|
||||
self._check_variable_id(variable_id)
|
||||
return self.variables[variable_id].lower_bound
|
||||
|
||||
def get_variable_ub(self, variable_id: int) -> float:
|
||||
self._check_variable_id(variable_id)
|
||||
return self.variables[variable_id].upper_bound
|
||||
|
||||
def get_variable_is_integer(self, variable_id: int) -> bool:
|
||||
self._check_variable_id(variable_id)
|
||||
return self.variables[variable_id].is_integer
|
||||
|
||||
def get_variable_name(self, variable_id: int) -> str:
|
||||
self._check_variable_id(variable_id)
|
||||
return self.variables[variable_id].name
|
||||
|
||||
def get_variables(self) -> Iterator[int]:
|
||||
yield from self.variables.keys()
|
||||
|
||||
def add_linear_constraint(self, lb: float, ub: float, name: str) -> int:
|
||||
lin_con_id = self._next_lin_con_id
|
||||
self._next_lin_con_id += 1
|
||||
self.linear_constraints[lin_con_id] = _LinearConstraintStorage(lb, ub, name)
|
||||
return lin_con_id
|
||||
|
||||
def delete_linear_constraint(self, linear_constraint_id: int) -> None:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
con = self.linear_constraints[linear_constraint_id]
|
||||
# First update the watchers
|
||||
for watcher in self._update_trackers:
|
||||
if linear_constraint_id < watcher.linear_constraints_checkpoint:
|
||||
watcher.linear_constraint_deletes.add(linear_constraint_id)
|
||||
watcher.linear_constraint_lbs.discard(linear_constraint_id)
|
||||
watcher.linear_constraint_ubs.discard(linear_constraint_id)
|
||||
for var_id in con.variable_nonzeros:
|
||||
if var_id < watcher.variables_checkpoint:
|
||||
watcher.linear_constraint_matrix.discard(
|
||||
(linear_constraint_id, var_id)
|
||||
)
|
||||
# Then update self.
|
||||
for var_id in con.variable_nonzeros:
|
||||
self.variables[var_id].linear_constraint_nonzeros.remove(
|
||||
linear_constraint_id
|
||||
)
|
||||
del self._linear_constraint_matrix[(linear_constraint_id, var_id)]
|
||||
del self.linear_constraints[linear_constraint_id]
|
||||
|
||||
def linear_constraint_exists(self, linear_constraint_id: int) -> bool:
|
||||
return linear_constraint_id in self.linear_constraints
|
||||
|
||||
def next_linear_constraint_id(self) -> int:
|
||||
return self._next_lin_con_id
|
||||
|
||||
def set_linear_constraint_lb(self, linear_constraint_id: int, lb: float) -> None:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
if lb == self.linear_constraints[linear_constraint_id].lower_bound:
|
||||
return
|
||||
self.linear_constraints[linear_constraint_id].lower_bound = lb
|
||||
for watcher in self._update_trackers:
|
||||
if linear_constraint_id < watcher.linear_constraints_checkpoint:
|
||||
watcher.linear_constraint_lbs.add(linear_constraint_id)
|
||||
|
||||
def set_linear_constraint_ub(self, linear_constraint_id: int, ub: float) -> None:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
if ub == self.linear_constraints[linear_constraint_id].upper_bound:
|
||||
return
|
||||
self.linear_constraints[linear_constraint_id].upper_bound = ub
|
||||
for watcher in self._update_trackers:
|
||||
if linear_constraint_id < watcher.linear_constraints_checkpoint:
|
||||
watcher.linear_constraint_ubs.add(linear_constraint_id)
|
||||
|
||||
def get_linear_constraint_lb(self, linear_constraint_id: int) -> float:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
return self.linear_constraints[linear_constraint_id].lower_bound
|
||||
|
||||
def get_linear_constraint_ub(self, linear_constraint_id: int) -> float:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
return self.linear_constraints[linear_constraint_id].upper_bound
|
||||
|
||||
def get_linear_constraint_name(self, linear_constraint_id: int) -> str:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
return self.linear_constraints[linear_constraint_id].name
|
||||
|
||||
def get_linear_constraints(self) -> Iterator[int]:
|
||||
yield from self.linear_constraints.keys()
|
||||
|
||||
def set_linear_constraint_coefficient(
|
||||
self, linear_constraint_id: int, variable_id: int, value: float
|
||||
) -> None:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
self._check_variable_id(variable_id)
|
||||
if value == self._linear_constraint_matrix.get(
|
||||
(linear_constraint_id, variable_id), 0.0
|
||||
):
|
||||
return
|
||||
if value == 0.0:
|
||||
self._linear_constraint_matrix.pop(
|
||||
(linear_constraint_id, variable_id), None
|
||||
)
|
||||
self.variables[variable_id].linear_constraint_nonzeros.discard(
|
||||
linear_constraint_id
|
||||
)
|
||||
self.linear_constraints[linear_constraint_id].variable_nonzeros.discard(
|
||||
variable_id
|
||||
)
|
||||
else:
|
||||
self._linear_constraint_matrix[(linear_constraint_id, variable_id)] = value
|
||||
self.variables[variable_id].linear_constraint_nonzeros.add(
|
||||
linear_constraint_id
|
||||
)
|
||||
self.linear_constraints[linear_constraint_id].variable_nonzeros.add(
|
||||
variable_id
|
||||
)
|
||||
for watcher in self._update_trackers:
|
||||
if (
|
||||
variable_id < watcher.variables_checkpoint
|
||||
and linear_constraint_id < watcher.linear_constraints_checkpoint
|
||||
):
|
||||
watcher.linear_constraint_matrix.add(
|
||||
(linear_constraint_id, variable_id)
|
||||
)
|
||||
|
||||
def get_linear_constraint_coefficient(
|
||||
self, linear_constraint_id: int, variable_id: int
|
||||
) -> float:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
self._check_variable_id(variable_id)
|
||||
return self._linear_constraint_matrix.get(
|
||||
(linear_constraint_id, variable_id), 0.0
|
||||
)
|
||||
|
||||
def get_linear_constraints_with_variable(self, variable_id: int) -> Iterator[int]:
|
||||
self._check_variable_id(variable_id)
|
||||
yield from self.variables[variable_id].linear_constraint_nonzeros
|
||||
|
||||
def get_variables_for_linear_constraint(
|
||||
self, linear_constraint_id: int
|
||||
) -> Iterator[int]:
|
||||
self._check_linear_constraint_id(linear_constraint_id)
|
||||
yield from self.linear_constraints[linear_constraint_id].variable_nonzeros
|
||||
|
||||
def get_linear_constraint_matrix_entries(
|
||||
self,
|
||||
) -> Iterator[model_storage.LinearConstraintMatrixIdEntry]:
|
||||
for (constraint, variable), coef in self._linear_constraint_matrix.items():
|
||||
yield model_storage.LinearConstraintMatrixIdEntry(
|
||||
linear_constraint_id=constraint,
|
||||
variable_id=variable,
|
||||
coefficient=coef,
|
||||
)
|
||||
|
||||
def clear_objective(self) -> None:
|
||||
for variable_id in self.linear_objective_coefficient:
|
||||
for watcher in self._update_trackers:
|
||||
if variable_id < watcher.variables_checkpoint:
|
||||
watcher.linear_objective_coefficients.add(variable_id)
|
||||
self.linear_objective_coefficient.clear()
|
||||
for key in self._quadratic_objective_coefficients.keys():
|
||||
for watcher in self._update_trackers:
|
||||
if key.id2 < watcher.variables_checkpoint:
|
||||
watcher.quadratic_objective_coefficients.add(key)
|
||||
self._quadratic_objective_coefficients.clear()
|
||||
self.set_objective_offset(0.0)
|
||||
|
||||
def set_linear_objective_coefficient(self, variable_id: int, value: float) -> None:
|
||||
self._check_variable_id(variable_id)
|
||||
if value == self.linear_objective_coefficient.get(variable_id, 0.0):
|
||||
return
|
||||
if value == 0.0:
|
||||
self.linear_objective_coefficient.pop(variable_id, None)
|
||||
else:
|
||||
self.linear_objective_coefficient[variable_id] = value
|
||||
for watcher in self._update_trackers:
|
||||
if variable_id < watcher.variables_checkpoint:
|
||||
watcher.linear_objective_coefficients.add(variable_id)
|
||||
|
||||
def get_linear_objective_coefficient(self, variable_id: int) -> float:
|
||||
self._check_variable_id(variable_id)
|
||||
return self.linear_objective_coefficient.get(variable_id, 0.0)
|
||||
|
||||
def get_linear_objective_coefficients(
|
||||
self,
|
||||
) -> Iterator[model_storage.LinearObjectiveEntry]:
|
||||
for var_id, coef in self.linear_objective_coefficient.items():
|
||||
yield model_storage.LinearObjectiveEntry(
|
||||
variable_id=var_id, coefficient=coef
|
||||
)
|
||||
|
||||
def set_quadratic_objective_coefficient(
|
||||
self, first_variable_id: int, second_variable_id: int, value: float
|
||||
) -> None:
|
||||
self._check_variable_id(first_variable_id)
|
||||
self._check_variable_id(second_variable_id)
|
||||
updated = self._quadratic_objective_coefficients.set_coefficient(
|
||||
first_variable_id, second_variable_id, value
|
||||
)
|
||||
if updated:
|
||||
for watcher in self._update_trackers:
|
||||
if (
|
||||
max(first_variable_id, second_variable_id)
|
||||
< watcher.variables_checkpoint
|
||||
):
|
||||
watcher.quadratic_objective_coefficients.add(
|
||||
_QuadraticKey(first_variable_id, second_variable_id)
|
||||
)
|
||||
|
||||
def get_quadratic_objective_coefficient(
|
||||
self, first_variable_id: int, second_variable_id: int
|
||||
) -> float:
|
||||
self._check_variable_id(first_variable_id)
|
||||
self._check_variable_id(second_variable_id)
|
||||
return self._quadratic_objective_coefficients.get_coefficient(
|
||||
first_variable_id, second_variable_id
|
||||
)
|
||||
|
||||
def get_quadratic_objective_coefficients(
|
||||
self,
|
||||
) -> Iterator[model_storage.QuadraticEntry]:
|
||||
yield from self._quadratic_objective_coefficients.coefficients()
|
||||
|
||||
def get_quadratic_objective_adjacent_variables(
|
||||
self, variable_id: int
|
||||
) -> Iterator[int]:
|
||||
self._check_variable_id(variable_id)
|
||||
yield from self._quadratic_objective_coefficients.get_adjacent_variables(
|
||||
variable_id
|
||||
)
|
||||
|
||||
def set_is_maximize(self, is_maximize: bool) -> None:
|
||||
if self._is_maximize == is_maximize:
|
||||
return
|
||||
self._is_maximize = is_maximize
|
||||
for watcher in self._update_trackers:
|
||||
watcher.objective_direction = True
|
||||
|
||||
def get_is_maximize(self) -> bool:
|
||||
return self._is_maximize
|
||||
|
||||
def set_objective_offset(self, offset: float) -> None:
|
||||
if self._objective_offset == offset:
|
||||
return
|
||||
self._objective_offset = offset
|
||||
for watcher in self._update_trackers:
|
||||
watcher.objective_offset = True
|
||||
|
||||
def get_objective_offset(self) -> float:
|
||||
return self._objective_offset
|
||||
|
||||
def export_model(self) -> model_pb2.ModelProto:
|
||||
m: model_pb2.ModelProto = model_pb2.ModelProto()
|
||||
m.name = self._name
|
||||
_variables_to_proto(self.variables.items(), m.variables)
|
||||
_linear_constraints_to_proto(
|
||||
self.linear_constraints.items(), m.linear_constraints
|
||||
)
|
||||
m.objective.maximize = self._is_maximize
|
||||
m.objective.offset = self._objective_offset
|
||||
if self.linear_objective_coefficient:
|
||||
obj_ids, obj_coefs = zip(*sorted(self.linear_objective_coefficient.items()))
|
||||
m.objective.linear_coefficients.ids.extend(obj_ids)
|
||||
m.objective.linear_coefficients.values.extend(obj_coefs)
|
||||
if self._quadratic_objective_coefficients:
|
||||
first_var_ids, second_var_ids, coefficients = zip(
|
||||
*sorted(
|
||||
[
|
||||
(entry.id_key.id1, entry.id_key.id2, entry.coefficient)
|
||||
for entry in self._quadratic_objective_coefficients.coefficients()
|
||||
]
|
||||
)
|
||||
)
|
||||
m.objective.quadratic_coefficients.row_ids.extend(first_var_ids)
|
||||
m.objective.quadratic_coefficients.column_ids.extend(second_var_ids)
|
||||
m.objective.quadratic_coefficients.coefficients.extend(coefficients)
|
||||
if self._linear_constraint_matrix:
|
||||
flat_matrix_items = [
|
||||
(con_id, var_id, coef)
|
||||
for ((con_id, var_id), coef) in self._linear_constraint_matrix.items()
|
||||
]
|
||||
lin_con_ids, var_ids, lin_con_coefs = zip(*sorted(flat_matrix_items))
|
||||
m.linear_constraint_matrix.row_ids.extend(lin_con_ids)
|
||||
m.linear_constraint_matrix.column_ids.extend(var_ids)
|
||||
m.linear_constraint_matrix.coefficients.extend(lin_con_coefs)
|
||||
return m
|
||||
|
||||
def add_update_tracker(self) -> model_storage.StorageUpdateTracker:
|
||||
tracker = _UpdateTracker(self)
|
||||
self._update_trackers.add(tracker)
|
||||
return tracker
|
||||
|
||||
def remove_update_tracker(
|
||||
self, tracker: model_storage.StorageUpdateTracker
|
||||
) -> None:
|
||||
self._update_trackers.remove(tracker)
|
||||
tracker.retired = True
|
||||
|
||||
def _check_variable_id(self, variable_id: int) -> None:
|
||||
if variable_id not in self.variables:
|
||||
raise model_storage.BadVariableIdError(variable_id)
|
||||
|
||||
def _check_linear_constraint_id(self, linear_constraint_id: int) -> None:
|
||||
if linear_constraint_id not in self.linear_constraints:
|
||||
raise model_storage.BadLinearConstraintIdError(linear_constraint_id)
|
||||
|
||||
|
||||
def _set_sparse_double_vector(
|
||||
id_value_pairs: Iterable[Tuple[int, float]],
|
||||
proto: sparse_containers_pb2.SparseDoubleVectorProto,
|
||||
) -> None:
|
||||
"""id_value_pairs must be sorted, proto is filled."""
|
||||
if not id_value_pairs:
|
||||
return
|
||||
ids, values = zip(*id_value_pairs)
|
||||
proto.ids[:] = ids
|
||||
proto.values[:] = values
|
||||
|
||||
|
||||
def _set_sparse_bool_vector(
|
||||
id_value_pairs: Iterable[Tuple[int, bool]],
|
||||
proto: sparse_containers_pb2.SparseBoolVectorProto,
|
||||
) -> None:
|
||||
"""id_value_pairs must be sorted, proto is filled."""
|
||||
if not id_value_pairs:
|
||||
return
|
||||
ids, values = zip(*id_value_pairs)
|
||||
proto.ids[:] = ids
|
||||
proto.values[:] = values
|
||||
|
||||
|
||||
def _variables_to_proto(
|
||||
variables: Iterable[Tuple[int, _VariableStorage]],
|
||||
proto: model_pb2.VariablesProto,
|
||||
) -> None:
|
||||
"""Exports variables to proto."""
|
||||
has_named_var = False
|
||||
for _, var_storage in variables:
|
||||
if var_storage.name:
|
||||
has_named_var = True
|
||||
break
|
||||
for var_id, var_storage in variables:
|
||||
proto.ids.append(var_id)
|
||||
proto.lower_bounds.append(var_storage.lower_bound)
|
||||
proto.upper_bounds.append(var_storage.upper_bound)
|
||||
proto.integers.append(var_storage.is_integer)
|
||||
if has_named_var:
|
||||
proto.names.append(var_storage.name)
|
||||
|
||||
|
||||
def _linear_constraints_to_proto(
|
||||
linear_constraints: Iterable[Tuple[int, _LinearConstraintStorage]],
|
||||
proto: model_pb2.LinearConstraintsProto,
|
||||
) -> None:
|
||||
"""Exports variables to proto."""
|
||||
has_named_lin_con = False
|
||||
for _, lin_con_storage in linear_constraints:
|
||||
if lin_con_storage.name:
|
||||
has_named_lin_con = True
|
||||
break
|
||||
for lin_con_id, lin_con_storage in linear_constraints:
|
||||
proto.ids.append(lin_con_id)
|
||||
proto.lower_bounds.append(lin_con_storage.lower_bound)
|
||||
proto.upper_bounds.append(lin_con_storage.upper_bound)
|
||||
if has_named_lin_con:
|
||||
proto.names.append(lin_con_storage.name)
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/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.
|
||||
|
||||
"""Tests for hash_model_storage that cannot be covered by model_storage_(update)_test."""
|
||||
|
||||
from absl.testing import absltest
|
||||
from ortools.math_opt.python import hash_model_storage
|
||||
|
||||
|
||||
class HashModelStorageTest(absltest.TestCase):
|
||||
|
||||
def test_quadratic_term_storage(self):
|
||||
storage = hash_model_storage._QuadraticTermStorage()
|
||||
storage.set_coefficient(0, 1, 1.0)
|
||||
storage.delete_variable(0)
|
||||
self.assertEmpty(list(storage.get_adjacent_variables(0)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
@@ -1,441 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""An interface for in memory storage of optimization problems."""
|
||||
|
||||
import abc
|
||||
import dataclasses
|
||||
from typing import Iterator, Optional, Type, TypeVar
|
||||
|
||||
from ortools.math_opt import model_pb2
|
||||
from ortools.math_opt import model_update_pb2
|
||||
|
||||
|
||||
# TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is
|
||||
# available.
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class LinearConstraintMatrixIdEntry:
|
||||
__slots__ = "linear_constraint_id", "variable_id", "coefficient"
|
||||
linear_constraint_id: int
|
||||
variable_id: int
|
||||
coefficient: float
|
||||
|
||||
|
||||
# TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is
|
||||
# available.
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class LinearObjectiveEntry:
|
||||
__slots__ = "variable_id", "coefficient"
|
||||
variable_id: int
|
||||
coefficient: float
|
||||
|
||||
|
||||
# TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is
|
||||
# available.
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class QuadraticTermIdKey:
|
||||
"""An ordered pair of ints used as a key for quadratic terms.
|
||||
|
||||
QuadraticTermIdKey.id1 <= QuadraticTermIdKey.id2.
|
||||
"""
|
||||
|
||||
__slots__ = "id1", "id2"
|
||||
id1: int
|
||||
id2: int
|
||||
|
||||
def __init__(self, a: int, b: int):
|
||||
"""Ints a and b will be ordered internally."""
|
||||
id1 = a
|
||||
id2 = b
|
||||
if id1 > id2:
|
||||
id1, id2 = id2, id1
|
||||
object.__setattr__(self, "id1", id1)
|
||||
object.__setattr__(self, "id2", id2)
|
||||
|
||||
|
||||
# TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is
|
||||
# available.
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class QuadraticEntry:
|
||||
"""Represents an id-indexed quadratic term."""
|
||||
|
||||
__slots__ = "id_key", "coefficient"
|
||||
id_key: QuadraticTermIdKey
|
||||
coefficient: float
|
||||
|
||||
|
||||
class StorageUpdateTracker(abc.ABC):
|
||||
"""Tracks updates to an optimization model from a ModelStorage.
|
||||
|
||||
Do not instantiate directly, instead create through
|
||||
ModelStorage.add_update_tracker().
|
||||
|
||||
Interacting with an update tracker after it has been removed from the model
|
||||
will result in an UsedUpdateTrackerAfterRemovalError error.
|
||||
|
||||
Example:
|
||||
mod = model_storage.ModelStorage()
|
||||
x = mod.add_variable(0.0, 1.0, True, 'x')
|
||||
y = mod.add_variable(0.0, 1.0, True, 'y')
|
||||
tracker = mod.add_update_tracker()
|
||||
mod.set_variable_ub(x, 3.0)
|
||||
tracker.export_update()
|
||||
=> "variable_updates: {upper_bounds: {ids: [0], values[3.0] }"
|
||||
mod.set_variable_ub(y, 2.0)
|
||||
tracker.export_update()
|
||||
=> "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }"
|
||||
tracker.advance_checkpoint()
|
||||
tracker.export_update()
|
||||
=> ""
|
||||
mod.set_variable_ub(y, 4.0)
|
||||
tracker.export_update()
|
||||
=> "variable_updates: {upper_bounds: {ids: [1], values[4.0] }"
|
||||
tracker.advance_checkpoint()
|
||||
mod.remove_update_tracker(tracker)
|
||||
=> ""
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]:
|
||||
"""Returns changes to the model since last call to checkpoint/creation, or None if no changes occurred."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def advance_checkpoint(self) -> None:
|
||||
"""Track changes to the model only after this function call."""
|
||||
pass
|
||||
|
||||
|
||||
class UsedUpdateTrackerAfterRemovalError(RuntimeError):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
"Attempted to use update tracker after removing it from model storage."
|
||||
)
|
||||
|
||||
|
||||
class BadVariableIdError(LookupError):
|
||||
"""Raised by ModelStorage when a bad variable id is given."""
|
||||
|
||||
def __init__(self, variable_id):
|
||||
super().__init__(f"Unexpected variable id: {variable_id}")
|
||||
self.id = variable_id
|
||||
|
||||
|
||||
class BadLinearConstraintIdError(LookupError):
|
||||
"""Raised by ModelStorage when a bad linear constraint id is given."""
|
||||
|
||||
def __init__(self, linear_constraint_id):
|
||||
super().__init__(f"Unexpected linear constraint id: {linear_constraint_id}")
|
||||
self.id = linear_constraint_id
|
||||
|
||||
|
||||
class ModelStorage(abc.ABC):
|
||||
"""An interface for in memory storage of an optimization model.
|
||||
|
||||
Most users should not use this class directly and use Model defined in
|
||||
model.py.
|
||||
|
||||
Stores an mixed integer programming problem of the form:
|
||||
|
||||
{max/min} c*x + d
|
||||
s.t. lb_c <= A * x <= ub_c
|
||||
lb_v <= x <= ub_v
|
||||
x_i integer for i in I
|
||||
|
||||
where x is a vector of n decision variables, d is a number, lb_v, ub_v, and c
|
||||
are vectors of n numbers, lb_c and ub_c are vectors of m numbers, A is a
|
||||
m by n matrix, and I is a subset of {1,..., n}.
|
||||
|
||||
Each of the n variables and m constraints have an integer id that you use to
|
||||
get/set the problem data (c, A, lb_c etc.). Ids begin at zero and increase
|
||||
sequentially. They are not reused after deletion. Note that if a variable is
|
||||
deleted, your model has nonconsecutive variable ids.
|
||||
|
||||
For all methods taking an id (e.g. set_variable_lb), providing a bad id
|
||||
(including the id of a deleted variable) will raise a BadVariableIdError or
|
||||
BadLinearConstraintIdError. Further, the ModelStorage instance is assumed to
|
||||
be in a bad state after any such error and there are no guarantees on further
|
||||
interactions.
|
||||
|
||||
All implementations must have a constructor taking a str argument for the
|
||||
model name with a default value of the empty string.
|
||||
|
||||
Any ModelStorage can be exported to model_pb2.ModelProto, the format consumed
|
||||
by MathOpt solvers. Changes to a model can be exported to a
|
||||
model_update_pb2.ModelUpdateProto with an UpdateTracker, see the UpdateTracker
|
||||
documentation for details.
|
||||
|
||||
When solving this optimization problem we will additionally require that:
|
||||
* No numbers are NaN,
|
||||
* c, d, and A are all finite,
|
||||
* lb_c and lb_v are not +inf,
|
||||
* ub_c and ub_v are not -inf,
|
||||
but those assumptions are not checked or enforced here (NaNs and infinite
|
||||
values can be used anywhere).
|
||||
"""
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def name(self) -> str:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_variable(self, lb: float, ub: float, is_integer: bool, name: str) -> int:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_variable(self, variable_id: int) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def variable_exists(self, variable_id: int) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def next_variable_id(self) -> int:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_variable_lb(self, variable_id: int, lb: float) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_variable_ub(self, variable_id: int, ub: float) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_variable_lb(self, variable_id: int) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_variable_ub(self, variable_id: int) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_variable_is_integer(self, variable_id: int) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_variable_name(self, variable_id: int) -> str:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_variables(self) -> Iterator[int]:
|
||||
"""Yields the variable ids in order of creation."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_linear_constraint(self, lb: float, ub: float, name: str) -> int:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def delete_linear_constraint(self, linear_constraint_id: int) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def linear_constraint_exists(self, linear_constraint_id: int) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def next_linear_constraint_id(self) -> int:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_linear_constraint_lb(self, linear_constraint_id: int, lb: float) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_linear_constraint_ub(self, linear_constraint_id: int, ub: float) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraint_lb(self, linear_constraint_id: int) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraint_ub(self, linear_constraint_id: int) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraint_name(self, linear_constraint_id: int) -> str:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraints(self) -> Iterator[int]:
|
||||
"""Yields the linear constraint ids in order of creation."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_linear_constraint_coefficient(
|
||||
self, linear_constraint_id: int, variable_id: int, lb: float
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraint_coefficient(
|
||||
self, linear_constraint_id: int, variable_id: int
|
||||
) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraints_with_variable(self, variable_id: int) -> Iterator[int]:
|
||||
"""Yields the linear constraints with nonzero coefficient for a variable in undefined order."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_variables_for_linear_constraint(
|
||||
self, linear_constraint_id: int
|
||||
) -> Iterator[int]:
|
||||
"""Yields the variables with nonzero coefficient in a linear constraint in undefined order."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_constraint_matrix_entries(
|
||||
self,
|
||||
) -> Iterator[LinearConstraintMatrixIdEntry]:
|
||||
"""Yields the nonzero elements of the linear constraint matrix in undefined order."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def clear_objective(self) -> None:
|
||||
"""Clears objective coefficients and offset. Does not change direction."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_linear_objective_coefficient(self, variable_id: int, value: float) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_objective_coefficient(self, variable_id: int) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_linear_objective_coefficients(self) -> Iterator[LinearObjectiveEntry]:
|
||||
"""Yields the nonzero linear objective terms in undefined order."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_quadratic_objective_coefficient(
|
||||
self, first_variable_id: int, second_variable_id: int, value: float
|
||||
) -> None:
|
||||
"""Sets the objective coefficient for the product of two variables.
|
||||
|
||||
The ordering of the input variables does not matter.
|
||||
|
||||
Args:
|
||||
first_variable_id: The first variable in the product.
|
||||
second_variable_id: The second variable in the product.
|
||||
value: The value of the coefficient.
|
||||
|
||||
Raises:
|
||||
BadVariableIdError if first_variable_id or second_variable_id are not in
|
||||
the model.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_quadratic_objective_coefficient(
|
||||
self, first_variable_id: int, second_variable_id: int
|
||||
) -> float:
|
||||
"""Gets the objective coefficient for the product of two variables.
|
||||
|
||||
The ordering of the input variables does not matter.
|
||||
|
||||
Args:
|
||||
first_variable_id: The first variable in the product.
|
||||
second_variable_id: The second variable in the product.
|
||||
|
||||
Raises:
|
||||
BadVariableIdError if first_variable_id or second_variable_id are not in
|
||||
the model.
|
||||
|
||||
Returns:
|
||||
The value of the coefficient.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_quadratic_objective_coefficients(self) -> Iterator[QuadraticEntry]:
|
||||
"""Yields the nonzero quadratic objective terms in undefined order."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_quadratic_objective_adjacent_variables(
|
||||
self, variable_id: int
|
||||
) -> Iterator[int]:
|
||||
"""Yields the variables multiplying a variable in the objective function.
|
||||
|
||||
Variables are returned in an unspecified order.
|
||||
|
||||
For example, if variables x and y have ids 0 and 1 respectively, and the
|
||||
quadratic portion of the objective is x^2 + 2 x*y, then
|
||||
get_quadratic_objective_adjacent_variables(0) = (0, 1).
|
||||
|
||||
Args:
|
||||
variable_id: Function yields the variables multiplying variable_id in the
|
||||
objective function.
|
||||
|
||||
Yields:
|
||||
The variables multiplying variable_id in the objective function.
|
||||
|
||||
Raises:
|
||||
BadVariableIdError if variable_id is not in the model.
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_is_maximize(self, is_maximize: bool) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_is_maximize(self) -> bool:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def set_objective_offset(self, offset: float) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_objective_offset(self) -> float:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def export_model(self) -> model_pb2.ModelProto:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_update_tracker(self) -> StorageUpdateTracker:
|
||||
"""Creates a StorageUpdateTracker registered with self to view model changes."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def remove_update_tracker(self, tracker: StorageUpdateTracker):
|
||||
"""Stops tracker from getting updates on model changes in self.
|
||||
|
||||
An error will be raised if tracker is not a StorageUpdateTracker created by
|
||||
this Model that has not previously been removed.
|
||||
|
||||
Using an UpdateTracker (via checkpoint or export_update) after it has been
|
||||
removed will result in an error.
|
||||
|
||||
Args:
|
||||
tracker: The StorageUpdateTracker to unregister.
|
||||
|
||||
Raises:
|
||||
KeyError: The tracker was created by another model or was already removed.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
ModelStorageImpl = TypeVar("ModelStorageImpl", bound=ModelStorage)
|
||||
ModelStorageImplClass = Type[ModelStorageImpl]
|
||||
@@ -1,941 +0,0 @@
|
||||
#!/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 math
|
||||
from typing import Any, Callable
|
||||
|
||||
from absl.testing import absltest
|
||||
from absl.testing import parameterized
|
||||
from ortools.math_opt import model_pb2
|
||||
from ortools.math_opt import sparse_containers_pb2
|
||||
from ortools.math_opt.python import hash_model_storage
|
||||
from ortools.math_opt.python import model_storage
|
||||
from ortools.math_opt.python.testing import compare_proto
|
||||
|
||||
_StorageClass = model_storage.ModelStorageImplClass
|
||||
_MatEntry = model_storage.LinearConstraintMatrixIdEntry
|
||||
_ObjEntry = model_storage.LinearObjectiveEntry
|
||||
|
||||
|
||||
@parameterized.parameters((hash_model_storage.HashModelStorage,))
|
||||
class ModelStorageTest(compare_proto.MathOptProtoAssertions, parameterized.TestCase):
|
||||
|
||||
def test_add_and_read_variables(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("test_model")
|
||||
self.assertEqual(0, storage.next_variable_id())
|
||||
v1 = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
v2 = storage.add_variable(-math.inf, math.inf, False, "")
|
||||
self.assertEqual("test_model", storage.name)
|
||||
|
||||
self.assertEqual(-1.0, storage.get_variable_lb(v1))
|
||||
self.assertEqual(2.5, storage.get_variable_ub(v1))
|
||||
self.assertTrue(storage.get_variable_is_integer(v1))
|
||||
self.assertEqual("x", storage.get_variable_name(v1))
|
||||
self.assertEqual(0, v1)
|
||||
self.assertTrue(storage.variable_exists(v1))
|
||||
|
||||
self.assertEqual(-math.inf, storage.get_variable_lb(v2))
|
||||
self.assertEqual(math.inf, storage.get_variable_ub(v2))
|
||||
self.assertFalse(storage.get_variable_is_integer(v2))
|
||||
self.assertEqual("", storage.get_variable_name(v2))
|
||||
self.assertEqual(1, v2)
|
||||
self.assertTrue(storage.variable_exists(v2))
|
||||
|
||||
self.assertFalse(storage.variable_exists(max(v1, v2) + 1))
|
||||
self.assertListEqual([v1, v2], list(storage.get_variables()))
|
||||
self.assertEqual(2, storage.next_variable_id())
|
||||
|
||||
def test_set_variable_lb(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
v1 = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.set_variable_lb(v1, -5.5)
|
||||
self.assertEqual(-5.5, storage.get_variable_lb(v1))
|
||||
|
||||
def test_set_variable_ub(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
v1 = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.set_variable_ub(v1, 1.2)
|
||||
self.assertEqual(1.2, storage.get_variable_ub(v1))
|
||||
|
||||
def test_set_variable_is_integer(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
v1 = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.set_variable_is_integer(v1, False)
|
||||
self.assertFalse(storage.get_variable_is_integer(v1))
|
||||
|
||||
def test_add_and_read_linear_constraints(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
self.assertEqual(0, storage.next_linear_constraint_id())
|
||||
c1 = storage.add_linear_constraint(-1.0, 2.5, "c")
|
||||
c2 = storage.add_linear_constraint(-math.inf, math.inf, "")
|
||||
|
||||
self.assertEqual(-1.0, storage.get_linear_constraint_lb(c1))
|
||||
self.assertEqual(2.5, storage.get_linear_constraint_ub(c1))
|
||||
self.assertEqual("c", storage.get_linear_constraint_name(c1))
|
||||
self.assertEqual(0, c1)
|
||||
self.assertTrue(storage.linear_constraint_exists(c1))
|
||||
|
||||
self.assertEqual(-math.inf, storage.get_linear_constraint_lb(c2))
|
||||
self.assertEqual(math.inf, storage.get_linear_constraint_ub(c2))
|
||||
self.assertEqual("", storage.get_linear_constraint_name(c2))
|
||||
self.assertEqual(1, c2)
|
||||
self.assertTrue(storage.linear_constraint_exists(c2))
|
||||
|
||||
self.assertListEqual([c1, c2], list(storage.get_linear_constraints()))
|
||||
self.assertFalse(storage.linear_constraint_exists(1 + max(c1, c2)))
|
||||
self.assertEqual(2, storage.next_linear_constraint_id())
|
||||
|
||||
def test_set_linear_constraint_lb(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
c1 = storage.add_linear_constraint(-1.0, 2.5, "c")
|
||||
storage.set_linear_constraint_lb(c1, -5.5)
|
||||
self.assertEqual(-5.5, storage.get_linear_constraint_lb(c1))
|
||||
|
||||
def test_set_linear_constraint_ub(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
c1 = storage.add_linear_constraint(-1.0, 2.5, "c")
|
||||
storage.set_linear_constraint_ub(c1, 1.2)
|
||||
self.assertEqual(1.2, storage.get_linear_constraint_ub(c1))
|
||||
|
||||
def test_delete_variable_get_other(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
v1 = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
v2 = storage.add_variable(-3.0, 4.5, False, "y")
|
||||
storage.delete_variable(v1)
|
||||
self.assertEqual(-3.0, storage.get_variable_lb(v2))
|
||||
self.assertEqual(4.5, storage.get_variable_ub(v2))
|
||||
self.assertFalse(storage.get_variable_is_integer(v2))
|
||||
self.assertEqual("y", storage.get_variable_name(v2))
|
||||
self.assertEqual(1, v2)
|
||||
self.assertFalse(storage.variable_exists(v1))
|
||||
self.assertTrue(storage.variable_exists(v2))
|
||||
|
||||
self.assertListEqual([v2], list(storage.get_variables()))
|
||||
|
||||
def test_double_variable_delete(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.delete_variable(x)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def _deleted_variable_invoke_lookup(
|
||||
self,
|
||||
storage_class: _StorageClass,
|
||||
getter: Callable[[model_storage.ModelStorage, int], Any],
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
v1 = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.delete_variable(v1)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
getter(storage, v1)
|
||||
self.assertEqual(v1, cm.exception.id)
|
||||
|
||||
def test_delete_variable_lb_error(self, storage_class: _StorageClass) -> None:
|
||||
self._deleted_variable_invoke_lookup(
|
||||
storage_class, storage_class.get_variable_lb
|
||||
)
|
||||
|
||||
def test_delete_variable_ub_error(self, storage_class: _StorageClass) -> None:
|
||||
self._deleted_variable_invoke_lookup(
|
||||
storage_class, storage_class.get_variable_ub
|
||||
)
|
||||
|
||||
def test_delete_variable_is_integer_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
self._deleted_variable_invoke_lookup(
|
||||
storage_class, storage_class.get_variable_is_integer
|
||||
)
|
||||
|
||||
def test_delete_variable_name_error(self, storage_class: _StorageClass) -> None:
|
||||
self._deleted_variable_invoke_lookup(
|
||||
storage_class, storage_class.get_variable_name
|
||||
)
|
||||
|
||||
def test_delete_variable_set_lb_error(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_variable_lb(x, -2.0)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_delete_variable_set_ub_error(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_variable_ub(x, 12.0)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_delete_variable_set_integer_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_variable_is_integer(x, False)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_delete_linear_constraint_get_other(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
c1 = storage.add_linear_constraint(-1.0, 2.5, "c1")
|
||||
c2 = storage.add_linear_constraint(-math.inf, 5.0, "c2")
|
||||
storage.delete_linear_constraint(c1)
|
||||
self.assertEqual(-math.inf, storage.get_linear_constraint_lb(c2))
|
||||
self.assertEqual(5.0, storage.get_linear_constraint_ub(c2))
|
||||
self.assertEqual("c2", storage.get_linear_constraint_name(c2))
|
||||
self.assertEqual(1, c2)
|
||||
|
||||
self.assertListEqual([c2], list(storage.get_linear_constraints()))
|
||||
|
||||
def test_double_linear_constraint_delete(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
c = storage.add_linear_constraint(-1.0, 2.5, "c")
|
||||
storage.delete_linear_constraint(c)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
storage.delete_linear_constraint(c)
|
||||
self.assertEqual(c, cm.exception.id)
|
||||
|
||||
def _deleted_linear_constraint_invoke_lookup(
|
||||
self,
|
||||
storage_class: _StorageClass,
|
||||
getter: Callable[[model_storage.ModelStorage, int], Any],
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
c1 = storage.add_linear_constraint(-1.0, 2.5, "c1")
|
||||
storage.delete_linear_constraint(c1)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
getter(storage, c1)
|
||||
self.assertEqual(c1, cm.exception.id)
|
||||
|
||||
def test_delete_linear_constraint_lb_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
self._deleted_linear_constraint_invoke_lookup(
|
||||
storage_class, storage_class.get_linear_constraint_lb
|
||||
)
|
||||
|
||||
def test_delete_linear_constraint_ub_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
self._deleted_linear_constraint_invoke_lookup(
|
||||
storage_class, storage_class.get_linear_constraint_ub
|
||||
)
|
||||
|
||||
def test_delete_linear_constraint_name_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
self._deleted_linear_constraint_invoke_lookup(
|
||||
storage_class, storage_class.get_linear_constraint_name
|
||||
)
|
||||
|
||||
def test_delete_linear_constraint_set_lb_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
c = storage.add_linear_constraint(-1.0, 2.5, "c")
|
||||
storage.delete_linear_constraint(c)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
storage.set_linear_constraint_lb(c, -2.0)
|
||||
self.assertEqual(c, cm.exception.id)
|
||||
|
||||
def test_delete_linear_constraint_set_ub_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
c = storage.add_linear_constraint(-1.0, 2.5, "c")
|
||||
storage.delete_linear_constraint(c)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
storage.set_linear_constraint_ub(c, 12.0)
|
||||
self.assertEqual(c, cm.exception.id)
|
||||
|
||||
def test_objective_offset(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
self.assertEqual(0.0, storage.get_objective_offset())
|
||||
storage.set_objective_offset(1.5)
|
||||
self.assertEqual(1.5, storage.get_objective_offset())
|
||||
|
||||
def test_objective_direction(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
self.assertFalse(storage.get_is_maximize())
|
||||
storage.set_is_maximize(True)
|
||||
self.assertTrue(storage.get_is_maximize())
|
||||
storage.set_is_maximize(False)
|
||||
self.assertFalse(storage.get_is_maximize())
|
||||
|
||||
def test_set_linear_objective_coefficient(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(0.0, 1.0, False, "y")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
storage.set_linear_objective_coefficient(x, 2.0)
|
||||
storage.set_linear_objective_coefficient(z, -5.5)
|
||||
self.assertEqual(2.0, storage.get_linear_objective_coefficient(x))
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(y))
|
||||
self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z))
|
||||
|
||||
self.assertCountEqual(
|
||||
[
|
||||
_ObjEntry(variable_id=x, coefficient=2.0),
|
||||
_ObjEntry(variable_id=z, coefficient=-5.5),
|
||||
],
|
||||
storage.get_linear_objective_coefficients(),
|
||||
)
|
||||
|
||||
def test_clear_linear_objective_coefficient(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(0.0, 1.0, False, "y")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
storage.set_linear_objective_coefficient(x, 2.0)
|
||||
storage.set_linear_objective_coefficient(z, -5.5)
|
||||
storage.set_objective_offset(1.0)
|
||||
self.assertEqual(2.0, storage.get_linear_objective_coefficient(x))
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(y))
|
||||
self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z))
|
||||
self.assertEqual(1.0, storage.get_objective_offset())
|
||||
storage.clear_objective()
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(x))
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(y))
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(z))
|
||||
self.assertEqual(0.0, storage.get_objective_offset())
|
||||
|
||||
def test_set_linear_objective_coefficient_bad_id(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_linear_objective_coefficient(x + 1, 2.0)
|
||||
self.assertEqual(x + 1, cm.exception.id)
|
||||
|
||||
def test_set_linear_objective_coefficient_deleted_id(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_linear_objective_coefficient(y, 3.0)
|
||||
storage.delete_variable(x)
|
||||
self.assertEqual(3.0, storage.get_linear_objective_coefficient(y))
|
||||
self.assertCountEqual(
|
||||
[model_storage.LinearObjectiveEntry(variable_id=y, coefficient=3.0)],
|
||||
storage.get_linear_objective_coefficients(),
|
||||
)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_linear_objective_coefficient(x, 2.0)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_get_linear_objective_coefficient_deleted_nonzero(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_linear_objective_coefficient(x, 1.0)
|
||||
storage.set_linear_objective_coefficient(y, 3.0)
|
||||
storage.delete_variable(x)
|
||||
self.assertEqual(3.0, storage.get_linear_objective_coefficient(y))
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.get_linear_objective_coefficient(x)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_set_quadratic_objective_coefficient(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(0.0, 1.0, False, "y")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
storage.set_quadratic_objective_coefficient(x, y, 2.0)
|
||||
storage.set_quadratic_objective_coefficient(z, z, -5.5)
|
||||
storage.set_quadratic_objective_coefficient(z, y, 1.5)
|
||||
self.assertEqual(2.0, storage.get_quadratic_objective_coefficient(x, y))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y))
|
||||
self.assertEqual(-5.5, storage.get_quadratic_objective_coefficient(z, z))
|
||||
self.assertEqual(1.5, storage.get_quadratic_objective_coefficient(y, z))
|
||||
|
||||
self.assertCountEqual(
|
||||
[
|
||||
model_storage.QuadraticEntry(
|
||||
id_key=model_storage.QuadraticTermIdKey(x, y), coefficient=2.0
|
||||
),
|
||||
model_storage.QuadraticEntry(
|
||||
id_key=model_storage.QuadraticTermIdKey(z, z), coefficient=-5.5
|
||||
),
|
||||
model_storage.QuadraticEntry(
|
||||
id_key=model_storage.QuadraticTermIdKey(y, z), coefficient=1.5
|
||||
),
|
||||
],
|
||||
storage.get_quadratic_objective_coefficients(),
|
||||
)
|
||||
|
||||
self.assertCountEqual(
|
||||
[y], storage.get_quadratic_objective_adjacent_variables(x)
|
||||
)
|
||||
self.assertCountEqual(
|
||||
[x, z], storage.get_quadratic_objective_adjacent_variables(y)
|
||||
)
|
||||
self.assertCountEqual(
|
||||
[y, z], storage.get_quadratic_objective_adjacent_variables(z)
|
||||
)
|
||||
|
||||
def test_clear_quadratic_objective_coefficient(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(0.0, 1.0, False, "y")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
storage.set_linear_objective_coefficient(x, 2.0)
|
||||
storage.set_linear_objective_coefficient(z, -5.5)
|
||||
storage.set_quadratic_objective_coefficient(x, y, 2.0)
|
||||
storage.set_quadratic_objective_coefficient(z, z, -5.5)
|
||||
storage.set_quadratic_objective_coefficient(z, y, 1.5)
|
||||
storage.set_objective_offset(1.0)
|
||||
storage.clear_objective()
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(x))
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(y))
|
||||
self.assertEqual(0.0, storage.get_linear_objective_coefficient(z))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, y))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(z, z))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, z))
|
||||
self.assertEqual(0.0, storage.get_objective_offset())
|
||||
self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(x)))
|
||||
self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(y)))
|
||||
self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(z)))
|
||||
|
||||
def test_set_quadratic_objective_coefficient_bad_id(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_quadratic_objective_coefficient(x, x + 1, 2.0)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_quadratic_objective_coefficient(x + 1, x, 2.0)
|
||||
self.assertEqual(x + 1, cm.exception.id)
|
||||
|
||||
def test_get_quadratic_objective_coefficient_bad_id(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.get_quadratic_objective_coefficient(x, x + 1)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.get_quadratic_objective_coefficient(x + 1, x)
|
||||
self.assertEqual(x + 1, cm.exception.id)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
list(storage.get_quadratic_objective_adjacent_variables(x + 1))
|
||||
self.assertEqual(x + 1, cm.exception.id)
|
||||
|
||||
def test_set_quadratic_objective_coefficient_existing_to_zero(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_quadratic_objective_coefficient(x, x, -1.0)
|
||||
storage.set_quadratic_objective_coefficient(x, y, 1.0)
|
||||
storage.set_quadratic_objective_coefficient(y, y, 3.0)
|
||||
|
||||
storage.set_quadratic_objective_coefficient(x, x, 0.0)
|
||||
storage.set_quadratic_objective_coefficient(x, y, 0.0)
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, x))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, y))
|
||||
self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(y, y))
|
||||
self.assertCountEqual(
|
||||
[y], storage.get_quadratic_objective_adjacent_variables(y)
|
||||
)
|
||||
self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(x)))
|
||||
|
||||
def test_set_quadratic_objective_coefficient_deleted_id(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_quadratic_objective_coefficient(x, y, 1.0)
|
||||
storage.set_quadratic_objective_coefficient(y, y, 3.0)
|
||||
storage.delete_variable(x)
|
||||
self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(y, y))
|
||||
self.assertCountEqual(
|
||||
[y], storage.get_quadratic_objective_adjacent_variables(y)
|
||||
)
|
||||
|
||||
def test_set_quadratic_objective_coefficient_deleted_id_get_coeff_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_quadratic_objective_coefficient(x, y, 1.0)
|
||||
storage.set_quadratic_objective_coefficient(y, y, 3.0)
|
||||
storage.delete_variable(x)
|
||||
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.get_quadratic_objective_coefficient(x, y)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_set_quadratic_objective_coefficient_deleted_id_set_coeff_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_quadratic_objective_coefficient(x, y, 1.0)
|
||||
storage.set_quadratic_objective_coefficient(y, y, 3.0)
|
||||
storage.delete_variable(x)
|
||||
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_quadratic_objective_coefficient(x, y, 1.0)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_set_quadratic_objective_coefficient_deleted_id_adjacent_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, True, "y")
|
||||
storage.set_quadratic_objective_coefficient(x, y, 1.0)
|
||||
storage.set_quadratic_objective_coefficient(y, y, 3.0)
|
||||
storage.delete_variable(x)
|
||||
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
list(storage.get_quadratic_objective_adjacent_variables(x))
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_constraint_matrix(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, False, "y")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
d = storage.add_linear_constraint(-math.inf, 1.0, "d")
|
||||
storage.set_linear_constraint_coefficient(c, y, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, x, 2.0)
|
||||
storage.set_linear_constraint_coefficient(d, y, -1.0)
|
||||
storage.set_linear_constraint_coefficient(d, z, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, z, 0.0)
|
||||
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, x))
|
||||
self.assertEqual(1.0, storage.get_linear_constraint_coefficient(c, y))
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, z))
|
||||
|
||||
self.assertEqual(2.0, storage.get_linear_constraint_coefficient(d, x))
|
||||
self.assertEqual(-1.0, storage.get_linear_constraint_coefficient(d, y))
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, z))
|
||||
|
||||
self.assertCountEqual([y], storage.get_variables_for_linear_constraint(c))
|
||||
self.assertCountEqual([x, y], storage.get_variables_for_linear_constraint(d))
|
||||
|
||||
self.assertCountEqual([d], storage.get_linear_constraints_with_variable(x))
|
||||
self.assertCountEqual([c, d], storage.get_linear_constraints_with_variable(y))
|
||||
self.assertCountEqual([], storage.get_linear_constraints_with_variable(z))
|
||||
|
||||
self.assertCountEqual(
|
||||
[
|
||||
_MatEntry(linear_constraint_id=c, variable_id=y, coefficient=1.0),
|
||||
_MatEntry(linear_constraint_id=d, variable_id=x, coefficient=2.0),
|
||||
_MatEntry(linear_constraint_id=d, variable_id=y, coefficient=-1.0),
|
||||
],
|
||||
storage.get_linear_constraint_matrix_entries(),
|
||||
)
|
||||
|
||||
def test_constraint_matrix_zero_unset_entry(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.set_linear_constraint_coefficient(c, x, 0.0)
|
||||
self.assertEmpty(list(storage.get_linear_objective_coefficients()))
|
||||
self.assertEmpty(list(storage.get_variables_for_linear_constraint(c)))
|
||||
self.assertEmpty(list(storage.get_linear_constraints_with_variable(x)))
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, x))
|
||||
|
||||
def test_constraint_matrix_with_deletion(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, False, "y")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
d = storage.add_linear_constraint(-math.inf, 1.0, "d")
|
||||
storage.set_linear_constraint_coefficient(c, y, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, x, 2.0)
|
||||
storage.set_linear_constraint_coefficient(d, y, -1.0)
|
||||
storage.set_linear_constraint_coefficient(c, z, 1.0)
|
||||
|
||||
storage.delete_variable(y)
|
||||
storage.delete_linear_constraint(c)
|
||||
|
||||
self.assertEqual(2.0, storage.get_linear_constraint_coefficient(d, x))
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, z))
|
||||
|
||||
self.assertCountEqual([x], storage.get_variables_for_linear_constraint(d))
|
||||
|
||||
self.assertCountEqual([d], storage.get_linear_constraints_with_variable(x))
|
||||
self.assertCountEqual([], storage.get_linear_constraints_with_variable(z))
|
||||
|
||||
self.assertCountEqual(
|
||||
[_MatEntry(linear_constraint_id=d, variable_id=x, coefficient=2.0)],
|
||||
storage.get_linear_constraint_matrix_entries(),
|
||||
)
|
||||
|
||||
def test_variables_for_linear_constraint_deleted_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.set_linear_constraint_coefficient(c, x, 1.0)
|
||||
storage.delete_linear_constraint(c)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
list(storage.get_variables_for_linear_constraint(c))
|
||||
self.assertEqual(c, cm.exception.id)
|
||||
|
||||
def test_linear_constraints_with_variable_deleted_error(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.set_linear_constraint_coefficient(c, x, 1.0)
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
list(storage.get_linear_constraints_with_variable(x))
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_constraint_matrix_set_deleted_var(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.set_linear_constraint_coefficient(c, x, 2.0)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_constraint_matrix_get_deleted_var(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.delete_variable(x)
|
||||
with self.assertRaises(model_storage.BadVariableIdError) as cm:
|
||||
storage.get_linear_constraint_coefficient(c, x)
|
||||
self.assertEqual(x, cm.exception.id)
|
||||
|
||||
def test_constraint_matrix_set_deleted_constraint(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.delete_linear_constraint(c)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
storage.set_linear_constraint_coefficient(c, x, 2.0)
|
||||
self.assertEqual(c, cm.exception.id)
|
||||
|
||||
def test_constraint_matrix_get_deleted_constraint(
|
||||
self, storage_class: _StorageClass
|
||||
) -> None:
|
||||
storage = storage_class()
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "c")
|
||||
storage.delete_linear_constraint(c)
|
||||
with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm:
|
||||
storage.get_linear_constraint_coefficient(c, x)
|
||||
self.assertEqual(c, cm.exception.id)
|
||||
|
||||
def test_proto_export(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("test_model")
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, False, "")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "")
|
||||
d = storage.add_linear_constraint(0.0, 1.0, "d")
|
||||
storage.set_linear_constraint_coefficient(c, y, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, x, 2.0)
|
||||
storage.set_linear_constraint_coefficient(d, y, -1.0)
|
||||
storage.set_linear_constraint_coefficient(d, z, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, z, 0.0)
|
||||
storage.set_linear_objective_coefficient(x, 2.5)
|
||||
storage.set_linear_objective_coefficient(z, -1.0)
|
||||
storage.set_quadratic_objective_coefficient(x, x, 3.0)
|
||||
storage.set_quadratic_objective_coefficient(x, y, 4.0)
|
||||
storage.set_quadratic_objective_coefficient(x, z, 5.0)
|
||||
storage.set_is_maximize(True)
|
||||
storage.set_objective_offset(7.0)
|
||||
|
||||
expected = model_pb2.ModelProto(
|
||||
name="test_model",
|
||||
variables=model_pb2.VariablesProto(
|
||||
ids=[0, 1, 2],
|
||||
lower_bounds=[-1.0, -1.0, 0.0],
|
||||
upper_bounds=[2.5, 2.5, 1.0],
|
||||
integers=[True, False, True],
|
||||
names=["x", "", "z"],
|
||||
),
|
||||
linear_constraints=model_pb2.LinearConstraintsProto(
|
||||
ids=[0, 1],
|
||||
lower_bounds=[-math.inf, 0.0],
|
||||
upper_bounds=[3.0, 1.0],
|
||||
names=["", "d"],
|
||||
),
|
||||
objective=model_pb2.ObjectiveProto(
|
||||
maximize=True,
|
||||
offset=7.0,
|
||||
linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto(
|
||||
ids=[0, 2], values=[2.5, -1.0]
|
||||
),
|
||||
quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto(
|
||||
row_ids=[0, 0, 0],
|
||||
column_ids=[0, 1, 2],
|
||||
coefficients=[3.0, 4.0, 5.0],
|
||||
),
|
||||
),
|
||||
linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto(
|
||||
row_ids=[0, 1, 1],
|
||||
column_ids=[1, 0, 1],
|
||||
coefficients=[1.0, 2.0, -1.0],
|
||||
),
|
||||
)
|
||||
self.assert_protos_equiv(expected, storage.export_model())
|
||||
|
||||
def test_proto_export_with_deletes(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("test_model")
|
||||
x = storage.add_variable(-1.0, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, 2.5, False, "")
|
||||
z = storage.add_variable(0.0, 1.0, True, "z")
|
||||
c = storage.add_linear_constraint(-math.inf, 3.0, "")
|
||||
d = storage.add_linear_constraint(0.0, 1.0, "d")
|
||||
storage.set_linear_constraint_coefficient(c, y, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, x, 2.0)
|
||||
storage.set_linear_constraint_coefficient(d, y, -1.0)
|
||||
storage.set_linear_constraint_coefficient(d, z, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, z, 0.0)
|
||||
storage.set_linear_objective_coefficient(x, 2.5)
|
||||
storage.set_quadratic_objective_coefficient(x, x, 3.0)
|
||||
storage.set_quadratic_objective_coefficient(x, y, 4.0)
|
||||
storage.set_quadratic_objective_coefficient(x, z, 5.0)
|
||||
storage.set_is_maximize(False)
|
||||
storage.delete_variable(y)
|
||||
storage.delete_linear_constraint(c)
|
||||
|
||||
expected = model_pb2.ModelProto(
|
||||
name="test_model",
|
||||
variables=model_pb2.VariablesProto(
|
||||
ids=[0, 2],
|
||||
lower_bounds=[-1.0, 0.0],
|
||||
upper_bounds=[2.5, 1.0],
|
||||
integers=[True, True],
|
||||
names=["x", "z"],
|
||||
),
|
||||
linear_constraints=model_pb2.LinearConstraintsProto(
|
||||
ids=[1], lower_bounds=[0.0], upper_bounds=[1.0], names=["d"]
|
||||
),
|
||||
objective=model_pb2.ObjectiveProto(
|
||||
maximize=False,
|
||||
offset=0.0,
|
||||
linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto(
|
||||
ids=[0], values=[2.5]
|
||||
),
|
||||
quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto(
|
||||
row_ids=[0, 0], column_ids=[0, 2], coefficients=[3.0, 5.0]
|
||||
),
|
||||
),
|
||||
linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto(
|
||||
row_ids=[1], column_ids=[0], coefficients=[2.0]
|
||||
),
|
||||
)
|
||||
self.assert_protos_equiv(expected, storage.export_model())
|
||||
|
||||
def test_proto_export_empty(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("test_model")
|
||||
expected = model_pb2.ModelProto(name="test_model")
|
||||
self.assert_protos_equiv(expected, storage.export_model())
|
||||
|
||||
def test_proto_export_feasibility(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("test_model")
|
||||
storage.add_variable(-1.0, 2.5, True, "x")
|
||||
expected = model_pb2.ModelProto(
|
||||
name="test_model",
|
||||
variables=model_pb2.VariablesProto(
|
||||
ids=[0],
|
||||
lower_bounds=[-1.0],
|
||||
upper_bounds=[2.5],
|
||||
integers=[True],
|
||||
names=["x"],
|
||||
),
|
||||
)
|
||||
self.assert_protos_equiv(expected, storage.export_model())
|
||||
|
||||
def test_proto_export_empty_names(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("")
|
||||
storage.add_variable(-1.0, 2.5, True, "")
|
||||
storage.add_linear_constraint(0.0, 1.0, "")
|
||||
expected = model_pb2.ModelProto(
|
||||
variables=model_pb2.VariablesProto(
|
||||
ids=[0],
|
||||
lower_bounds=[-1.0],
|
||||
upper_bounds=[2.5],
|
||||
integers=[True],
|
||||
# NOTE: names is the empty list not a list with an empty string.
|
||||
names=[],
|
||||
),
|
||||
linear_constraints=model_pb2.LinearConstraintsProto(
|
||||
ids=[0],
|
||||
lower_bounds=[0.0],
|
||||
upper_bounds=[1.0],
|
||||
# NOTE: names is the empty list not a list with an empty string.
|
||||
names=[],
|
||||
),
|
||||
)
|
||||
self.assert_protos_equiv(expected, storage.export_model())
|
||||
|
||||
def _assert_nan(self, x):
|
||||
self.assertTrue(math.isnan(x), f"Expected nan, found {x}")
|
||||
|
||||
# Ensure that we don't silently drop NaNs.
|
||||
def test_nans_pass_through(self, storage_class: _StorageClass) -> None:
|
||||
storage = storage_class("nan_model")
|
||||
nan = math.nan
|
||||
x = storage.add_variable(nan, 2.5, True, "x")
|
||||
y = storage.add_variable(-1.0, nan, True, "y")
|
||||
c = storage.add_linear_constraint(nan, math.inf, "c")
|
||||
d = storage.add_linear_constraint(0.0, nan, "d")
|
||||
storage.set_objective_offset(nan)
|
||||
storage.set_linear_objective_coefficient(x, 1.0)
|
||||
storage.set_linear_objective_coefficient(y, nan)
|
||||
storage.set_quadratic_objective_coefficient(x, x, 3.0)
|
||||
storage.set_quadratic_objective_coefficient(x, y, nan)
|
||||
storage.set_linear_constraint_coefficient(c, x, nan)
|
||||
storage.set_linear_constraint_coefficient(c, y, 1.0)
|
||||
storage.set_linear_constraint_coefficient(d, y, nan)
|
||||
|
||||
# Test the getters.
|
||||
self.assertEqual("nan_model", storage.name)
|
||||
self._assert_nan(storage.get_objective_offset())
|
||||
self._assert_nan(storage.get_variable_lb(x))
|
||||
self.assertEqual(2.5, storage.get_variable_ub(x))
|
||||
self.assertEqual(-1.0, storage.get_variable_lb(y))
|
||||
self._assert_nan(storage.get_variable_ub(y))
|
||||
self.assertEqual(1.0, storage.get_linear_objective_coefficient(x))
|
||||
self._assert_nan(storage.get_linear_objective_coefficient(y))
|
||||
self._assert_nan(storage.get_linear_constraint_lb(c))
|
||||
self.assertEqual(math.inf, storage.get_linear_constraint_ub(c))
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_lb(d))
|
||||
self._assert_nan(storage.get_linear_constraint_ub(d))
|
||||
self._assert_nan(storage.get_linear_constraint_coefficient(c, x))
|
||||
self.assertEqual(1.0, storage.get_linear_constraint_coefficient(c, y))
|
||||
self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, x))
|
||||
self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(x, x))
|
||||
self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y))
|
||||
self._assert_nan(storage.get_quadratic_objective_coefficient(x, y))
|
||||
self._assert_nan(storage.get_linear_constraint_coefficient(d, y))
|
||||
|
||||
# Test the iterators that interact with the NaN values.
|
||||
self.assertCountEqual([x, y], storage.get_variables_for_linear_constraint(c))
|
||||
self.assertCountEqual([y], storage.get_variables_for_linear_constraint(d))
|
||||
|
||||
self.assertCountEqual([c], storage.get_linear_constraints_with_variable(x))
|
||||
self.assertCountEqual([c, d], storage.get_linear_constraints_with_variable(y))
|
||||
|
||||
mat_entries = {}
|
||||
for e in storage.get_linear_constraint_matrix_entries():
|
||||
key = (e.linear_constraint_id, e.variable_id)
|
||||
self.assertNotIn(
|
||||
key,
|
||||
mat_entries,
|
||||
msg=f"found key:{key} twice, e:{e} mat_entries:{mat_entries}",
|
||||
)
|
||||
mat_entries[key] = e.coefficient
|
||||
self.assertSetEqual(set(mat_entries.keys()), set(((c, x), (c, y), (d, y))))
|
||||
self._assert_nan(mat_entries[(c, x)])
|
||||
self.assertEqual(mat_entries[(c, y)], 1.0)
|
||||
self._assert_nan(mat_entries[(d, y)])
|
||||
|
||||
obj_entries = {}
|
||||
for e in storage.get_linear_objective_coefficients():
|
||||
self.assertNotIn(
|
||||
e.variable_id,
|
||||
obj_entries,
|
||||
msg=(
|
||||
f"found variable:{e.variable_id} twice,"
|
||||
f" e:{e} obj_entries:{obj_entries}"
|
||||
),
|
||||
)
|
||||
obj_entries[e.variable_id] = e.coefficient
|
||||
self.assertSetEqual(set(obj_entries.keys()), set((x, y)))
|
||||
self.assertEqual(obj_entries[x], 1.0)
|
||||
self._assert_nan(obj_entries[y])
|
||||
|
||||
# Export to proto
|
||||
expected = model_pb2.ModelProto(
|
||||
name="nan_model",
|
||||
variables=model_pb2.VariablesProto(
|
||||
ids=[0, 1],
|
||||
lower_bounds=[nan, -1.0],
|
||||
upper_bounds=[2.5, nan],
|
||||
integers=[True, True],
|
||||
names=["x", "y"],
|
||||
),
|
||||
linear_constraints=model_pb2.LinearConstraintsProto(
|
||||
ids=[0, 1],
|
||||
lower_bounds=[nan, 0.0],
|
||||
upper_bounds=[math.inf, nan],
|
||||
names=["c", "d"],
|
||||
),
|
||||
objective=model_pb2.ObjectiveProto(
|
||||
maximize=False,
|
||||
offset=nan,
|
||||
linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto(
|
||||
ids=[0, 1], values=[1.0, nan]
|
||||
),
|
||||
quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto(
|
||||
row_ids=[0, 0], column_ids=[0, 1], coefficients=[3.0, nan]
|
||||
),
|
||||
),
|
||||
linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto(
|
||||
row_ids=[0, 0, 1],
|
||||
column_ids=[0, 1, 1],
|
||||
coefficients=[nan, 1.0, nan],
|
||||
),
|
||||
)
|
||||
self.assert_protos_equiv(expected, storage.export_model())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
absltest.main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user