ortools.math_opt.python.statistics

Statistics about MIP/LP models.

  1# Copyright 2010-2024 Google LLC
  2# Licensed under the Apache License, Version 2.0 (the "License");
  3# you may not use this file except in compliance with the License.
  4# You may obtain a copy of the License at
  5#
  6#     http://www.apache.org/licenses/LICENSE-2.0
  7#
  8# Unless required by applicable law or agreed to in writing, software
  9# distributed under the License is distributed on an "AS IS" BASIS,
 10# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 11# See the License for the specific language governing permissions and
 12# limitations under the License.
 13
 14"""Statistics about MIP/LP models."""
 15
 16import dataclasses
 17import io
 18import math
 19from typing import Iterable, Optional
 20
 21from ortools.math_opt.python import model
 22
 23
 24@dataclasses.dataclass(frozen=True)
 25class Range:
 26    """A close range of values [min, max].
 27
 28    Attributes:
 29      minimum: The minimum value.
 30      maximum: The maximum value.
 31    """
 32
 33    minimum: float
 34    maximum: float
 35
 36
 37def merge_optional_ranges(
 38    lhs: Optional[Range], rhs: Optional[Range]
 39) -> Optional[Range]:
 40    """Merges the two optional ranges.
 41
 42    Args:
 43      lhs: The left hand side range.
 44      rhs: The right hand side range.
 45
 46    Returns:
 47      A merged range (None if both lhs and rhs are None).
 48    """
 49    if lhs is None:
 50        return rhs
 51    if rhs is None:
 52        return lhs
 53    return Range(
 54        minimum=min(lhs.minimum, rhs.minimum),
 55        maximum=max(lhs.maximum, rhs.maximum),
 56    )
 57
 58
 59def absolute_finite_non_zeros_range(values: Iterable[float]) -> Optional[Range]:
 60    """Returns the range of the absolute values of the finite non-zeros.
 61
 62    Args:
 63      values: An iterable object of float values.
 64
 65    Returns:
 66      The range of the absolute values of the finite non-zeros, None if no such
 67      value is found.
 68    """
 69    minimum: Optional[float] = None
 70    maximum: Optional[float] = None
 71    for v in values:
 72        v = abs(v)
 73        if math.isinf(v) or v == 0.0:
 74            continue
 75        if minimum is None:
 76            minimum = v
 77            maximum = v
 78        else:
 79            minimum = min(minimum, v)
 80            maximum = max(maximum, v)
 81
 82    assert (maximum is None) == (minimum is None), (minimum, maximum)
 83
 84    if minimum is None:
 85        return None
 86    return Range(minimum=minimum, maximum=maximum)
 87
 88
 89@dataclasses.dataclass(frozen=True)
 90class ModelRanges:
 91    """The ranges of the absolute values of the finite non-zero values in the model.
 92
 93    Each range is optional since there may be no finite non-zero values
 94    (e.g. empty model, empty objective, all variables unbounded, ...).
 95
 96    Attributes:
 97      objective_terms: The linear and quadratic objective terms (not including the
 98        offset).
 99      variable_bounds: The variables' lower and upper bounds.
100      linear_constraint_bounds: The linear constraints' lower and upper bounds.
101      linear_constraint_coefficients: The coefficients of the variables in linear
102        constraints.
103    """
104
105    objective_terms: Optional[Range]
106    variable_bounds: Optional[Range]
107    linear_constraint_bounds: Optional[Range]
108    linear_constraint_coefficients: Optional[Range]
109
110    def __str__(self) -> str:
111        """Prints the ranges in scientific format with 2 digits (i.e.
112
113        f'{x:.2e}').
114
115        It returns a multi-line table list of ranges. The last line does NOT end
116        with a new line.
117
118        Returns:
119          The ranges in multiline string.
120        """
121        buf = io.StringIO()
122
123        def print_range(prefix: str, value: Optional[Range]) -> None:
124            buf.write(prefix)
125            if value is None:
126                buf.write("no finite values")
127                return
128            # Numbers are printed in scientific notation with a precision of 2. Since
129            # they are expected to be positive we can ignore the optional leading
130            # minus sign. We thus expects `d.dde[+-]dd(d)?` (the exponent is at least
131            # 2 digits but double can require 3 digits, with max +308 and min
132            # -308). Thus we can use a width of 9 to align the ranges properly.
133            buf.write(f"[{value.minimum:<9.2e}, {value.maximum:<9.2e}]")
134
135        print_range("Objective terms           : ", self.objective_terms)
136        print_range("\nVariable bounds           : ", self.variable_bounds)
137        print_range("\nLinear constraints bounds : ", self.linear_constraint_bounds)
138        print_range(
139            "\nLinear constraints coeffs : ", self.linear_constraint_coefficients
140        )
141        return buf.getvalue()
142
143
144def compute_model_ranges(mdl: model.Model) -> ModelRanges:
145    """Returns the ranges of the finite non-zero values in the given model.
146
147    Args:
148      mdl: The input model.
149
150    Returns:
151      The ranges of the finite non-zero values in the model.
152    """
153    return ModelRanges(
154        objective_terms=absolute_finite_non_zeros_range(
155            term.coefficient for term in mdl.objective.linear_terms()
156        ),
157        variable_bounds=merge_optional_ranges(
158            absolute_finite_non_zeros_range(v.lower_bound for v in mdl.variables()),
159            absolute_finite_non_zeros_range(v.upper_bound for v in mdl.variables()),
160        ),
161        linear_constraint_bounds=merge_optional_ranges(
162            absolute_finite_non_zeros_range(
163                c.lower_bound for c in mdl.linear_constraints()
164            ),
165            absolute_finite_non_zeros_range(
166                c.upper_bound for c in mdl.linear_constraints()
167            ),
168        ),
169        linear_constraint_coefficients=absolute_finite_non_zeros_range(
170            e.coefficient for e in mdl.linear_constraint_matrix_entries()
171        ),
172    )
@dataclasses.dataclass(frozen=True)
class Range:
25@dataclasses.dataclass(frozen=True)
26class Range:
27    """A close range of values [min, max].
28
29    Attributes:
30      minimum: The minimum value.
31      maximum: The maximum value.
32    """
33
34    minimum: float
35    maximum: float

A close range of values [min, max].

Attributes:
  • minimum: The minimum value.
  • maximum: The maximum value.
Range(minimum: float, maximum: float)
minimum: float
maximum: float
def merge_optional_ranges( lhs: Optional[Range], rhs: Optional[Range]) -> Optional[Range]:
38def merge_optional_ranges(
39    lhs: Optional[Range], rhs: Optional[Range]
40) -> Optional[Range]:
41    """Merges the two optional ranges.
42
43    Args:
44      lhs: The left hand side range.
45      rhs: The right hand side range.
46
47    Returns:
48      A merged range (None if both lhs and rhs are None).
49    """
50    if lhs is None:
51        return rhs
52    if rhs is None:
53        return lhs
54    return Range(
55        minimum=min(lhs.minimum, rhs.minimum),
56        maximum=max(lhs.maximum, rhs.maximum),
57    )

Merges the two optional ranges.

Arguments:
  • lhs: The left hand side range.
  • rhs: The right hand side range.
Returns:

A merged range (None if both lhs and rhs are None).

def absolute_finite_non_zeros_range( values: Iterable[float]) -> Optional[Range]:
60def absolute_finite_non_zeros_range(values: Iterable[float]) -> Optional[Range]:
61    """Returns the range of the absolute values of the finite non-zeros.
62
63    Args:
64      values: An iterable object of float values.
65
66    Returns:
67      The range of the absolute values of the finite non-zeros, None if no such
68      value is found.
69    """
70    minimum: Optional[float] = None
71    maximum: Optional[float] = None
72    for v in values:
73        v = abs(v)
74        if math.isinf(v) or v == 0.0:
75            continue
76        if minimum is None:
77            minimum = v
78            maximum = v
79        else:
80            minimum = min(minimum, v)
81            maximum = max(maximum, v)
82
83    assert (maximum is None) == (minimum is None), (minimum, maximum)
84
85    if minimum is None:
86        return None
87    return Range(minimum=minimum, maximum=maximum)

Returns the range of the absolute values of the finite non-zeros.

Arguments:
  • 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.

@dataclasses.dataclass(frozen=True)
class ModelRanges:
 90@dataclasses.dataclass(frozen=True)
 91class ModelRanges:
 92    """The ranges of the absolute values of the finite non-zero values in the model.
 93
 94    Each range is optional since there may be no finite non-zero values
 95    (e.g. empty model, empty objective, all variables unbounded, ...).
 96
 97    Attributes:
 98      objective_terms: The linear and quadratic objective terms (not including the
 99        offset).
100      variable_bounds: The variables' lower and upper bounds.
101      linear_constraint_bounds: The linear constraints' lower and upper bounds.
102      linear_constraint_coefficients: The coefficients of the variables in linear
103        constraints.
104    """
105
106    objective_terms: Optional[Range]
107    variable_bounds: Optional[Range]
108    linear_constraint_bounds: Optional[Range]
109    linear_constraint_coefficients: Optional[Range]
110
111    def __str__(self) -> str:
112        """Prints the ranges in scientific format with 2 digits (i.e.
113
114        f'{x:.2e}').
115
116        It returns a multi-line table list of ranges. The last line does NOT end
117        with a new line.
118
119        Returns:
120          The ranges in multiline string.
121        """
122        buf = io.StringIO()
123
124        def print_range(prefix: str, value: Optional[Range]) -> None:
125            buf.write(prefix)
126            if value is None:
127                buf.write("no finite values")
128                return
129            # Numbers are printed in scientific notation with a precision of 2. Since
130            # they are expected to be positive we can ignore the optional leading
131            # minus sign. We thus expects `d.dde[+-]dd(d)?` (the exponent is at least
132            # 2 digits but double can require 3 digits, with max +308 and min
133            # -308). Thus we can use a width of 9 to align the ranges properly.
134            buf.write(f"[{value.minimum:<9.2e}, {value.maximum:<9.2e}]")
135
136        print_range("Objective terms           : ", self.objective_terms)
137        print_range("\nVariable bounds           : ", self.variable_bounds)
138        print_range("\nLinear constraints bounds : ", self.linear_constraint_bounds)
139        print_range(
140            "\nLinear constraints coeffs : ", self.linear_constraint_coefficients
141        )
142        return buf.getvalue()

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.
ModelRanges( objective_terms: Optional[Range], variable_bounds: Optional[Range], linear_constraint_bounds: Optional[Range], linear_constraint_coefficients: Optional[Range])
objective_terms: Optional[Range]
variable_bounds: Optional[Range]
linear_constraint_bounds: Optional[Range]
linear_constraint_coefficients: Optional[Range]
def compute_model_ranges( mdl: ortools.math_opt.python.model.Model) -> ModelRanges:
145def compute_model_ranges(mdl: model.Model) -> ModelRanges:
146    """Returns the ranges of the finite non-zero values in the given model.
147
148    Args:
149      mdl: The input model.
150
151    Returns:
152      The ranges of the finite non-zero values in the model.
153    """
154    return ModelRanges(
155        objective_terms=absolute_finite_non_zeros_range(
156            term.coefficient for term in mdl.objective.linear_terms()
157        ),
158        variable_bounds=merge_optional_ranges(
159            absolute_finite_non_zeros_range(v.lower_bound for v in mdl.variables()),
160            absolute_finite_non_zeros_range(v.upper_bound for v in mdl.variables()),
161        ),
162        linear_constraint_bounds=merge_optional_ranges(
163            absolute_finite_non_zeros_range(
164                c.lower_bound for c in mdl.linear_constraints()
165            ),
166            absolute_finite_non_zeros_range(
167                c.upper_bound for c in mdl.linear_constraints()
168            ),
169        ),
170        linear_constraint_coefficients=absolute_finite_non_zeros_range(
171            e.coefficient for e in mdl.linear_constraint_matrix_entries()
172        ),
173    )

Returns the ranges of the finite non-zero values in the given model.

Arguments:
  • mdl: The input model.
Returns:

The ranges of the finite non-zero values in the model.