Skip to main content

Optimal Solver (MIP-based)

The Optimal solver uses Mixed Integer Programming (MIP) to find provably optimal solutions. It's recommended for small to medium problems (<10,000 objects) where solution quality is critical.

How It Works

The Optimal solver:

  1. Formulates the problem as a Mixed Integer Program (MIP)

    • Binary variables: Is object X assigned to container Y?
    • Linear constraints: Capacity, diversity, etc.
    • Objective function: Minimize goals
  2. Solves using external MIP solver (HiGHS, Gurobi, or XPRESS)

    • Branch-and-bound search
    • Cutting planes
    • Presolving and heuristics
  3. Returns optimal solution (or best found within time limit)

    • Proven optimal if solved to completion
    • Otherwise provides optimality gap

Supported MIP Solvers

Rebalancer supports three MIP solvers:

SolverLicensePerformanceScaleBest For
HiGHSOpen source (MIT)Good<5K objectsFree, open source, small problems
XPRESSCommercial + CommunityExcellent<10K objectsCommunity license available
GurobiCommercial + AcademicExcellent<10K objectsAcademic license, best performance

HiGHS (Open Source)

Pros:

  • Completely free and open source (MIT license)
  • No license required
  • Good for experimentation and small problems
  • Active development and community

Cons:

  • Slower than commercial solvers
  • May not scale to 10K+ objects

Installation:

conda install conda-forge::highs
# or
pip install highspy

Enable in CMake following the README.md instructions.

XPRESS (Community License)

Pros:

  • Free community license available
  • Excellent performance
  • Scales well to 10K objects

Cons:

  • Requires registration
  • Community license has size limits
  • Commercial license for large-scale use

Getting Started:

  1. Download from FICO XPRESS website
  2. Get community license (free)
  3. Install and add to PATH
  4. Enable in CMake according to the README.md instructions.

Gurobi (Commercial/Academic)

Pros:

  • Best MIP performance
  • Free academic licenses
  • Excellent documentation and support

Cons:

  • Requires license
  • Commercial licenses expensive
  • Academic licenses need verification

Getting Started:

  1. Download from Gurobi website
  2. Get academic license (free) or purchase commercial license
  3. Install and set license file
  4. Enable in CMake according to the README.md instructions.

Quick Example

solver.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
timeLimitMs=300000, # 5 minute time limit
mipGap=0.01, # Stop when within 1% of optimal
)
)
)

solution = solver.solve()

# Check if optimal
if solution.profile.optimal:
print("Proven optimal solution!")
else:
gap = solution.profile.mipGap
print(f"Solution within {gap * 100:.2f}% of optimal")

Configuration Parameters

ParameterTypeDefaultDescription
solveTimeintNo limitMaximum solve time in milliseconds
solverPackageenumXPRESSMIP solver to use (XPRESS, GUROBI, HIGHS)
xpressArgsmap<string, double>Solver-specific parameters (e.g., XPRS_MIPRELSTOP, XPRS_THREADS)
printFullLpboolfalsePrint full LP formulation for debugging
skipInitialAssignmentHintboolfalseSkip using initial assignment as warm start
enablePartitionHeuristicboolfalseEnable partition-based heuristic
simplifyLpProblemboolfalseSimplify LP before solving

Understanding MIP Gap

The MIP gap measures solution quality:

MIP Gap = (Upper Bound - Lower Bound) / Lower Bound
  • Lower Bound: Best solution found so far (feasible)
  • Upper Bound: Proven best possible objective (may be infeasible)
  • Gap = 0: Proven optimal
  • Gap = 0.01: Solution is within 1% of optimal

Example

solution = solver.solve()

if solution["profile"].optimal:
print("Exactly optimal")
elif solution["profile"].mipGap == 0.0:
print("Proven optimal (gap closed)")
elif solution["profile"].mipGap <= 0.05:
print(f"Very good: within {solution["profile"].mipGap*100:.1f}% of optimal")
elif solution["profile"].mipGap <= 0.20:
print(f"Acceptable: within {solution["profile"].mipGap*100:.1f}% of optimal")
else:
print(f"Poor: {solution["profile"].mipGap*100:.1f}% gap - may want to increase time limit")

Performance Characteristics

Scalability

Problem SizeTypical TimeNotes
10 objects, 5 containers<1 secondTrivial
100 objects, 20 containers1-10 secondsEasy
1,000 objects, 100 containers10 seconds - 5 minutesModerate
5,000 objects, 500 containersMinutes to hoursHard
10,000 objects, 1,000 containersHours or timeoutVery hard
>10,000 objectsUsually infeasibleUse Local Search

Note: Times vary significantly based on:

  • Problem structure
  • Number/complexity of goals and constraints
  • MIP solver used (Gurobi > XPRESS > HiGHS)
  • Initial assignment quality

Memory Usage

MIP solvers are memory-intensive:

  • Small problem (100 objects): ~100 MB
  • Medium problem (1,000 objects): ~1-5 GB
  • Large problem (10,000 objects): ~10-50 GB (may OOM)

Recommendation: Monitor memory usage for problems >1,000 objects.

Solution Quality Guarantees

Proven Optimal

When mipGap == 0 and optimal == true:

  • Solution is provably optimal
  • No other solution can be better
  • Mathematically guaranteed

Within Gap

When mipGap > 0:

  • Solution is within (mipGap × 100)% of optimal
  • Example: mipGap = 0.05 means solution is at most 5% worse than optimal

No Solution Found

If timeout before finding any feasible solution:

  • Problem may be infeasible
  • Or just needs more time
  • Check solver logs

Common Patterns

Small Problem, Need Optimal

# Small problem, can wait for optimal
solver.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
timeLimitMs=600000, # 10 minutes
mipGap=0.0, # Don't stop until proven optimal
)
)
)

Medium Problem, Time Limited

# Medium problem, accept "good enough"
solver.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
timeLimitMs=300000, # 5 minutes
mipGap=0.05, # Stop when within 5%
)
)
)

Quick Optimality Check

# See if we can solve quickly
solver.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
timeLimitMs=60000, # 1 minute
mipGap=0.10, # Accept 10% gap
)
)
)
# Run Local Search first
solver1 = ProblemSolver(service_name="example", service_scope="test")
# ... set up problem ...
solver1.addSolver(SolverSpecs(localSearchSolverSpec=LocalSearchSolverSpec()))
ls_solution = solver1.solve()

# Then run Optimal on same problem
solver2 = ProblemSolver(service_name="example", service_scope="test")
# ... set up same problem ...
solver2.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
timeLimitMs=300000,
)
)
)
opt_solution = solver2.solve()

# Compare
gap = (
ls_solution.objectiveValue - opt_solution.objectiveValue
) / opt_solution.objectiveValue
print(f"Local Search was {gap * 100:.1f}% from optimal")

Warm Starting

Provide an initial solution to help the MIP solver:

# Run Local Search first, then Optimal
solver.addSolver(
SolverSpecs(localSearchSolverSpec=LocalSearchSolverSpec(timeLimitMs=10000))
)
solver.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
timeLimitMs=60000,
)
)
)
# Optimal will use Local Search result as warmstart

Warm starting can significantly speed up solving.

Debugging Slow Solving

Problem: Takes forever, no progress

Diagnosis:

# Check if solver is making progress
# Look at logs for bound improvements

Solutions:

  • Reduce problem size (fewer objects, containers)
  • Simplify goals/constraints (remove non-essential ones)
  • Increase mipGap (accept worse solution)
  • Use Local Search instead

Problem: Runs out of memory

Diagnosis:

  • Solver crashes or system OOM
  • Large problem (>5K objects)

Solutions:

  • Reduce problem size
  • Use Local Search instead
  • Add more RAM
  • Reduce threads (less parallel memory usage)

Problem: Can't find feasible solution

Diagnosis:

  • Timeout with no solution found
  • "Infeasible problem" error

Solutions:

  • Check constraints for conflicts
  • Relax some constraints (use as goals instead)
  • Increase time limit
  • Check initial assignment violates constraints

Problem: Gap not closing

Diagnosis:

  • MIP gap stuck at high value
  • Bounds not improving

Solutions:

  • Increase time limit (may take hours)
  • Accept current gap (stop early)
  • Try different MIP solver (Gurobi often better)
  • Simplify problem

Solver Selection

Which MIP solver to use?

Use HiGHS if:

  • ✅ You want completely open source
  • ✅ Small problems (<1,000 objects)
  • ✅ Just experimenting
  • ✅ Don't want to deal with licenses

Use XPRESS if:

  • ✅ You have community license
  • ✅ Medium problems (1,000-10,000 objects)
  • ✅ Need good performance
  • ✅ Willing to register for license

Use Gurobi if:

  • ✅ You have academic/commercial license
  • ✅ Need best performance
  • ✅ Medium to large problems
  • ✅ Already using Gurobi for other work

Note: All three use the same Rebalancer API - switching solvers is just a matter of installation.

AspectOptimalLocal Search
Solution quality100% optimal*95-99% of optimal
SpeedSlow (minutes-hours)Fast (seconds)
Scalability<10K objects1M+ objects
GuaranteesOptimality gapNone
MemoryHigh (GBs)Low (MBs)
Best forSmall problems, validationProduction, large problems

* Given enough time

Advanced Settings

Thread Control

# Use specific number of threads
solver.addSolver(
SolverSpecs(
optimalSolverSpec=OptimalSolverSpec(
solverPackage=OptimalSolverPackage.HIGHS,
threads=4, # Use 4 cores
)
)
)

Solver Tuning

# Configure solver-specific parameters via xpressArgs
solver.add_solver(
SolverSpec(
optimalSolverSpec=OptimalSolverSpec(
solveTime=300000, # 5 minutes
xpressArgs={
"XPRS_MIPRELSTOP": 0.01, # Stop at 1% MIP gap
"XPRS_THREADS": 8, # Use 8 threads
"XPRS_PRESOLVE": 2, # Aggressive presolving
"XPRS_CUTSTRATEGY": 3, # Aggressive cuts
"XPRS_HEURSTRATEGY": 3, # Aggressive heuristics
},
enablePartitionHeuristic=True, # Enable rebalancer heuristic
)
))

Note: The xpressArgs keys depend on your chosen solver package. See XPRESS docs or Gurobi docs for available parameters.

Licensing Notes

HiGHS

  • MIT license
  • Completely free
  • No restrictions

XPRESS Community License

  • Free registration required
  • Size limits (check FICO website for current limits)
  • For academic and small commercial use
  • Full commercial license available for purchase

Gurobi Academic License

  • Free for academic use
  • Requires .edu email
  • Annual renewal
  • Cannot be used for commercial purposes

Gurobi Commercial License

  • Paid license
  • No restrictions
  • Pricing based on usage/features
  • Contact Gurobi for pricing

Next Steps