# A Branching rule branching on the most violated Integer/Binary.
#
# This example demonstrates the Xpress change branch callbacks.
#
# (C) 2024-2026 Fair Isaac Corporation

import xpress as xp
import math


# Class for data object to pass to variable selection callback.
class SolData:
    def __init__(self, colind, coltype, tol):
        self.colind = colind
        self.coltype = coltype
        self.tol = tol


# This function will be called every time the Optimizer has selected a candidate entity for branching.
def varselectioncb(prob, soldata, obranch):

    dsol = prob.getCallbackPresolveSolution()   # Get solution in the presolved space.

    branchcol = None    # To store column index of the variable to branch on, if any.

    # Find the most fractional column.
    maxdist = 0
    for i in soldata.colind:
        updist = math.ceil(dsol[i]) - dsol[i]
        downdist = dsol[i] - math.floor(dsol[i])

        dist = min(updist, downdist)

        if dist > soldata.tol and dist > maxdist:
            branchcol = i
            maxdist = dist

    # If we found a column to branch on then create a branching object
    # (in the presolved space) that reflects branching on that column and
    # return it to the caller.
    if branchcol is not None:
        UB = math.floor(dsol[branchcol])
        LB = math.ceil(dsol[branchcol])

        new_branch = xp.branchobj(prob, isoriginal=False)   # Create new branch object to be returned.
        new_branch.addBranches(2)                           # Number of branches to add.
        new_branch.addBounds(0, ["U"], [branchcol], [UB])   # Add upper bound to first branch.
        new_branch.addBounds(1, ["L"], [branchcol], [LB])   # Add lower bound to second branch.

        return new_branch
    else:
        return None


# Create a new problem.
p = xp.problem()

modelfile = "Data/burglar.mps"
p.readProb(modelfile)

# Turn off dual presolve reductions for this example,
# otherwise the problem is reduced to an empty problem.
p.controls.mipdualreductions = 0

print(f"Start solving problem {modelfile}.")

# Stop after solving the root relaxation
p.controls.mipstopstage = xp.MIPStopStage.INITIALRELAXATION
p.mipOptimize("")
p.setDefaultControl('mipstopstage')

p.controls.miplog = 3

# Change some controls to make sure we actually need to branch to solve the problem.
p.controls.heuremphasis = 0
p.controls.cutstrategy = 0

# Get solution data (presolved problem) to pass to callback.
coltype, colind, limit, settype, start, setcols, refval = p.getMIPEntities()
mtol = p.controls.miptol

sol = SolData(colind, coltype, mtol)

p.addChangeBranchObjectCallback(varselectioncb, sol)

p.mipOptimize()

print("Solving complete.")
