FICO
FICO Xpress Optimization Examples Repository
FICO Optimization Community FICO Xpress Optimization Home
Back to examples browserPrevious exampleNext example

Solving an MPS problem using a REST webservice, from NodeJS

Description
In this example, the Xpress Executor should first be configured to solve MPS problems. Then, you run the coco-mps-xe.js program locally; this uses REST webservice requests to send the coco.mps.gz file to the Xpress Executor and remotely solve the problem, then download and display the results. This example does not require a local installation of Xpress.

executor_rest_mps_nodejs.zip[download all files]

Source Files

Data Files





coco-mps-xe.js

/*******************************************************
  Xpress Executor Example Model
  =============================

  file coco-mps-xe.js
  ```````````````````
  Demonstrates executing the 'coco.mps' model using Xpress Executor
  and displays the results.  This example is written in JavaScript and
  intended to be run using node.js.

  Local prerequisites:
    node.js
    npm

  Instructions
    1) Configure your Executor component to solve MPS/LP files
    2) Fill in the DMP_* variables with the details of your Executor component
    3) Open a command prompt and type:   npm install
    4) When that completes, type:        node coco-mps-xe.js

  (c) 2017 Fair Isaac Corporation
  author: J. Farmer, May. 2017
*******************************************************/

// The REST endpoint of the Xpress Executor DMP component
// ! Obtain this by clicking "View Links" for the Xpress Executor component on the DMP UI
var DMP_XE_REST_ENDPOINT="https://e2hbu1a7ma-e2hbu1a7ma.us-west.dmsuitecloud.com/rest/runtime/execution?solutionID=e2gzewa9sc";

// The client ID of solution containing the Xpress Executor DMP component
// Obtain this through the DMP UI
var DMP_SOLUTION_CLIENT_ID="e2gzewa9sc";

// The secret of the solution containing the Xpress Executor DMP component
// Obtain this through the DMP UI
var DMP_SOLUTION_SECRET="4U3uD128Jj8x4cQm*zCjPZQwdqWPj9OEk57V";

// The root DMP manager URL. This will be different depending on which instance of DMP you are using.
var DMP_MANAGER_URL="https://manager-svc.us-west.dmsuitecloud.com";

// The input file for the remote model
var MODELFILE="../model/coco.mps.gz";

// The file to which to write the solution
var SOLUTIONFILE="solution.slx";

// Any additional parameters to set
var MODELPARAMS={
    PROBLEM_FORMAT: "MPS.GZ",  // MPS file is gzipped
    SOLUTION_FORMAT: "SLX.GZ",  // Download solution file in gzipped format
    XPRS_MAXTIME: -1800        // Expect solver to take no longer than 30 minutes
}


// Third-party dependency: request
// for aysynchronous HTTP requests
var request = require('request');

// Third-party dependency: prequest
// for promise-based HTTP requests
var prequest = require('prequest');

// Third-party dependency: delay
// for promise-based delays
var delay = require('delay');

// Third-party dependency: CombinedStream
// for concatenating multiple streams
var CombinedStream = require('combined-stream2');

// Third-party dependency: StringToStream
// for creating stream from string
var StringToStream = require('string-to-stream');

// Standard node.js filesystem module
var fs = require('fs');
// Standard node.js URL handling module
var url = require('url');
// Standard node.js compression handling module
var zlib = require('zlib');
// Standard node.js stream module
var stream = require('stream');


// Map from status codes to meaningful names returned by Xpress Executor
var solverStatusCodes = {
    0: "NOT_STARTED",
    1: "LOCALLY OPTIMAL",
    2: "OPTIMAL",
    3: "LOCALLY INFEASIBLE",
    4: "INFEASIBLE",
    5: "UNBOUNDED",
    6: "UNFINISHED"
};


// Create a basic 'WallTimer' class - this measures time elapsed, in seconds, between calls to start() and stop()
function WallTimer() {
    this.startTime = null;
    this.endTime = null;
    this.elapsedSeconds = null;
}
WallTimer.prototype.start = function() {
    if (this.startTime!=null) throw new Error("timer already started");
    this.startTime = process.hrtime();
};
WallTimer.prototype.stop = function() {
    if (this.startTime==null) throw new Error("timer not started");
    if (this.endTime!=null) throw new Error("timer already stopped");
    this.endTime = process.hrtime();
    this.elapsedSeconds =  (this.endTime[0]-this.startTime[0])+
        ( (Math.round(this.endTime[1]/1e6)/1e3) - (Math.round(this.endTime[1]/1e6)/1e3) );
}

// Request an authentication token from DMP
console.log("Requesting authorization token from DMP");
var authorizationToken;
var timers = {};
    timers.authRequest = new WallTimer();
    timers.authRequest.start();
prequest({
        method: 'POST',
        url: DMP_MANAGER_URL+"/registration/rest/client/token",
        body: {
            clientId: DMP_SOLUTION_CLIENT_ID,
            secret: DMP_SOLUTION_SECRET
        }
}).then(function(body) {
    // This token can be re-used in subsequent requests, but should be refreshed every half hour
    authorizationToken = body;
    timers.authRequest.stop();
    console.log("Obtained authorization token after "+timers.authRequest.elapsedSeconds+"s");

     // Start the execution of the model in our Xpress Executor service
    console.log("Requesting execution");
    timers.execRequest = new WallTimer();
    timers.execRequest.start();
    // Note: because the input body might be large, we stream the request rather than using prequest, and also use
    // the multipart-form upload API rather than the REST API for this request.  If we know
    // the MPS file is small enough to be base64-encoded into a JavaScript string, the below could be replaced with:
    // return prequest({
    //     method: 'POST',
    //     url: DMP_XE_REST_ENDPOINT,
    //     headers: {
    //         "Authorization": 'Bearer '+authorizationToken
    //     },
    //     body: {
    //         parameters: MODELPARAMS,
    //         inputBase64: new Buffer(fs.readFileSync(MODELFILE)).toString('base64')
    //     }
    // });

    return new Promise(function(resolvePromise,rejectPromise) {
        var boundary = "----OptimizationExecutorNodeJSFormMessageBoundary";
        requestOptions = {
            method: 'POST',
            url: DMP_XE_REST_ENDPOINT,
            headers: {
                "Authorization": 'Bearer '+authorizationToken,
                "Content-Type": "multipart/form-data; boundary="+boundary,
                "Accepts": "text/html"
            }
        };
        // Pipe from execRequestStream to the request
        var execRequestStream = CombinedStream.create();
        // Construct the request body from several concatenated streams
        // The body is in the standard multipart/form-data format, as described in RFC 1867.
        for (var paramName in MODELPARAMS) {
            execRequestStream.append( StringToStream( "--" + boundary + "\n" ) );
            execRequestStream.append( StringToStream( "Content-Disposition: form-data; name=\"param-" + paramName + "\"\n" ) );
            execRequestStream.append( StringToStream( "\n" ) );
            execRequestStream.append( StringToStream( MODELPARAMS[paramName] + "\n" ) );
        }
        execRequestStream.append( StringToStream( "--" + boundary + "\n" ) );
        execRequestStream.append( StringToStream( "Content-Disposition: form-data; name=\"input\"; filename=\"input.mps.gz\"\n" ) );
        execRequestStream.append( StringToStream( "Content-Type: application/octet-stream\n" ) );
        execRequestStream.append( StringToStream( "\n" ) );
        execRequestStream.append( fs.createReadStream(MODELFILE) );
        execRequestStream.append( StringToStream( "\n" ) );
        execRequestStream.append( StringToStream( "--" + boundary + "--\n" ) );

        // Now, pipe our message body to the HTTP request
        execRequestStream.pipe( request.post(requestOptions, function(err, response, body) {
            if (err) {
                rejectPromise(new Error(err));
            }
            else if (!response) {
                rejectPromise(new Error("No response"));
            }
            else if (response.statusCode!=200) {
                rejectPromise(new Error("Unexpected status code: "+response.statusCode+", body: "+body));
            }
            else {
                resolvePromise(JSON.parse(body));
            }
        } ) );
    });
}).then(function(executionResponse) {
    // Note that the multipart-form-upload endpoint returns the execution status 'wrapped' in another structure.
    // So check the status code & extract the status code from this.
    // This would not be necessary if we were calling the REST endpoint for file upload.
    if (executionResponse.status!=200) {
        throw new Error("Execution request failed with status "+executionResponse.status+", entity: "+JSON.stringify(executionResponse.entity));
    }
    if (!executionResponse.entity) {
        throw new Error("Execution request did not return execution status");
    }
    return executionResponse.entity
}).then(function(executionStatus) {
    // executionStatus is a standard structure that contains various meta-data about an execution in
    // the Xpress Executor service.  It also contains relative paths to various REST resources
    // relating to this execution - e.g. input, result, status, run log...

    timers.execRequest.stop();
    console.log("Execution accepted after "+timers.execRequest.elapsedSeconds+"s");

    // Model will be executing asynchronously; repeatedly wait 1/4 second then re-fetch status until it
    // is complete
    console.log("Waiting for completion of execution");
    timers.execInProgress = new WallTimer();
    timers.execInProgress.start();
    function waitForCompletion() {
        if (executionStatus.status!=='NOT_COMPLETED' && executionStatus.status!=='NOT_LOADED') {
            // Execution has finishd!
            return Promise.resolve(executionStatus);
        }
        else {
            // Wait 250ms
            return delay(250).then(function() {
                // Refresh executionInfo
                return prequest({
                    method: 'GET',
                    url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.statusPath),
                    headers: {
                        "Authorization": 'Bearer '+authorizationToken
                    }
                }).then(function(body) {
                    // Request returns updated executionStatus
                    executionStatus = body;
                    return waitForCompletion();
                });
            });
        }
    }
    return waitForCompletion();
}).then(function(executionStatus) {

    timers.execInProgress.stop();
    console.log("Execution accepted after "+timers.execInProgress.elapsedSeconds+"s");

    // Execution has completed; check that it was successful and display results as appropriate
    console.log("Processing model results");

    // In event of failure, echo the remote model status, exit code & run log to aid with troubleshooting
    if (executionStatus.status!=='OK' || solverStatusCodes[executionStatus.exitCode]!=="OPTIMAL") {
        // Execution failed for some reason
        if (executionStatus.status!=='OK') {
            console.log("Execution failed!");
            console.log("Execution status: "+executionStatus.status);
            console.log("Execution exit code: "+executionStatus.exitCode);
        }
        else {
            console.log("Failed to solve to optimality!");
            console.log("Solver status: "+solverStatusCodes[executionStatus.exitCode]);
        }
        console.log("");
        console.log("Execution log:");
        // Fetch the remote execution log as it will likely contain error messages from the model
        return prequest({
            method: 'GET',
            url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.runLogPath),
            headers: {
                "Authorization": 'Bearer '+authorizationToken,
                "Accept": 'text/plain'
            }
        }).then(function(runLog) {
            console.log(runLog);
            return executionStatus;
        });
    }

    else {
        timers.resultsRequest = new WallTimer();
        timers.resultsRequest.start();

        // Download results file
        return prequest({
            method: 'GET',
            url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.resultPath),
            headers: {
                "Authorization": 'Bearer '+authorizationToken,
                "Accept": 'application/octet-stream'
            },
            encoding: null   // will return binary results file as a 'buffer'
        }).then(function(solGzResultsBuffer) {
            timers.resultsRequest.stop();
            console.log("Results downloaded after "+timers.resultsRequest.elapsedSeconds+"s");

            // We have downloaded the .slx file in compressed format
            // Use zlib to decompress
            console.log("Decompressing results to "+SOLUTIONFILE);
            timers.uncompressResults = new WallTimer();
            timers.uncompressResults.start();
            var gunzip = zlib.createGunzip();
            var bufferstream = new stream.PassThrough();
            var slxfile = fs.createWriteStream(SOLUTIONFILE);
            var gunzipStream = bufferstream.pipe(gunzip).pipe(slxfile);
            return new Promise(function(resolve,reject) {
                bufferstream.end(solGzResultsBuffer);
                gunzipStream.on('finish',function() { resolve(executionStatus); });
                gunzipStream.on('error', function(err) { reject(err); });
            });
        });
    }

}).then(function(executionStatus) {
    // Finally, delete execution from component, to free the resources it holds
    console.log("Deleting execution from component");
    timers.deleteRequest = new WallTimer();
    timers.deleteRequest.start();
    return prequest({
        method: 'DELETE',
        url: url.resolve(DMP_XE_REST_ENDPOINT, executionStatus.statusPath),
        headers: {
            "Authorization": 'Bearer '+authorizationToken
        }
    });

}).then(function() {
    timers.deleteRequest.stop();
    console.log("execution deleted after "+timers.deleteRequest.elapsedSeconds+"s");
}).catch(function(err) {
    if (err.statusCode) {
        console.error("ERROR returned by Xpress Executor service: HTTP status code "+err.statusCode);
    }
    else {
        console.error("ERROR encountered: "+err.message);
    }
});
Back to examples browserPrevious exampleNext example