| |||||||||||
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.
Source Files By clicking on a file name, a preview is opened at the bottom of this page.
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); } }); | |||||||||||
© Copyright 2024 Fair Isaac Corporation. |