small cleaning

This commit is contained in:
Laurent Perron
2025-07-01 16:51:11 +02:00
parent b644c5fb1e
commit 5b1376b9df
47 changed files with 868 additions and 3569 deletions

View File

@@ -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"],

View File

@@ -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)

View File

@@ -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()

View File

@@ -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]

View File

@@ -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