173 lines
5.5 KiB
Python
173 lines
5.5 KiB
Python
# Copyright 2010-2022 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.
|
|
|
|
"""Statistics about MIP/LP models."""
|
|
|
|
import dataclasses
|
|
import io
|
|
import math
|
|
from typing import Iterable, Optional
|
|
|
|
from ortools.math_opt.python import model
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class Range:
|
|
"""A close range of values [min, max].
|
|
|
|
Attributes:
|
|
minimum: The minimum value.
|
|
maximum: The maximum value.
|
|
"""
|
|
|
|
minimum: float
|
|
maximum: float
|
|
|
|
|
|
def merge_optional_ranges(
|
|
lhs: Optional[Range], rhs: Optional[Range]
|
|
) -> Optional[Range]:
|
|
"""Merges the two optional ranges.
|
|
|
|
Args:
|
|
lhs: The left hand side range.
|
|
rhs: The right hand side range.
|
|
|
|
Returns:
|
|
A merged range (None if both lhs and rhs are None).
|
|
"""
|
|
if lhs is None:
|
|
return rhs
|
|
if rhs is None:
|
|
return lhs
|
|
return Range(
|
|
minimum=min(lhs.minimum, rhs.minimum),
|
|
maximum=max(lhs.maximum, rhs.maximum),
|
|
)
|
|
|
|
|
|
def absolute_finite_non_zeros_range(values: Iterable[float]) -> Optional[Range]:
|
|
"""Returns the range of the absolute values of the finite non-zeros.
|
|
|
|
Args:
|
|
values: An iterable object of float values.
|
|
|
|
Returns:
|
|
The range of the absolute values of the finite non-zeros, None if no such
|
|
value is found.
|
|
"""
|
|
minimum: Optional[float] = None
|
|
maximum: Optional[float] = None
|
|
for v in values:
|
|
v = abs(v)
|
|
if math.isinf(v) or v == 0.0:
|
|
continue
|
|
if minimum is None:
|
|
minimum = v
|
|
maximum = v
|
|
else:
|
|
minimum = min(minimum, v)
|
|
maximum = max(maximum, v)
|
|
|
|
assert (maximum is None) == (minimum is None), (minimum, maximum)
|
|
|
|
if minimum is None:
|
|
return None
|
|
return Range(minimum=minimum, maximum=maximum)
|
|
|
|
|
|
@dataclasses.dataclass(frozen=True)
|
|
class ModelRanges:
|
|
"""The ranges of the absolute values of the finite non-zero values in the model.
|
|
|
|
Each range is optional since there may be no finite non-zero values
|
|
(e.g. empty model, empty objective, all variables unbounded, ...).
|
|
|
|
Attributes:
|
|
objective_terms: The linear and quadratic objective terms (not including the
|
|
offset).
|
|
variable_bounds: The variables' lower and upper bounds.
|
|
linear_constraint_bounds: The linear constraints' lower and upper bounds.
|
|
linear_constraint_coefficients: The coefficients of the variables in linear
|
|
constraints.
|
|
"""
|
|
|
|
objective_terms: Optional[Range]
|
|
variable_bounds: Optional[Range]
|
|
linear_constraint_bounds: Optional[Range]
|
|
linear_constraint_coefficients: Optional[Range]
|
|
|
|
def __str__(self) -> str:
|
|
"""Prints the ranges in scientific format with 2 digits (i.e.
|
|
|
|
f'{x:.2e}').
|
|
|
|
It returns a multi-line table list of ranges. The last line does NOT end
|
|
with a new line.
|
|
|
|
Returns:
|
|
The ranges in multiline string.
|
|
"""
|
|
buf = io.StringIO()
|
|
|
|
def print_range(prefix: str, value: Optional[Range]) -> None:
|
|
buf.write(prefix)
|
|
if value is None:
|
|
buf.write("no finite values")
|
|
return
|
|
# Numbers are printed in scientific notation with a precision of 2. Since
|
|
# they are expected to be positive we can ignore the optional leading
|
|
# minus sign. We thus expects `d.dde[+-]dd(d)?` (the exponent is at least
|
|
# 2 digits but double can require 3 digits, with max +308 and min
|
|
# -308). Thus we can use a width of 9 to align the ranges properly.
|
|
buf.write(f"[{value.minimum:<9.2e}, {value.maximum:<9.2e}]")
|
|
|
|
print_range("Objective terms : ", self.objective_terms)
|
|
print_range("\nVariable bounds : ", self.variable_bounds)
|
|
print_range("\nLinear constraints bounds : ", self.linear_constraint_bounds)
|
|
print_range(
|
|
"\nLinear constraints coeffs : ", self.linear_constraint_coefficients
|
|
)
|
|
return buf.getvalue()
|
|
|
|
|
|
def compute_model_ranges(mdl: model.Model) -> ModelRanges:
|
|
"""Returns the ranges of the finite non-zero values in the given model.
|
|
|
|
Args:
|
|
mdl: The input model.
|
|
|
|
Returns:
|
|
The ranges of the finite non-zero values in the model.
|
|
"""
|
|
return ModelRanges(
|
|
objective_terms=absolute_finite_non_zeros_range(
|
|
term.coefficient for term in mdl.objective.linear_terms()
|
|
),
|
|
variable_bounds=merge_optional_ranges(
|
|
absolute_finite_non_zeros_range(v.lower_bound for v in mdl.variables()),
|
|
absolute_finite_non_zeros_range(v.upper_bound for v in mdl.variables()),
|
|
),
|
|
linear_constraint_bounds=merge_optional_ranges(
|
|
absolute_finite_non_zeros_range(
|
|
c.lower_bound for c in mdl.linear_constraints()
|
|
),
|
|
absolute_finite_non_zeros_range(
|
|
c.upper_bound for c in mdl.linear_constraints()
|
|
),
|
|
),
|
|
linear_constraint_coefficients=absolute_finite_non_zeros_range(
|
|
e.coefficient for e in mdl.linear_constraint_matrix_entries()
|
|
),
|
|
)
|