/***********************************************************************
   Xpress Optimizer Examples
   =========================

   file AddMipSol.java
   ````````````
   Apply an LP rounding heuristic at each node and inject feasible
   solutions found.

   (c) 2020-2024 Fair Isaac Corporation
***********************************************************************/

import com.dashoptimization.AbstractOptNodeListener;
import com.dashoptimization.DefaultMessageListener;
import com.dashoptimization.XPRS;
import com.dashoptimization.XPRSoptNodeListener;
import com.dashoptimization.XPRSprob;
import static com.dashoptimization.XPRSenumerations.CutStrategy;

public class AddMipSol {

    /** Callback that runs a simple rounding heuristic at each node.
     *
     * The callback fixes all integer fractional variables to their rounded
     * values and solves the resulting problem. If that leads to a feasible
     * solution then that solution is injected back into the solver as a
     * new incumbent.
     */
    private static final class OptNode extends AbstractOptNodeListener {

        /** Problem name. */
        final String probName;
        /** Number of columns. */
        final int nCol;
        /** Number of MIP entities. */
        final int nMipEnt;
        /** Information about MIP entities. */
        final XPRSprob.MIPEntityInfo mipEntInfo;

        public OptNode(XPRSprob prob) {
            // Capture some global statistics that will be needed in the
            // heuristic.
            probName = prob.getProbName();
            nCol = prob.attributes().getCols();
            nMipEnt = prob.attributes().getMIPEnts();
            mipEntInfo = prob.getDiscreteCols();
        }

        /** Callback main method.
         * This is the method that is invoked by the solver.
         */
        public int optNodeEvent(XPRSprob prob, int feas) {
            // Only run once per node (since adding a solution will call
            // the callback to be fired again)
            if (prob.attributes().getCallbackCount_OptNode() > 1)
                return feas;

            // Only run the search for solutions with small numbers of
            // integer infeasibilities */
            if (prob.attributes().getMIPInfeas() > 0.25 * nMipEnt)
                return feas;

            fixedSearch(prob);

            return feas;
        }

        /** Run the heuristic that fixes all fractional integer variables
         * to their rounded values.
         */
        private void fixedSearch(XPRSprob parent) {
            // Create resources we need
            int[] bndInd = new int[nCol];
            double[] bndVal = new double[nCol];
            byte[] bndType = new byte[nCol];
            Thread me = Thread.currentThread(); // to get unique names

            // Create a problem containing the original problem
            try (XPRSprob prob = new XPRSprob(null)) {
                prob.copyProb(parent);
                prob.postSolve();

                // Reset the controls of the problem.
                prob.setDefaults();

                // Rename the problem so we don't get collisions with
                // temporary files produced by the optimizer
                prob.setProbname(probName + "_" + me);

                // Output all messages.
                prob.addMessageListener(DefaultMessageListener::console);

                // Get information from the solution at the current node
                // of 'parent' problem */
                boolean hasMipIncumbent = parent.attributes().getMIPSols() != 0;
                double incumbentObj = parent.attributes().getMIPObjVal();
                double[] x = parent.getCallbackSolution();

                // Initialise bound counter
                int nBnd = 0;
                double mipTol = parent.controls().getMIPTol();

                // Go through MIP entities
                for (int i = 0; i < nMipEnt; i++) {
                    // Test whether each is a binary or integer variable
                    if (mipEntInfo.coltype[i] == 'B' || mipEntInfo.coltype[i] == 'I') {
                        int j = mipEntInfo.colind[i];

                        // Fix variables that are fractional
                        double rounded = Math.round(x[j]);
                        if (Math.abs(rounded - x[j]) <= mipTol) {
                            continue;
                        }
                        bndInd[nBnd] = j;
                        bndVal[nBnd] = rounded;
                        bndType[nBnd] = 'B';
                        nBnd++;
                    }
                }

                // Instruct Optimizer to change the bounds
                prob.chgBounds(nBnd, bndInd, bndType, bndVal);
                System.out.println(me + ": After MIP search, " + nBnd +
                                   " binary and integer variables were fixed");

                // Ensure we only do a small search in the fixed problem and
                // use the current incumbent objective from the main problem
                prob.controls().setMaxNode(100);
                prob.controls().setMaxMIPSol(1);
                if (hasMipIncumbent) {
                    // Set a cut-off to help the fixed search
                    prob.controls().setMIPAbsCutoff(incumbentObj);
                }
                prob.controls().setMIPLog(3);

                // Read the basis for the LP relaxation to reduce run time
                prob.readBasis(probName, "");

                // Run the optimizer
                prob.mipOptimize();

                // If we found a solution then load it into the main problem
                if (prob.attributes().getMIPSols() > 0) {
                    x = prob.getSolution();
                    parent.addMipSol(nCol, x, null, null);
                }
            }
        }
    }

    public static void main(String[] args) {
        // Get problem name from command line or use a default problem.
        String model = args.length == 0 ? "../data/addmipsol" : args[0];

        try (XPRSprob prob = new XPRSprob(null)) {
            prob.readProb(model, "");
            prob.addMessageListener(DefaultMessageListener::console);
            // Register the callback that will perform heuristic search.
            prob.addOptNodeListener(new OptNode(prob));

            // Make the problem difficult for the optimizer to solve.
            prob.controls().setCutStrategy(CutStrategy.NONE);
            prob.controls().setHeurEmphasis(0);
            prob.controls().setSBBest(0);
            prob.controls().setMIPThreads(20);

            // Log every node in the branch-and-bound search.
            prob.controls().setMIPLog(3);

            // Solve the root node relaxation and write out the basis
            // (to reduce solve time of the fixed problems) */
            prob.mipOptimize("l");
            prob.writeBasis("", "");

            // Run the MIP search
            prob.mipOptimize("");

            System.out.printf("Optimal objective value: %f%n",
                              prob.attributes().getMIPObjVal());

        }
    }
}
