Advanced Usage Guide¶
Introduction¶
The Quick Start Guide covers the basic workflow for scaling a model and recovering the unscaled solution. This section describes the more advanced capabilities of the Model Scaling module: choosing between scaling methods, controlling convergence, opting out selected variables or constraints, providing user-defined initial scaling factors, inheriting variable and constraint attributes, accessing the scaling matrices directly, and computing the unscaled objective value.
Choosing a Scaling Method¶
Three iterative scaling methods are available. The table below summarises which model types each supports.
Note
This module uses a finer classification of model types than Gurobi’s own documentation. Gurobi refers to models with quadratic constraints generically as “QCP”, regardless of whether a quadratic objective is also present. Here we distinguish:
(MI)QCP: quadratic constraints with a linear objective.
(MI)QCQP: quadratic constraints and a quadratic objective.
This distinction matters because support for the three scaling methods depends on whether a quadratic objective is present, not on whether quadratic constraints are present.
Method |
(MI)LP |
(MI)QP |
(MI)QCP |
(MI)QCQP |
(MI)NLP |
|---|---|---|---|---|---|
|
✓ |
✓ |
✓ |
✓ |
— |
|
✓ |
— |
✓ |
— |
— |
|
✓ |
— |
✓ |
— |
— |
(MI)NLP models are not currently supported by any scaling method.
Equilibration is the recommended default. It iteratively scales rows and
columns to bring the magnitudes of the nonzero coefficients to a similar range,
following the approach described in [ES11]. It is the only method
that supports models with a quadratic objective ((MI)QP and (MI)QCQP). For
(MI)LP and (MI)QCP models (linear objective), geometric_mean and
arithmetic_mean are also available.
For models with a quadratic objective ((MI)QP), the equilibration procedure differs from the LP case. Instead of scaling the constraint matrix alone, a symmetric KKT matrix is formed from the quadratic objective matrix and the constraint matrix, and a modified Ruiz equilibration [SBG+20] is applied jointly to scale the objective and constraints. An additional cost scaling step normalises the objective coefficients at each iteration.
Geometric mean scaling [ES11] scales each row \(i\) by \(1/\sqrt{a_i^{\max} \cdot a_i^{\min}}\), where \(a_i^{\max}\) and \(a_i^{\min}\) are the largest and smallest absolute nonzero values in that row or column. This tends to perform well when the coefficient range spans many orders of magnitude.
Arithmetic mean scaling [ES11] scales both rows and columns by the reciprocal of the mean absolute value of their nonzero entries. It can perform better than equilibration when the distribution of coefficient magnitudes is unimodal and roughly symmetric on a log scale.
When a geometric_mean or arithmetic_mean method is called for a model
with a quadratic objective, a warning is issued and equilibration is used
automatically.
Controlling Convergence¶
The iterative scaling algorithm repeats until the maximum deviation of the
scaling factors from 1 falls below scale_conv_tol, the number of passes reaches
scale_passes, or the elapsed time budget is exhausted. When
scaling_time_limit is set, the algorithm completes the current pass before
stopping; it does not interrupt a pass mid-way.
m_scaled = gma.scale_model(
m,
method="equilibration",
scale_passes=10, # allow up to 10 iterations
scale_conv_tol=1e-6, # tighter convergence threshold
scaling_time_limit=30.0, # stop after completing the current pass
)
The default of one pass is intentionally conservative: a single scaling pass can itself take significant time on large models, and the primary goal is to reduce the solver’s work, not to achieve a perfectly scaled matrix at any cost. The tradeoff has two dimensions:
Scaling time vs. solution time. Additional passes improve conditioning and can substantially reduce solver runtime, but they add upfront cost. For a model that is already moderately well-scaled, one pass may be enough to achieve the desired speedup. For severely ill-conditioned models, allowing more passes is likely worthwhile.
Conditioning vs. unscaled solution quality. More scaling passes generally improve numerical conditioning but can increase unscaled constraint and bound violations after solving, because the solution is recovered in the original variable space by applying the inverse scaling transformation.
Users are encouraged to experiment with scale_passes for their specific
model and to evaluate the resulting MaxUnscVio alongside solver runtime to
find the right balance.
The scaling_lb and scaling_ub parameters bound the scaling factors,
preventing extreme rescaling that could itself introduce numerical issues:
m_scaled = gma.scale_model(
m,
method="geometric_mean",
scaling_lb=1e-4, # factors clipped to [1e-4, 1e4]
scaling_ub=1e4,
)
Opt-Out Per Variable or Constraint¶
Individual variables or constraints can be excluded from scaling by setting a
_scale attribute to 0 before calling scale_model(). This is
useful when a particular variable or constraint is already well-scaled and
should not be modified, or when the physical interpretation of a coefficient
must be preserved.
m.getVarByName("x1")._scale = 0 # exclude from column scaling
m.getConstrByName("c1")._scale = 0 # exclude from row scaling
m_scaled = gma.scale_model(m, method="equilibration")
Integer and binary variables are always excluded from column scaling,
regardless of the _scale attribute. Setting _scale = 0 on a continuous
variable or a constraint simply extends this opt-out to those objects.
User-Provided Initial Scaling¶
For cases where domain knowledge about suitable scaling factors is available,
the init_scaling parameter controls how user-provided initial factors are
used. These factors are set via the _init_scaling attribute on individual
variables and constraints before calling scale_model().
Mode 0 (default): ignore any _init_scaling attributes and run the
iterative algorithm from the identity scaling.
m_scaled = gma.scale_model(m, method="equilibration", init_scaling=0)
Mode 1: apply _init_scaling as the final scaling and return
immediately, without running any iterative algorithm. This is useful when
the user has pre-computed scaling factors and wants to apply them directly.
for var in m.getVars():
var._init_scaling = my_col_factors[var.VarName]
for constr in m.getConstrs():
constr._init_scaling = my_row_factors[constr.ConstrName]
m_scaled = gma.scale_model(m, method="equilibration", init_scaling=1)
Mode 2 (warmstart): pre-apply _init_scaling to the coefficient matrix,
then run the iterative algorithm on top. The final scaling factors are the
product of the user-provided values and the algorithm’s output. This is useful
when rough scaling estimates are available but further refinement is desired.
m_scaled = gma.scale_model(m, method="equilibration", init_scaling=2)
When both _init_scaling and _scale = 0 are set on the same variable or
constraint, _init_scaling takes priority. The scaling factor is fixed at
the value of _init_scaling and is not modified by the algorithm. It is
held constant throughout all passes, effectively locking that factor in place.
Inherited Variable and Constraint Attributes¶
scale_model() automatically transfers selected
attributes from the original model’s variables and constraints to their
counterparts in the scaled model.
Attributes that represent primal values are divided by the column scaling factor \(s_i\) (since \(x_i = s_i y_i\), a value in the original space becomes \(v / s_i\) in the scaled space). All other attributes are copied without modification.
Attribute |
Treatment |
Description |
|---|---|---|
|
scaled |
MIP warm-start value. |
|
scaled |
Variable hint value for the MIP solver. |
|
scaled |
Primal start value for LP simplex. |
|
copied |
Variable hint priority. |
|
copied |
Branching priority for the MIP solver. |
|
copied |
Partition number for the sub-MIP heuristic. |
|
copied |
Simplex basis status. Works both when set by the user before a solve and when read from a solved model. |
Attribute |
Treatment |
Description |
|---|---|---|
|
copied |
Lazy constraint flag (0 = not lazy; 1, 2, 3 = laziness levels). |
|
copied |
Simplex basis status of the constraint’s slack variable. Works both when set by the user before a solve and when read from a solved model. |
Attributes that are not set on the original model are silently skipped.
VBasis and CBasis are handled via a temporary .bas file, which
captures both user-specified warm-start bases (set before any solve) and
post-solve bases. Because Gurobi only exposes these attributes for reading
after a basic solution exists, the file-based approach is the only reliable
way to transfer them in both scenarios.
Simplex warm start. A complete simplex warm start requires both VBasis
(variables) and CBasis (constraints). When both are available from a prior
solve of the original model, they are inherited by the scaled model and allow
the simplex algorithm to start from the corresponding basis:
import gurobipy as gp
import gurobi_modelanalyzer as gma
m = gp.read("model.mps")
m.setParam("Method", 1) # dual simplex
m.optimize()
# Scale: VBasis and CBasis are automatically inherited
m_scaled = gma.scale_model(m, method="equilibration")
m_scaled.setParam("Method", 1)
m_scaled.optimize() # starts from the inherited simplex basis
Power-of-Two Scaling¶
Setting power_of_2=True rounds every final scaling factor to the nearest
power of 2 before the scaled model is built:
m_scaled = gma.scale_model(m, method="equilibration", power_of_2=True)
Powers of 2 have exact representations in IEEE 754 floating-point arithmetic. After rounding, multiplying or dividing a coefficient by its scaling factor introduces no additional round-off error, which can reduce cancellation in subsequent computations. The trade-off is that the coefficient ranges in the scaled model will not be quite as tight as without rounding.
The rounding is applied only to the final factors, after all iterative
passes have completed and after any init_scaling accumulation. The scaled
coefficient matrices are recomputed from the original model data using the
rounded factors, so the scaled model is always built consistently.
Power-of-two scaling can be combined with any method and with
init_scaling:
# Warmstart from a previous run, then round to powers of 2
read_scaling_file("model.scl", m)
m_scaled = gma.scale_model(
m, method="equilibration", init_scaling=2, power_of_2=True
)
Accessing the Scaling Matrices¶
The row and column scaling factors are stored as diagonal sparse matrices on
the ScaledModel object and can be accessed via
the ColScaling and RowScaling properties:
m_scaled = gma.scale_model(m, method="geometric_mean")
col_factors = m_scaled.ColScaling.diagonal() # shape: (num_vars,)
row_factors = m_scaled.RowScaling.diagonal() # shape: (num_constrs,)
print(f"Column factor range: [{col_factors.min():.2e}, {col_factors.max():.2e}]")
print(f"Row factor range: [{row_factors.min():.2e}, {row_factors.max():.2e}]")
A wide column factor range suggests the original model had highly variable coefficient magnitudes across variables. Inspecting the individual factors can reveal which variables or constraints drove the need for scaling.
The factor for each individual variable or constraint is also accessible
directly as a scaling_factor attribute on the wrapper objects returned by
ScaledModel.getVarsUnscaled() and
ScaledModel.getConstrsUnscaled():
m_scaled = gma.scale_model(m, method="equilibration")
for var in m_scaled.getVarsUnscaled():
print(f"{var.VarName}: scaling factor = {var.scaling_factor:.4e}")
for constr in m_scaled.getConstrsUnscaled():
print(f"{constr.ConstrName}: scaling factor = {constr.scaling_factor:.4e}")
# Quadratic constraints follow the same pattern:
for qconstr in m_scaled.getQConstrsUnscaled():
print(f"{qconstr.QCName}: scaling factor = {qconstr.scaling_factor:.4e}")
Computing the Unscaled Objective¶
To retrieve the objective value in the original variable space after
optimization, use ScaledModel.computeUnscObj():
m_scaled.optimize()
m_scaled.computeUnscObj()
print(f"Unscaled objective: {m_scaled.UnscObjVal:.6e}")
print(f"Scaled objective: {m_scaled.ObjVal:.6e}")
The unscaled and scaled objective values will generally differ because the objective coefficients are also transformed during scaling.
Scaling Log¶
Scaling progress is printed to the console by default
(scaling_log_to_console=1). It can be redirected to a file, or suppressed
entirely:
# Log to file only
m_scaled = gma.scale_model(
m,
method="equilibration",
scaling_log="scaling.log",
scaling_log_to_console=0,
)
# Log to both console and file
m_scaled = gma.scale_model(
m,
method="equilibration",
scaling_log="scaling.log",
scaling_log_to_console=1,
)
The log reports the original model’s coefficient ranges, the maximum deviation of the scaling factors from 1 per pass, elapsed time per pass, total scaling time, and the scaled model’s coefficient ranges. A typical log looks like:
Scaling Method: arithmetic_mean
Scale Passes: 5
Conv. Tol.: 1.000000e-04
Original Model Statistics:
Statistics for model 'glass4':
Problem type : MIP
Linear constraint matrix : 396 rows, 322 columns, 1815 nonzeros
Variable types : 20 continuous, 302 integer (0 binary)
Matrix range : [1e+00, 8e+06]
Objective range : [1e+00, 1e+06]
Bounds range : [1e+00, 8e+02]
RHS range : [1e+00, 8e+06]
Scale Pass Max Factor Dev. Time (s)
1 8.104301e+03 0.01
2 3.270924e+01 0.01
3 2.626976e+01 0.02
4 4.890177e+00 0.02
5 1.053361e+00 0.03
Building scaled model...
Scaling completed in 0.04 seconds
Scaled Model Ranges:
Matrix range : [2e-01, 4e+00]
Objective range : [2e+04, 1e+10]
Bounds range : [5e-03, 1e+00]
RHS range : [9e-03, 4e+00]
The Max Factor Dev. column shows the maximum deviation of the scaling
factors from 1 in that pass. When it falls below scale_conv_tol,
the algorithm has converged and no further passes are performed. The Time (s)
column shows cumulative wall-clock time up to and including that pass. Comparing the
original and scaled Matrix range shows how much the coefficient spread has
been reduced. A tighter range often indicates better conditioning.
Scaling Files¶
Scaling factors can be saved to and loaded from plain-text .scl files.
This makes it possible to:
reproduce a previously found scaling exactly on a re-run,
share scaling factors between runs or users,
provide domain-knowledge-based initial factors without writing Python code,
use scaling factors found via the Python API as input to
gurobi_cls, and vice versa.
File Format¶
A .scl file is a plain-text file. Lines beginning with # and blank
lines are ignored. An optional version header must appear before the first
section if present:
GRB_SCL_FILE_VERSION 1
Data is organised into up to three named sections:
SECTION VARS
SECTION CONSTRS
SECTION QCONSTRS
Each data line within a section has the form:
name factor lock_flag
where name is the variable or constraint name as it appears in the model,
factor is a positive floating-point scaling factor, and lock_flag is
either 0 (keep the factor fixed; the algorithm will not modify it) or
1 (use the factor as a warmstart; the algorithm may adjust it further).
All three sections are optional. A file may contain only a subset of the variables or constraints in the model; any object not listed defaults to an initial factor of 1.0.
A minimal example:
# My custom scaling factors
GRB_SCL_FILE_VERSION 1
SECTION VARS
price 1e-3 0
quantity 1e+2 1
SECTION CONSTRS
budget 5e-1 0
Writing Scaling Factors (Python API)¶
After calling scale_model(), use
ScaledModel.write_scaling() to export the computed factors:
import gurobipy as gp
import gurobi_modelanalyzer as gma
m = gp.read("model.mps")
m_scaled = gma.scale_model(m, method="equilibration")
# Save with lock_flag=0 (default): factors are fixed on re-import
m_scaled.write_scaling("model.scl")
# Save with lock_flag=1: factors act as a warmstart on re-import
m_scaled.write_scaling("model.scl", lock_factors=False)
Reading Scaling Factors (Python API)¶
Use read_scaling_file() to load a .scl
file and apply its factors to a model before calling
scale_model():
import gurobipy as gp
from gurobi_modelanalyzer.scaling import scale_model, read_scaling_file
m = gp.read("model.mps")
read_scaling_file("model.scl", m)
m_scaled = scale_model(m, method="equilibration", init_scaling=2)
The function sets _init_scaling and, where applicable, _scale = 0
directly on the model’s variable and constraint objects. The init_scaling
parameter of scale_model() must be set to 1
or 2 for these attributes to take effect (see
User-Provided Initial Scaling above). Use init_scaling=2 to run the
algorithm as a warmstart on top of the loaded factors, or init_scaling=1
to apply them without any further iteration.
Malformed lines and unrecognised names issue UserWarning automatically
via Python’s warnings module. They can be suppressed with:
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
Round-Trip Example¶
The following pattern scales a model, saves the result, and reproduces the same scaling on a later run:
import gurobipy as gp
import gurobi_modelanalyzer as gma
from gurobi_modelanalyzer.scaling import read_scaling_file
# --- First run: compute and save scaling ---
m = gp.read("model.mps")
m_scaled = gma.scale_model(m, method="equilibration")
m_scaled.write_scaling("model.scl") # lock_flag=0 by default
# --- Later run: reproduce the exact same scaling ---
m2 = gp.read("model.mps")
read_scaling_file("model.scl", m2)
m2_scaled = gma.scale_model(m2, method="equilibration", init_scaling=2)
Because the saved factors use lock_flag=0, the algorithm cannot modify
them further, and m2_scaled has the same coefficient matrix as m_scaled.