ortools.math_opt.python.statistics

Statistics about MIP/LP models.

  1#!/usr/bin/env python3
  2# Copyright 2010-2025 Google LLC
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""Statistics about MIP/LP models."""
 16
 17import dataclasses
 18import io
 19import math
 20from typing import Iterable, Optional
 21
 22from ortools.math_opt.python import model
 23
 24
 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
 36
 37
 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    )
 58
 59
 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)
 88
 89
 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()
143
144
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    )
@dataclasses.dataclass(frozen=True)
class Range:
26@dataclasses.dataclass(frozen=True)
27class Range:
28    """A close range of values [min, max].
29
30    Attributes:
31      minimum: The minimum value.
32      maximum: The maximum value.
33    """
34
35    minimum: float
36    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: Range | None, rhs: Range | None) -> Range | None:
39def merge_optional_ranges(
40    lhs: Optional[Range], rhs: Optional[Range]
41) -> Optional[Range]:
42    """Merges the two optional ranges.
43
44    Args:
45      lhs: The left hand side range.
46      rhs: The right hand side range.
47
48    Returns:
49      A merged range (None if both lhs and rhs are None).
50    """
51    if lhs is None:
52        return rhs
53    if rhs is None:
54        return lhs
55    return Range(
56        minimum=min(lhs.minimum, rhs.minimum),
57        maximum=max(lhs.maximum, rhs.maximum),
58    )

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]) -> Range | None:
61def absolute_finite_non_zeros_range(values: Iterable[float]) -> Optional[Range]:
62    """Returns the range of the absolute values of the finite non-zeros.
63
64    Args:
65      values: An iterable object of float values.
66
67    Returns:
68      The range of the absolute values of the finite non-zeros, None if no such
69      value is found.
70    """
71    minimum: Optional[float] = None
72    maximum: Optional[float] = None
73    for v in values:
74        v = abs(v)
75        if math.isinf(v) or v == 0.0:
76            continue
77        if minimum is None:
78            minimum = v
79            maximum = v
80        else:
81            minimum = min(minimum, v)
82            maximum = max(maximum, v)
83
84    assert (maximum is None) == (minimum is None), (minimum, maximum)
85
86    if minimum is None:
87        return None
88    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:
 91@dataclasses.dataclass(frozen=True)
 92class ModelRanges:
 93    """The ranges of the absolute values of the finite non-zero values in the model.
 94
 95    Each range is optional since there may be no finite non-zero values
 96    (e.g. empty model, empty objective, all variables unbounded, ...).
 97
 98    Attributes:
 99      objective_terms: The linear and quadratic objective terms (not including the
100        offset).
101      variable_bounds: The variables' lower and upper bounds.
102      linear_constraint_bounds: The linear constraints' lower and upper bounds.
103      linear_constraint_coefficients: The coefficients of the variables in linear
104        constraints.
105    """
106
107    objective_terms: Optional[Range]
108    variable_bounds: Optional[Range]
109    linear_constraint_bounds: Optional[Range]
110    linear_constraint_coefficients: Optional[Range]
111
112    def __str__(self) -> str:
113        """Prints the ranges in scientific format with 2 digits (i.e.
114
115        f'{x:.2e}').
116
117        It returns a multi-line table list of ranges. The last line does NOT end
118        with a new line.
119
120        Returns:
121          The ranges in multiline string.
122        """
123        buf = io.StringIO()
124
125        def print_range(prefix: str, value: Optional[Range]) -> None:
126            buf.write(prefix)
127            if value is None:
128                buf.write("no finite values")
129                return
130            # Numbers are printed in scientific notation with a precision of 2. Since
131            # they are expected to be positive we can ignore the optional leading
132            # minus sign. We thus expects `d.dde[+-]dd(d)?` (the exponent is at least
133            # 2 digits but double can require 3 digits, with max +308 and min
134            # -308). Thus we can use a width of 9 to align the ranges properly.
135            buf.write(f"[{value.minimum:<9.2e}, {value.maximum:<9.2e}]")
136
137        print_range("Objective terms           : ", self.objective_terms)
138        print_range("\nVariable bounds           : ", self.variable_bounds)
139        print_range("\nLinear constraints bounds : ", self.linear_constraint_bounds)
140        print_range(
141            "\nLinear constraints coeffs : ", self.linear_constraint_coefficients
142        )
143        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: Range | None, variable_bounds: Range | None, linear_constraint_bounds: Range | None, linear_constraint_coefficients: Range | None)
objective_terms: Range | None
variable_bounds: Range | None
linear_constraint_bounds: Range | None
linear_constraint_coefficients: Range | None
def compute_model_ranges( mdl: ortools.math_opt.python.model.Model) -> ModelRanges:
146def compute_model_ranges(mdl: model.Model) -> ModelRanges:
147    """Returns the ranges of the finite non-zero values in the given model.
148
149    Args:
150      mdl: The input model.
151
152    Returns:
153      The ranges of the finite non-zero values in the model.
154    """
155    return ModelRanges(
156        objective_terms=absolute_finite_non_zeros_range(
157            term.coefficient for term in mdl.objective.linear_terms()
158        ),
159        variable_bounds=merge_optional_ranges(
160            absolute_finite_non_zeros_range(v.lower_bound for v in mdl.variables()),
161            absolute_finite_non_zeros_range(v.upper_bound for v in mdl.variables()),
162        ),
163        linear_constraint_bounds=merge_optional_ranges(
164            absolute_finite_non_zeros_range(
165                c.lower_bound for c in mdl.linear_constraints()
166            ),
167            absolute_finite_non_zeros_range(
168                c.upper_bound for c in mdl.linear_constraints()
169            ),
170        ),
171        linear_constraint_coefficients=absolute_finite_non_zeros_range(
172            e.coefficient for e in mdl.linear_constraint_matrix_entries()
173        ),
174    )

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.