Branching Rules#

For the following let us assume that a Model object is available, which is created as follows:

from pyscipopt import Model, Branchrule, SCIP_RESULT

scip = Model()

What is Branching#

Branching is when an optimization problem is split into smaller subproblems. Traditionally this is done on an integer variable with a fractional LP solution, with two child nodes being created with constraints x >= ceil(frac) and x <= floor(frac). In SCIP, arbitrary amount of children nodes can be created, and the constraints added the created nodes can also be arbitrary. This is not going to be used in the examples below, but this should be kept in mind when considering your application of your branching rule.

Example Branching Rule#

Here we will program a most infeasible branching rule. This rule selects the integer variable whose LP solution is most fractional.

import numpy as np

class MostInfBranchRule(Branchrule):

    def __init__(self, scip):
        self.scip = scip

    def branchexeclp(self, allowaddcons):

        # Get the branching candidates. Only consider the number of priority candidates (they are sorted to be first)
        # The implicit integer candidates in general shouldn't be branched on. Unless specified by the user
        # npriocands and ncands are the same (npriocands are variables that have been designated as priorities)
        branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands()

        # Find the variable that is most fractional
        best_cand_idx = 0
        best_dist = np.inf
        for i in range(npriocands):
            if abs(branch_cand_fracs[i] - 0.5) <= best_dist:
                best_dist = abs(branch_cand_fracs[i] - 0.5)
                best_cand_idx = i

        # Branch on the variable with the largest score
        down_child, eq_child, up_child = self.model.branchVarVal(
            branch_cands[best_cand_idx], branch_cand_sols[best_cand_idx])

        return {"result": SCIP_RESULT.BRANCHED}

Let’s talk about some features of this branching rule. Currently, we only explicitly programmed a single function, which is called branchexeclp. This is the function that gets called when branching on an LP optimal solution. While this is the main case, it is not the only case that SCIP handles. What if there was an LP error at the node, or you are given a set of external candidates? For more information on this please read this page.

Now let’s discuss what the function returned. We see that it returned a simple dictionary, with the "result" key and a SCIP_RESULT. This is because inside the function the child nodes have already been created, and the solver just needs to be made aware of this with the appropriate code.

In the case of something going wrong while branching (and you have not made the children nodes), just simply return SCIP_RESULT:DIDNOTRUN. This will then move on to the next branching rule with the next highest priority.

Note

Returning SCIP_RESULT:DIDNOTRUN for more complicated components of the branching rule line in branchexecps is completely encouraged. It is even strongly suggested if you are doing an LP specific branching rule.

Now we will finally see how to include the branching rule.

scip = Model()

most_inf_branch_rule = MostInfBranchRule(scip)
scip.includeBranchrule(most_inf_branch_rule, "mostinf", "custom most infeasible branching rule",
                       priority=10000000, maxdepth=-1, maxbounddist=1)

This function includeBranchrule takes the branching rule as an argument, the name (which will be visible in the statistics), a description, the priority (for your rule to be called by default it must be higher than the current highest, which can be quite large), the maxdepth of the branch and bound tree for which the rule still works (-1 for unlimited), and the maxbounddist (We recommend using 1 and to see SCIP documentation for an explanation).

Strong Branching Information#

Now let’s look at a more complicated example, namely one where we implement our own strong branching rule. The aim of this example is to provide a basic understanding of what functions are necessary to use strong branching or obtain some strong branching information.

Note

This example is not equivalent to the strong branching rule in SCIP. It ignores some of the more complicated interactions in a MIP solver for information found during strong branching. These include but are not strictly limited to:

  • What happens if a new primal solution is found and the bound is larger than the cutoff bound?

  • What happens if the bound for one of the children is above a cutoff bound?

  • If probing is enabled then one would need to handle new-found bounds appropriately.

class StrongBranchingRule(Branchrule):

    def __init__(self, scip):
        self.scip = scip

    def branchexeclp(self, allowaddcons):

        branch_cands, branch_cand_sols, branch_cand_fracs, ncands, npriocands, nimplcands = self.scip.getLPBranchCands()

        # Initialise scores for each variable
        scores = [-self.scip.infinity() for _ in range(npriocands)]
        down_bounds = [None for _ in range(npriocands)]
        up_bounds = [None for _ in range(npriocands)]

        # Initialise placeholder values
        num_nodes = self.scip.getNNodes()
        lpobjval = self.scip.getLPObjVal()
        lperror = False
        best_cand_idx = 0

        # Start strong branching and iterate over the branching candidates
        self.scip.startStrongbranch()
        for i in range(npriocands):

            # Check the case that the variable has already been strong branched on at this node.
            # This case occurs when events happen in the node that should be handled immediately.
            # When processing the node again (because the event did not remove it), there's no need to duplicate work.
            if self.scip.getVarStrongbranchNode(branch_cands[i]) == num_nodes:
                down, up, downvalid, upvalid, _, lastlpobjval = self.scip.getVarStrongbranchLast(branch_cands[i])
                if downvalid:
                    down_bounds[i] = down
                if upvalid:
                    up_bounds[i] = up
                downgain = max([down - lastlpobjval, 0])
                upgain = max([up - lastlpobjval, 0])
                scores[i] = self.scip.getBranchScoreMultiple(branch_cands[i], [downgain, upgain])
                continue

            # Strong branch!
            down, up, downvalid, upvalid, downinf, upinf, downconflict, upconflict, lperror = self.scip.getVarStrongbranch(
                branch_cands[i], 200, idempotent=False)

            # In the case of an LP error handle appropriately (for this example we just break the loop)
            if lperror:
                break

            # In the case of both infeasible sub-problems cutoff the node
            if downinf and upinf:
                return {"result": SCIP_RESULT.CUTOFF}

            # Calculate the gains for each up and down node that strong branching explored
            if not downinf and downvalid:
                down_bounds[i] = down
                downgain = max([down - lpobjval, 0])
            else:
                downgain = 0
            if not upinf and upvalid:
                up_bounds[i] = up
                upgain = max([up - lpobjval, 0])
            else:
                upgain = 0

            # Update the pseudo-costs
            lpsol = branch_cands[i].getLPSol()
            if not downinf and downvalid:
                self.scip.updateVarPseudocost(branch_cands[i], -self.scip.frac(lpsol), downgain, 1)
            if not upinf and upvalid:
                self.scip.updateVarPseudocost(branch_cands[i], 1 - self.scip.frac(lpsol), upgain, 1)

            scores[i] = self.scip.getBranchScoreMultiple(branch_cands[i], [downgain, upgain])
            if scores[i] > scores[best_cand_idx]:
                best_cand_idx = i

        # End strong branching
        self.scip.endStrongbranch()

        # In the case of an LP error
        if lperror:
            return {"result": SCIP_RESULT.DIDNOTRUN}

        # Branch on the variable with the largest score
        down_child, eq_child, up_child = self.model.branchVarVal(
            branch_cands[best_cand_idx], branch_cands[best_cand_idx].getLPSol())

        # Update the bounds of the down node and up node. Some cols might not exist due to pricing
        if self.scip.allColsInLP():
            if down_child is not None and down_bounds[best_cand_idx] is not None:
                self.scip.updateNodeLowerbound(down_child, down_bounds[best_cand_idx])
            if up_child is not None and up_bounds[best_cand_idx] is not None:
                self.scip.updateNodeLowerbound(up_child, up_bounds[best_cand_idx])

        return {"result": SCIP_RESULT.BRANCHED}

Note

In SCIP we must call startStrongbranch before doing any actual strong branching (which is done with the call getVarStrongbranch). When we’re done with strong branching we must then also call endStrongbranch.