Discrete optimization

Our Journey with CP-SAT in Energy Trading

Jorge Rodriguez Vecino & Narve Sætre

Who?

Jorge Rodriguez

Software Engineer @ NODES

Narve Sætre

Software Engineer @ Dedica / NODES

Good old times

Happy traders

What is NODES™

Unlocking flexibility. Driving sustainability.
ShortFlex

A typical day @ NODES™

Buy

  • Buy order:
    • UnitPrice: 100€
    • MinQty: 4MW
    • MaxQty: 6MW
    • Renewable
  • Buy order:
    • UnitPrice: 80€
    • MinQty: 4MW
    • MaxQty: 10MW

...tens...

Sell

  • Sell order:
    • UnitPrice: 90€
    • MinQty: 2MW
    • MaxQty: 10MW
    • MaxAdjacentBlocks: 4
    • MinAdjacentRestBlocks: 5
    • NonRenewable
  • Sell order:
    • UnitPrice: 132€
    • MinQty: 0.3MW
    • MaxQty: 0.8MW
    • MaxAdjacentBlocks: 2

...hundreds...

Simple trading

Cavemen

Greedy algorithm

                  %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true }}}%%
                  graph LR
                      subgraph Buyers
                        B1[10 Wood]
                      end

                      subgraph Sellers
                        S1[1 Steak @ 3 Wood]
                        S2[1 Steak @ 5 Wood]
                        S3[1 Steak @ 6 Wood]
                      end

                      classDef highlighted stroke:#f00,stroke-width:4px;
                      B1 -- "Buys 1" --> S1
                      B1 -- "Buys 1" --> S2
                      B1 -- "Buys 1/3" --> S3
                

Greedy algorithm

                
    // Sort sell orders by lowest price
    var sortedOrders = sellOrders
        .Where(s => s.Price <= buy.Price)
        .OrderBy(p => p.Price);

    // Match orders until buy order is completed
    var matches = new List<(sellId, quantity)>();
    var quantityRemaining = buy.quantity;
    foreach (var sell in sortedOrders)
    {
        var matchQuantity = Math.Min(sell.Quantity, quantityRemaining);
        matches.Add(sell.Id, matchQuantity);
        quantityRemaining -= matchQuantity;

        if (quantityRemaining == 0)
            break;
    }
                
            

Non-fractional trading

Medieval

Greediness - not always good

Case

  • Buyer 1500 Coins
  • Seller 1 2KG for 400 coins
  • Seller 2 4KG for 1200 coins
  • Seller 3 1KG for 1000 coins

Solutions

  • Greedy algorithm:
    • Seller 1 and 3
    • Meat: 3KG
    • Price: 1400
  • Best solution:
    • Meat: 4KG
    • Price: 1200

Branch-And-Bound

1500 Coins
  • Seller 1: 2KG @ 400
  • Seller 2: 4KG @ 1200
  • Seller 3: 1KG @ 1000
                    %%{init: {'theme': 'dark', 'themeVariables': { 'darkMode': true }}}%%
                    flowchart TD
                        A["**Start**
                            Coins: 1500
                            Meat: 0KG
                            Bound: 5.66KG"] --> B1["**Take Seller 1**
                            Coins: 1100
                            Meat: 2KG
                            Bound: 5.66KG"]
                        A --> B2["**Skip Seller 1**
                            Coins: 1500
                            Meat: 0KG
                            Bound: 4.33KG"]

                        B1 --> C1["**Take Seller 2**
                            Coins: -100
                            Meat: -
                            Bound: -"]
                        B1 --> C2["**Skip Seller 2**
                            Coins: 1100
                            Meat: 2KG
                            Bound: 3KG"]

                        B2 --> C3["**Take Seller 2**
                            Coins: 300
                            Meat: 4KG
                            Bound: 4.33KG"]
                        B2 --> C4["**Skip Seller 2**
                            Coins: 1500
                            Meat: 0KG
                            Bound: **1KG**"]

                        C3 --> D5["**Take Seller 3**
                            Coins: -500
                            Meat: -
                            Bound: -"]
                        C3 --> D6["**Skip Seller 3**
                            Coins: 300
                            Meat: 4KG
                            Bound: 4KG"]

                    class C1,D5 red
                    class D6 green
                    class C4,C2 pruned
                

Complex constraints

Complex constraints

What we tried

  • To be smart
  • Split the problem into easier to tackle sub-problems
  • Use heuristics to find feasible solutions to the sub-problems
  • Use heuristics and branch-and-bound to find a good global solution

What it led to

  • Great performance for easy optimization cases
  • Great performance for medium optimization cases
  • Splitting the problem at the wrong place led to impossible heuristics for some cases, which made optimization impossible.
Be careful with splitting problems and heuristics!

What now?

Sudoku

Sudoku

Sudoku

Sudoku
\[ \begin{aligned} \textbf{Variables:} & \quad x_{i,j} \in \{1, \dots, 9\} \quad \forall i,j \in \{1, \dots, 9\} \\ \textbf{Constraints:} & \\ \text{(1) Rows:} & \quad \forall i,\; \text{AllDifferent}(x_{i,1}, \dots, x_{i,9}) \\ \text{(2) Columns:} & \quad \forall j,\; \text{AllDifferent}(x_{1,j}, \dots, x_{9,j}) \\ \text{(3) Blocks:} & \quad \forall b_r, b_c \in \{0,1,2\},\; \text{AllDifferent of 3x3 block} \end{aligned} \]

Constraint Propagation

Sudoku
\[ \begin{aligned} \text{Domain:} & \quad x_{8,6} \in \{1, 2, 3, 4, 5, 6, 7, 8, 9\} \\ \\ \text{Rows:} & \quad \text{AllDifferent}(x_{8,1}, \dots, x_{8,9}) \\ \text{Domain:} & \quad x_{8,6} \in \{1, 3, 4, 5, 7, 9\} \\ \\ \text{Columns:} & \quad \text{AllDifferent}(x_{1,6}, \dots, x_{9,6}) \\ \text{Domain:} & \quad x_{8,6} \in \{4, 7\} \\ \\ \text{Blocks:} & \quad \forall b_r, b_c \in \{0,1,2\},\; \text{AllDifferent of 3x3 block} \\ \text{Domain:} & \quad x_{8,6} \in \{4\} \\ \end{aligned} \]

Show me the code!

Using C# and Google OR-Tools

Initializing the model:
                
    using Google.OrTools.Sat;

    var model = new CpModel();
                
            

Define decision variables

Examples:
                
    var intVariable = model.NewIntVar(0, 10, "intVar");
    var boolVariable = model.NewBoolVar("boolVar");
                
            
Sudoku:
                
    var cells = new IntVar[9, 9];

    for (int i = 0; i < 9; i++) {
        for (int j = 0; j < 9; j++) {
            cells[i, j] = model.NewIntVar(1, 9, $"cell_{i}_{j}");
        }
    }
                
            

Define model constraints

Examples:
                
    model.Add(x + y <= 15).OnlyEnforceIf(boolVariable);
                
            
Sudoku:
                
    for (int i = 0; i < 9; i++) {
        var row = new IntVar[9];
        for (int j = 0; j < 9; j++) {
            row[j] = cells[i, j];
        }

        model.AddAllDifferent(row);
    }
                
            

Running the solver

                
    var solver = new CpSolver();
    var status = solver.Solve(model);

    if (status == CpSolverStatus.Optimal || status == CpSolverStatus.Feasible)
    {
        Console.WriteLine(@$"Solution found: x = {solver.Value(x)},
        y = {solver.Value(y)},
        condition = {solver.Value(condition)}");
    }
    else
    {
        Console.WriteLine("No solution found.");
    }
                
            

Insights from NODES

Final words

Model optimization

  • Reification:
    • Link a boolean variable to the truth of a constraint.
    • Example: quantityOrderA > 0 => matchedOrderA = 1
  • Global constraints:
    • Using built-in methods such as AllDifferent helps the solver deduce better.
  • Breaking symmetry:
    • Identify and eliminate equivalent solutions
    • Example: For identical sell orders, enforce matching in a specific order

Beyond constraint programming

Solvers can be used for more than just constraint programming:

  • Mixed Integer Programming (MIP)
  • Linear Programming (LP)
  • and other kinds of problems

How to use solvers?

There are many libraries, open-source and closed-source, for most programming languages.

In Java there is even a standard: JSR 331: Constraint Programming API.

Thank you!