/*********************************************************************** Xpress Optimizer Examples ========================= file MostViolated.java ``````````````````` A Branching rule branching on the most violated Integer/Binary Demonstrate the Xpress-MP change branch callbacks. (c) 2020-2024 Fair Isaac Corporation ***********************************************************************/ import com.dashoptimization.AbstractChangeBranchObjectListener; import com.dashoptimization.DefaultMessageListener; import com.dashoptimization.XPRS; import com.dashoptimization.XPRSbranchobject; import com.dashoptimization.XPRSprob; import static com.dashoptimization.XPRSenumerations.CutStrategy; import java.io.IOException; /** Example for a branch callback that always branches on the most infeasible * variable. * *

* Here "most infeasible" means the variable with the biggest fractionality, * i.e., the integer variable that is farthest away from an integral value. *

* *

* In order to find the variable with the biggest fractionality, we need the * variable types (so that we don't end up branching on a fractional variable) * and the values of the variables in the current relaxation. This data is * typically queried into arrays cType and x at each * node at which we have to find a branching variable. * Allocation of these arrays may take time, so it is desirable to cache those * arrays so that we don't have to (re)allocate them at each and every node. * However, when caching these arrays a number of things have to be considered, * most of which are implemented in this example: *

*
Multi-Threading
*
* In a multi-threaded solve, callbacks may execute in parallel. This means * that each thread requires its own x array since otherwise * multiple threads might read into the same array from different nodes. * The cType array can be shared between threads since this is * a read-only array. * By default Xpress optimizer serializes the execution of callbacks * using a mutex. That is, by default callbacks will not be executed * simultaneously. In that case the threads can share the same * x array. You can tell the XPress optimizer to not * serialize callbacks by setting the MUTEXCALLBACKS control * to 0. In that case, the threads cannot share the same x * array and each thread has to allocate its own array. *
*
Presolved or original space
*
* We can choose whether we want to make branching decisions in the * original problem space or in the presolved problem space. Branching * in the original space has the advantage that variables have their * original meaning and that all variables are still present. Given that * internally the optimizer works with the presolved problem, a disadvantage * is that relaxation values and branching decisions must be translated * back and forth between the presolved and original space. * Branching in the presolved space allows working directly with the same * data as the optimizer does internally. This is usually faster but fewer * assumptions can be made about the presolved model (variables may have * a different meaning, may have disappeared, etc.). * When setting up the cType an x arrays one * has to be careful that they are setup for the right model. *
*
Restarts
*
* For performance reasons the optimizer may decide to discard the current * search tree and restart optimization from scratch, exploting what it * has learned about the problem so far. In this case, the presolved model * may change (for example, it is common that more variables can be fixed * and thus the size of the presolved model reduces). This means that in * case of branching on the presolved model the arrays cType * and x have to be resetup at a restart. *
*
Constraints by branching
*
* It is possible to not put all constraints into the initial model * description but to enforce certain constraints by means of branching * decisions. An example for this is the handling of symmetries. In that * case the optimizer does not see all constraints in the presolve phase. * However, by default the optimizer assumes that the submitted problem * is complete. If it is not then you may have to disable certain * reductions (in particular dual reductions) since otherwise you may * end up with incorrect results. This special case is not illustrated * in this example since the "branch on most fractional" strategy does not * interfere with any presolve reductions. *
*
* In this example we illustrate how to implement the callback in the different * contexts mentioned above. For each combination of contexts we have a * class that creates and caches the cType and x * arrays in a way that is safe in the given context. *

* *

* Note that in this example we always go through the whole list of variables * and check for each variable whether it is integer or not. This is for * illustrative purposes and to keep things simple. In a production code you * may want to consider using {@link XPRSprob#getMIPEntities()} or * {@link XPRSprob#getDiscreteCols()} to get a list of integer variables. *

*/ public class MostViolated { /** Branch callback that branches on the most fractional variable. * * The actual branching happens in function * {@link #changeBranchObjectEvent(XPRSprob, XPRSbranchobject)} * This function uses the two abstract functions {@link #getCType(XPRSprob)} * and {@link #getX(XPRSprob)} to get the arrays of variable types and * current relaxation values. * We provide different subclasses of BranchBase that have * different implementations of these functions, depending on the context * in which the class can be used. */ private abstract static class BranchBase extends AbstractChangeBranchObjectListener { /** Tolerance used for checking fractionality. */ private final double tolerance; /** Flag to indicate whether branches are created in terms of * the original model. * If they are not created in terms of the original model, they are * created in terms of the presolved model. */ private final boolean isOriginal; /** Create a new branching callback for prob. * @param prob The problem on which this callback will be invoked. * @param isOriginal Flag that indicates whether the data returned * by {@link #getX(XPRSprob)} and * {@link #getCType(XPRSprob)} is in terms of the * original problem. */ protected BranchBase(XPRSprob prob, boolean isOriginal) { tolerance = prob.controls().getMatrixTol(); this.isOriginal = isOriginal; } /** Get the relaxation at the current node. * @param prob The problem on which the callback is invoked. * @return the relaxation vector for the current node. */ protected abstract double[] getX(XPRSprob prob); /** Get the variable types at the current node. * @param prob The problem on which the callback is invoked. * @return the variable type vector at the current node. */ protected abstract byte[] getCType(XPRSprob prob); /** Get the number of MIP threads available for use when solving a problem * @param prob The problem to query. * @return the number of MIP threads that may be used when solving the problem. */ protected int getNumThreads(XPRSprob prob) { int threads = prob.controls().getMIPThreads(); if (threads == -1) // MIP threads set to -1 means use the Threads control threads = prob.controls().getThreads(); if (threads == -1) // If still at the default then optimizer will use as many // threads as there are cores threads = prob.attributes().getCoresDetected(); return threads; } /** Branch on the most fractional integer. * This is the function that gets invoked by the optimizer whenever * a branching decision has to be made. * @param prob The problem on which the callback is invoked. * @param oBranch The branch that the optimizer plans to perform. * @return the branch we want the optimizer to perform (may be * oBranch or null in which case the * optimizer will perform oBranch). */ public XPRSbranchobject changeBranchObjectEvent(XPRSprob prob, XPRSbranchobject oBranch) { // Get the current relaxation byte[] cType = getCType(prob); double[] x = getX(prob); // This is just a safeguard. The two arrays should always have the // same length. if (x.length != cType.length) throw new IllegalArgumentException("Mismatch: " + cType.length + " vs. " + x.length); if (oBranch == null) return null; // Default branch is the original branch. XPRSbranchobject newBranch = oBranch; // Find the most fractional variable. double maxDist = 0.0; int branchCol = -1; double branchUp = 0; double branchDown = 0; for (int j = 0; j < x.length; ++j) { if (cType[j] == 'I' || cType[j] == 'B') { double upDist = Math.ceil(x[j]) - x[j]; double downDist = x[j] - Math.floor(x[j]); double dist = Math.min(upDist, downDist); if (downDist > tolerance && upDist > tolerance) { if (dist > maxDist) { branchCol = j; branchUp = Math.ceil(x[j]); branchDown = Math.floor(x[j]); maxDist = dist; } } } } // If we found a branching candidate then create the new branch // object for branching on that candidate. if (branchCol != -1) { // Create a branch object for the presolved space. // Wrap registering of the branch into try-catch so that // we properly delete the object in case of any error. // If there is no error then the optimizer will take care // of cleaning up the branch object. XPRSbranchobject bo = prob.createBranchObject(isOriginal); try { // Create the two branches. bo.addBranches(2); bo.addBounds(0, 1, new byte[]{'L'}, new int[]{branchCol}, new double[]{branchUp}); bo.addBounds(1, 1, new byte[]{'U'}, new int[]{branchCol}, new double[]{branchDown}); // Prefer the up branch. bo.setPreferredBranch(0); // Set a small priority so that the branch is actually used. bo.setPriority(100); // Submit the new branch to the solver and clear the // bo reference since the object is now owned (and cleaned // up) by the optimizer. newBranch = bo; bo = null; } finally { if (bo != null) bo.close(); } } return newBranch; } } /** Branch callback implementation that branches in the original space. * Instances of this class cache the cType array since the * types of variables never change in the original model. They also cache * the x array since the size of the original model never * changes. The x array is shared between threads, so the * the class cannot be used for parallel execution of callbacks. */ private static final class BranchOriginal extends BranchBase { private final byte[] cType; private final double[] x; /** Create a new instance. * @param prob The problem that this callback will be used for. * @param origCType Column types in the original model. */ public BranchOriginal(XPRSprob prob, byte[] origCType) { super(prob, true); if (prob.controls().getMutexCallBacks() == 0) throw new IllegalArgumentException("this implementation cannot be used for parallel execution"); cType = origCType; x = new double[cType.length]; } /** Get the current relaxation in terms of the original model. */ @Override protected double[] getX(XPRSprob prob) { prob.getLpSol(x, null, null, null); return x; } /** Get column types in terms of the original model. */ @Override protected byte[] getCType(XPRSprob prob) { return cType; } } /** Branch callback implementation that branches in the original space. * Instances of this class cache the cType array since the * types of variables never change in the original model. They also cache * the x array since the size of the original model never * changes. Each thread has its own x array, so the class * can be used when callbacks are allowed to run in parallel. */ private static final class BranchOriginalMT extends BranchBase { private final byte[] cType; private final double[][] threadX; /** Create a new instance. * @param prob The problem that this callback will be used for. * @param origCType Column types in the original model. */ public BranchOriginalMT(XPRSprob prob, byte[] origCType) { super(prob, true); cType = origCType; int threads = getNumThreads(prob); threadX = new double[threads][]; } /** Get an x vector buffer for the invoking thread. */ private synchronized double[] getThreadX(XPRSprob prob) { // Get the thread id from the optimizer int myId = prob.attributes().getMIPThreadID(); // Create (and cache) an x vector buffer if (threadX[myId] == null) threadX[myId] = new double[cType.length]; // Return the buffer for this thread return threadX[myId]; } /** Get the current relaxation in terms of the original model. */ @Override protected double[] getX(XPRSprob prob) { double[] x = getThreadX(prob); prob.getLpSol(x, null, null, null); return x; } /** Get column types in terms of the original model. */ @Override protected byte[] getCType(XPRSprob prob) { return cType; } } /** Branch callback implementation that branches in the presolved space. * Instances of this class assume that there will be no restarts and thus * cache the cType array. They also cache * the x array since without restarts the size of the presolved * model does not change. The x vector is shared between * threads, so this class cannot be used if callbacks are invoked in * parallel. */ private static final class BranchPresolved extends BranchBase { private final byte[] cType; private final double[] x; /** Create a new callback. * @param prob Problem for which the callback will be used. The active * problem referenced by this must be the presolved problem. */ public BranchPresolved(XPRSprob prob) { super(prob, false); if (prob.controls().getMipRestart() != 0) throw new IllegalArgumentException("this class is not compatible with restarts"); if (prob.controls().getMutexCallBacks() == 0) throw new IllegalArgumentException("this implementation cannot be used for parallel execution"); int cols = prob.attributes().getCols(); cType = new byte[cols]; prob.getColType(cType, 0, cols - 1); x = new double[cols]; } /** Get the current relaxation in terms of the presolved model. */ @Override protected double[] getX(XPRSprob prob) { prob.getPresolveSol(x, null, null, null); return x; } /** Get the current relaxation in terms of the presolved model. */ @Override protected byte[] getCType(XPRSprob prob) { return cType; } } /** Branch callback implementation that branches in the presolved space. * Instances of this class assume that there will be no restarts and thus * cache the cType array. They also cache * the x array since without restarts the size of the presolved * model does not change. The class uses a different x array * for each thread and is thus safe to use for parallel callback execution. */ private static final class BranchPresolvedMT extends BranchBase { private final byte[] cType; private final double[][] threadX; /** Create a new callback. * @param prob Problem for which the callback will be used. The active * problem referenced by this must be the presolved problem. */ public BranchPresolvedMT(XPRSprob prob) { super(prob, false); if (prob.controls().getMipRestart() != 0) throw new IllegalArgumentException("this class is not compatible with restarts"); int cols = prob.attributes().getCols(); cType = new byte[cols]; prob.getColType(cType, 0, cols - 1); int threads = getNumThreads(prob); threadX = new double[threads][]; } /** Get an x vector buffer for the invoking thread. */ private synchronized double[] getThreadX(XPRSprob prob) { // Get the thread id from the optimizer int myId = prob.attributes().getMIPThreadID(); // Create (and cache) an x vector buffer if (threadX[myId] == null) threadX[myId] = new double[cType.length]; // Return the buffer for this thread return threadX[myId]; } /** Get the current relaxation in terms of the presolved model. */ @Override protected double[] getX(XPRSprob prob) { double[] x = getThreadX(prob); prob.getPresolveSol(x, null, null, null); return x; } /** Get the current relaxation in terms of the presolved model. */ @Override protected byte[] getCType(XPRSprob prob) { return cType; } } /** Callback implementation that does not cache any arrays. * The class recreates the cType and x arrays * at every node and is thus safe to use in any context. */ private static final class BranchPresolvedUncached extends BranchBase { /** Create a new callback. * @param prob Problem for which the callback will be used. */ public BranchPresolvedUncached(XPRSprob prob) { super(prob, false); } /** Get the current relaxation in terms of the presolved model. */ @Override protected double[] getX(XPRSprob prob) { int cols = prob.attributes().getCols(); double[] x = new double[cols]; prob.getPresolveSol(x, null, null, null); return x; } /** Get the current relaxation in terms of the presolved model. */ @Override protected byte[] getCType(XPRSprob prob) { int cols = prob.attributes().getCols(); byte[] cType = new byte[cols]; prob.getColType(cType, 0, cols - 1); return cType; } } /** Run this example. * Command line arguments are: *
*
-mt
To invoke callbacks in parallel.
*
-original
Make branching decisions in original space.
*
-restart
Allow restarts.
*
model
Path to the model file to solve.
*
*/ public static void main(String[] args) { // Process the command line. // By default we don't use parallel callbacks, don't branch on the // original model and don't use restarts. Any of this can be changed // by the corresponding command line argument. boolean mt = false; boolean orig = false; boolean restart = false; String model = null; for (String arg : args) { if (arg.equals("-mt")) mt = true; else if (arg.equals("-original")) orig = true; else if (arg.equals("-restart")) restart = true; else model = arg; } if (model == null) { throw new IllegalArgumentException("No model specified on command line"); } try (XPRSprob prob = new XPRSprob(null)) { prob.setLogFile("mvb.log"); prob.readProb(model); // Install default output prob.addMessageListener(DefaultMessageListener::console); // Get the original column types (in case we need them later // for branching in the original space) int cols = prob.attributes().getCols(); byte[] origCType = new byte[cols]; prob.getColType(origCType, 0, cols - 1); // If branching decisions are performed in the original space // then we must be sure they can be translated to the presolved // space, hence we must disable dual reductions in this case. if (orig) prob.controls().setMIPDualReductions(0); // Solve the LP relaxation. // Note that this will change the active model to the presolved // model. So after this call to mipOptimize() all queries for // attributes etc. will refer to the presolved model. System.out.println("Start solving problem " + model); prob.mipOptimize("l"); prob.controls().setMIPLog(3); // Change some controls to make the problem "harder" for the // optimizer. This is to make sure we actually need to branch // to solve the problem. prob.controls().setHeurEmphasis(0); prob.controls().setCutStrategy(CutStrategy.NONE); // Register the branch callback. // Depending on what the user chose, we have to register a // different implementation of the callback. BranchBase callback = null; if (orig) { // User requested branching in the original space. In that // case restarts do not matter since the number and types of // the columns in the original space will never change. So // we ignore the 'restart' flag and only check whether we // want parallel execution of callbacks or not. if (mt) { prob.controls().setMutexCallBacks(0); callback = new BranchOriginalMT(prob, origCType); } else callback = new BranchOriginal(prob, origCType); } else { // We are asked to branch on the presolved model. if (restart) { // Restarts are allowed. We use the "safe" implementation // of the callback that re-allocates arrays at every // invocation. Since it does not cache any arrays, that // implementation is safe for parallel callbacks as well. if (mt) prob.controls().setMutexCallBacks(0); callback = new BranchPresolvedUncached(prob); } else { // No restarts. If there were restarts then the column count // and type could change. Thus we would not be able to cache // that information and would have to re-query it at each // callback invocation. That would slow down things a lot. prob.controls().setMipRestart(0); if (mt) { prob.controls().setMutexCallBacks(0); callback = new BranchPresolvedMT(prob); } else callback = new BranchPresolved(prob); } } prob.addChangeBranchObjectListener(callback); prob.mipOptimize(""); System.out.println("Solving complete.\n"); } // Note: We don't catch XPRSexception or XPRSprobException here. // Instead we rely on the JVM's default handling of an exception // being thrown from main() which should terminate the program // ungracefully without an appropriate message. } }