Initializing help system before first use

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.
File(s): coco-mps-xe.js
Data file(s): coco.mps.gz, package.json


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);
    }
});

© 2001-2024 Fair Isaac Corporation. All rights reserved. This documentation is the property of Fair Isaac Corporation (“FICO”). Receipt or possession of this documentation does not convey rights to disclose, reproduce, make derivative works, use, or allow others to use it except solely for internal evaluation purposes to determine whether to purchase a license to the software described in this documentation, or as otherwise set forth in a written software license agreement between you and FICO (or a FICO affiliate). Use of this documentation and the software described in it must conform strictly to the foregoing permitted uses, and no other use is permitted.