CP-SAT Primer

repository·main·Indexed 20 days ago

https://github.com/d-krupke/cpsat-primer

A comprehensive guide for using and understanding Google OR-Tools' CP-SAT solver. It covers educational content from basic concepts to advanced modeling techniques, including declarative modeling workflows, solver reasoning (CP, SAT, and MIP), and practical examples such as embedding CP-SAT in Streamlit apps using multiprocessing. The repository also includes benchmarks for the Traveling Salesperson Problem (TSP) and rectangular packing problems.

Tokens
100.2K
Snippets
205
Records
285
Agent score
71%

What's inside CP-SAT Primer

  1. Overview of the CP-SAT Primer content

    main

    The CP-SAT Primer is divided into two main parts:

    Part 1: The Basics

    Focuses on fundamental CP-SAT features:

    • Installation & Hardware: Setting up the environment.
    • Basic Modeling: Creating variables, objectives, and constraints.
    • Advanced Modeling: Handling complex constraints like circuit constraints and intervals.
    • Solver Behavior: Specifying parameters (time limits, parallelization), interpreting logs, and understanding the search core.
    • Context: Comparing CP-SAT with other techniques and using MathOpt as a modeling layer.

    Part 2: Advanced Topics

    Focuses on engineering and deployment skills:

    • Coding Patterns: Design patterns for maintainable algorithms.
    • Deployment: Building optimization APIs for long-running jobs.
    • Heuristics & Benchmarking: Developing powerful heuristics (e.g., Large Neighborhood Search) and scientifically benchmarking models.
  2. Advanced Modeling: Overview of Complex Constraints

    main

    CP-SAT provides several powerful, domain-specific constraints for complex optimization problems. These include:

    • Circuit/Tour Constraints: For routing and sequencing problems (e.g., TSP, VRP).
    • Interval Constraints: For scheduling and resource management (e.g., new_interval_var, add_no_overlap, add_cumulative).
    • Automaton Constraints: For modeling state-based transitions (add_automaton).
    • Reservoir Constraints: For modeling flow and storage (add_reservoir_constraint).
    • Piecewise Linear Constraints: For modeling non-linear relationships (available via custom code snippets).
  3. What is MathOpt and when to use it

    main

    MathOpt is a solver-agnostic modeling layer provided by Google OR-Tools. It offers a modern API for defining linear programs (LPs) and mixed-integer programs (MIPs).

    Key Benefits

    • Solver Agnostic: Easily switch between different solvers (e.g., HiGHS, Gurobi, CP-SAT) to find the best performance for your problem.
    • Continuous Variables: Unlike CP-SAT, MathOpt supports continuous variables and floating-point coefficients when using compatible backends.

    Limitations and Caveats

    • CP-SAT Backend: When using CP_SAT as the backend, MathOpt's continuous and floating-point features are unavailable. You must discretize variables and coefficients to use CP-SAT.
    • Constraint Types: MathOpt currently only supports linear constraints. It does not support non-linear operators like !=, <, or > (use equality or inequality forms instead).
    • Overhead: There is a small amount of overhead compared to using a solver's native API directly.
  4. Overview of the Declarative Modeling workflow

    main

    This manuscript focuses on the discipline of modeling: converting informal combinatorial optimization problems into correct formulations for the CP-SAT solver.

    Instead of ad hoc trial and error, the recommended systematic workflow follows these stages:

    1. Paradigm Shift: Moving from writing an algorithm to describing a problem.
    2. Mathematical Notation: Using math as a working tool.
    3. Paper Model: Creating a manual formulation.
    4. Python Verifier (Optional): Building a script that judges candidate solutions independently of the solver to ensure correctness.
    5. CP-SAT Encoding: Implementing the final working model in code.

    Note: This resource focuses on correctness (ensuring a verifier can check the model and a solver can execute it) rather than performance engineering.

  5. What is property-based testing and how does it work?

    main

    Property-based testing is a methodology where you specify general properties (invariants) that your code must satisfy, and a tool automatically generates a wide range of test cases to verify them.

    Unlike naive random testing, libraries like Hypothesis use strategies to systematically generate inputs. If a failure is found, the library performs shrinking—attempting to find the smallest possible counterexample that still triggers the failure. This makes debugging much easier.

    In the context of optimization problems, property-based testing is used to ask questions like:

    • "For any valid instance of my optimization problem, does the solver return a feasible solution (or report infeasibility correctly)?"
    • "For any valid instance of the nurse rostering problem, is there always sufficient staffing to cover every shift?"

    This approach complements unit tests: use unit tests for clear, deterministic components, and property-based tests to explore a wide space of edge cases and schema invariants.

  6. What is Large Neighborhood Search (LNS)?

    main

    Large Neighborhood Search (LNS) is a meta-heuristic technique used to solve large-scale optimization problems that might be too difficult for standard CP-SAT execution alone.

    Instead of generating individual neighbor solutions one by one, LNS uses a "destroy and repair" approach:

    1. Destroy: Randomly select a subset of variables or parts of the current solution and reset them (e.g., deleting items from a knapsack).
    2. Repair: Formulate a "mini-problem" (subproblem) using the remaining solution as context and use a solver like CP-SAT to find the optimal new values for the destroyed parts.

    This allows for a much broader exploration of the search space than traditional local search methods. While CP-SAT uses internal LNS subsolvers (like graph_arc_lns or rins/rens), you can implement your own problem-specific LNS to leverage your knowledge of the problem's structure, which often outperforms the solver's agnostic approach.

    8 incomplete subsolvers: [feasibility_pump, graph_arc_lns, graph_cst_lns, graph_dec_lns, graph_var_lns, rins/rens, rnd_cst_lns, rnd_var_lns]
  7. Implement Lazy Variable Construction

    main

    In models with a massive number of potential auxiliary variables (e.g., quadratic combinations of items), creating all variables upfront can lead to excessive memory usage and computational overhead.

    Lazy Variable Construction involves creating variables only when they are actually accessed. This is typically implemented by overriding the __getitem__ method in a manager class. When a specific variable is requested, the class checks if it has already been created; if not, it instantiates the cp_model.IntVar and adds the necessary constraints to the model before returning it.

    When to use:

    • When the number of possible variables is large (e.g., quadratic in the number of items) but only a small subset is expected to be relevant to the constraints.
    • To reduce the initial memory footprint of the model.
    class _CombiVariables:
        def __init__(
            self,
            instance: KnapsackInstance,
            model: cp_model.CpModel,
            item_vars: _ItemSelectionVars,
        ):
            self.instance = instance
            self.model = model
            self.item_vars = item_vars
            self.bonus_vars = {}
    
        def __getitem__(self, item_pair: Tuple[int, int]) -> cp_model.IntVar:
            i, j = sorted(item_pair)
            if (i, j) not in self.bonus_vars:
                var = self.model.NewBoolVar(f"bonus_{i}_{j}")
                # Add constraint: var is true only if both items are packed
                self.model.add(
                    self.item_vars.packs_item(i) + self.item_vars.packs_item(j) >= 2 * var
                )
                self.bonus_vars[(i, j)] = var
            return self.bonus_vars[(i, j)]
  8. Handle multi-objective optimization via iterative phases

    main

    When a problem has multiple competing objectives (e.g., maximizing value while minimizing weight), use a phased approach to explore the Pareto front:

    1. Phase 1: Optimize for the primary objective (e.g., maximize(value)).
    2. Phase 2: Add a constraint that the primary objective must stay within a certain threshold of the Phase 1 result (e.g., value >= 0.95 * optimal_value).
    3. Phase 3: Change the objective to the secondary goal (e.g., minimize(weight)).

    This allows you to find solutions that satisfy multiple criteria without needing a single complex weighted objective function.

  9. Maintain reservoir levels with `add_reservoir`

    main

    The reservoir constraint is used to maintain a balance between inflows and outflows, ensuring a level stays within a specified range over time.

    Logic: If times[i] is assigned a value t, the current level changes by level_changes[i]. The constraint ensures that for all time t, the cumulative sum of changes is within the bounds: sum(level_changes[i] if times[i] <= t) in [min_level, max_level].

    Constraints/Limitations:

    • level_changes must be constant values (variable level changes are currently not supported).