UNPKG

vrp-tabu-search

Version:

Tabu Search Algorithm for Vehicle Routing Problem

728 lines (566 loc) 23.2 kB
var util = require('util'); var assert = require('assert'); var defineClass = require('simple-cls').defineClass; var uuid = require('node-uuid').v4; var OProblem = require('./OProblem.js'); var OSolution = require('./OSolution.js'); var SLS = require('./SLS.js'); var ALGS_BOX_VERSION = "0.0.1"; var fs = require('fs'); /** An experiment utility manages a single experiment session - an experiment session may contain the following iterations * 1 or more problem instances to test against * 1 or more SLS algorithms to test against each problem * for each algorithm, a number of meta-parameter settings to test - the experiment result is a set of triples (problem, alg, config) * each trip element represent N independent runs of an algorithm under a parameter setting against a problem instance. * each trip associates a result table of the following form step0 step1 ... stepN xx xx xx xx .. N where each cell records the best found solution quality at that local search step interally, the data is a matrix, with the ith row representing data collected at the ith local search step this format conforms to tsv with the first line being the field name Notes: - this module will be the main interface connecting UI - handles file loading, parsing and stuff like that - it will incrementally streaming results to the files - so a watcher can still read the experimental progress Notes: - this class will export all experiment results to a target folder with the following structurs master.tsv - the main index file for this entire experiment session #version xx #problem algorithm config filename tsp tsp_sa [1,"arit"] xxx.tsv . * problem is the problem name being tested * algorithm is the unique algorithm name run against the problem * filename is the corresponding raw data file under the same directory * config is the algorithm's meta-parameter settings for this experiment it is an array of meta-parameter setting values for this experiment xx.tsv - the raw experiment data corresponding to a triple (problem,alg,config). name is randomly generated and indexed from master.tsv **/ var Experiment = defineClass({ name : "Experiment", /** config is an object containing the following synatx { "sat" : { //the SAT problem name "instances" : ["xxx.tsv", "yyy.xml"], //an array of filenames represeting the problem isntance to test agains "algorithms" : { //a list of algorithms to test against. Name must be matching that algorithm's global definition name "sat_iia" : [ //meta-parameter settings for each algorithm. An object with key being the algorithm name {"boltzmanconst":0.05, "coolingscheme":"geometric"}, {"boltzmanconst":0.005, "coolingscheme":"geometric"}, {"boltzmanconst":0.001, "coolingscheme":"geometric"}, ... ] , "sat_ts" : [ {"tabu_tenure" : 10}, {"tabu_tenure" : 50}, {"tabu_tenure" : 100} ... ], "sat_iia" : [] }, "runs" : N //a number specifing how many independent runs on each algorithm against each problem instance / acted as a max bound "max_ls_steps" : M //an upper bound for maximu number of local search steps on algs running against this problem categor }, "tsp" : { //the TSP problem name ... } } outdir - output directory (will create it if not exist) **/ construct : function(config, outdir){ this.config = config; if(outdir) this.outdir = outdir; }, variables : { config : null, //experiment config outdir : "default.box", //default is current folder //master_fd : null, //master file's file descriptor }, methods : { /** Some initialization task - ready the output dirs return true if initialization succeeded **/ init : function(){ if(!fs.existsSync(this.outdir)){ fs.mkdirSync(this.outdir); } else{ var stats = fs.statSync(this.outdir); if(!stats.isDirectory()){ console.log("output dir exists and is not a directory. abort experiment"); return false; } } this.save_config(); return true; }, /** Start executing the experiment **/ run : function(){ var self = this; if(!this.init()) return; //for each problem category for(var problem_name in this.config){ //load the Problem class var problem_config = this.config[problem_name]; var Problem = loadProblemClass(problem_name); //load each problem instance problem_config['instances'].forEach(function(instance_filename){ //loading the file from disk var fullpath = '../workspace/instances/'+instance_filename; assert.ok(fs.existsSync(fullpath, "instance file not exist " + fullpath)); console.log("reading " + fullpath); var content = fs.readFileSync(fullpath, {'encoding' : 'utf8'}); assert.ok(content!=undefined,"error reading file"); //parsing instance content and instantiate the problem instance object! var data = Problem.parseData(content); assert.ok(Problem.validData(data), "data invalid"); var instance = new Problem(data); //total independent runs var independent_runs = parseInt(problem_config.runs); assert(!isNaN(independent_runs) && independent_runs > 0); var max_ls_steps = parseInt(problem_config.max_ls_steps); assert(!isNaN(max_ls_steps) && max_ls_steps > 0); //for each testing algorithm for(var alg_name in problem_config.algorithms){ var algorithm_config = problem_config.algorithms[alg_name]; //load algorithm class var Algorithm = loadAlgorithmClass(problem_name, alg_name); //for each parameter setting, conduct N indepedent runs if(algorithm_config.length > 0){ algorithm_config.forEach(function(alg_config){ //add the local search step constraint alg_config['terminate_ls_steps'] = max_ls_steps; var alg = new Algorithm(instance, alg_config); //before an independent run, add index to master file var exp_filename = self.outdir + "/" + problem_name + "-" + alg_name + "-" + uuid() + ".tsv"; self.add_master(problem_name, instance_filename, alg_name, JSON.stringify(alg.config), exp_filename, independent_runs, max_ls_steps); console.log("problem %s, instance %s, algorithm %s, experiment_file %s", problem_name, instance_filename, alg_name, exp_filename); for(var run = 1 ; run <= independent_runs; run++){ var stats = []; //record best found solution for each step alg.init(); alg.run(function(step){ stats.push(alg.best_sol.fitness); }); //if alg stopped before reaching max, add * to indicate this and fill up the entire array for(var i=stats.length;i<=max_ls_steps;i++) stats.push("*"); //update on experiment file self.add_experiment_file(exp_filename, max_ls_steps, stats); } }); } //using alg's default setting and conduct a N indpedent run else{ var alg = new Algorithm(instance, {'terminate_ls_steps' : max_ls_steps}); //before an independent run, add index to master file var exp_filename = self.outdir + "/" + problem_name + "-" + alg_name + "-" + uuid() + ".tsv"; self.add_master(problem_name, instance_filename, alg_name, JSON.stringify(alg.config), exp_filename, independent_runs, max_ls_steps); console.log("problem %s, instance %s, algorithm %s, experiment_file %s", problem_name, instance_filename, alg_name, exp_filename); for(var run = 1 ; run <= independent_runs; run++){ var stats = []; //record best found solution for each step alg.init(); alg.run(function(step){ stats.push(alg.best_sol.fitness); }); //if alg stopped before reaching max, add * to indicate this and fill up the entire array for(var i=stats.length;i<=max_ls_steps;i++) stats.push("*"); //update on experiment file self.add_experiment_file(exp_filename, max_ls_steps, stats); } } } }) } }, /** add a new entry to the master index file **/ add_master : function(problem, instance, algorithm, config, filename, runs, steps){ var path = this.outdir+"/master.tsv"; var opt = {'encoding' : "utf8"}; if(!fs.existsSync(path)){ fs.appendFileSync(path, "#version "+ALGS_BOX_VERSION+"\n", opt); fs.appendFileSync(path, "#problem instance algorithm config filename runs steps\n", opt); } //adding the entry var entry = problem + "\t" + instance + "\t" + algorithm + "\t" + config + "\t" + filename + "\t" + runs + "\t" + steps; fs.appendFileSync(path, entry+"\n", opt); }, /** FORMAT step0 step1 ... stepN xx xx xx xx .. N steps is total number of local search steps in this single run fitnesses is an arary of fitness value at each local search step [0, MAX] **/ add_experiment_file : function(filepath,steps, fitnesses){ var opt = {'encoding' : "utf8"}; assert.ok(fitnesses.length == steps + 1); if(!fs.existsSync(filepath)){ var header = ""; for(var i=0;i<=steps;i++) header += "step"+i + "\t"; //step0 step1 ... stepN fs.appendFileSync(filepath, header+"\n", opt); } fs.appendFileSync(filepath, fitnesses.join('\t') + "\n", opt); }, /** the configuration for this entire experiment session **/ save_config : function(){ var opt = {'encoding' : "utf8"}; var path = this.outdir + "/config.json"; fs.writeFileSync(path, JSON.stringify(this.config), opt); } }, statics : { } }); /** Analyzer consumes an experiment session data, and renders it to output **/ var Analyzer = defineClass({ name : "Analyzer", construct : function(exp_dir){ this.exp_dir = exp_dir; this._load(); }, variables : { loaded : false, //indicate if loading session is success exp_dir : null, //input directory containing the experiment session config : null, master : null }, methods : { //obtain all tested problem categories in this experiment session problems : function(){ return !this.master ? [] : Object.keys(this.master); }, //obtain all tested problem instnaces in thsi experiment session instances : function(problem_name){ if(!this.master) return []; if(!this.master[problem_name]) return []; return Object.keys(this.master[problem_name]); }, /** return all algorithm tested on a problem or problem instnaces if instance not specified, simply return algoritms tested on any of that problem's instances **/ algorithms : function(problem_name, instance){ if(!this.master) return []; if(!this.master[problem_name]) return []; if(instance){ if(!this.master[problem_name][instance]) return []; return Object.keys(this.master[problem_name][instance]); } else{ var algs = {}; for(var instance_name in this.master[problem_name]){ var alg_names = Object.keys(this.master[problem_name][instance_name]); alg_names.forEach(function(alg_name){ algs[alg_name] = true; }); } return Object.keys(algs); } }, /** return all tested algorithm configuration for a givne problem instance & algorithm each element in the returned array is the master info cell **/ configs : function(problem_name, instance_name, alg_name){ if(!this.master) return []; if(!this.master[problem_name]) return []; if(!this.master[problem_name][instance_name]) return []; if(!this.master[problem_name][instance_name][alg_name]) return []; var configs = []; this.master[problem_name][instance_name][alg_name].forEach(function(exp){ configs.push(exp.config); }); return configs; }, /** return all algorithm performance on all isntances of a problem returned object has follwoing fields { 'instance1' { 'alg1' : alg1's best found solution quality on intance1 . { 'config' : xxx //the config that produces best solution quality 'fitness' : xx //the value of the best found solution quality } 'alg2' : ... } 'instance2' } - note if there are multiple experiemtns alg1 runed on instanc1 under diferent parameter settings. we simply return the best one **/ get_problem_solution_quality : function(problem_name){ var self = this; //find the problem clas var problem = loadProblemClass(problem_name); var instance = new problem(null); if(!this.master) return []; if(!this.master[problem_name]) return []; var stat = {}; for(var instance_name in this.master[problem_name]){ if(!stat[instance_name]) stat[instance_name] = {}; for(var alg_name in this.master[problem_name][instance_name]){ //loop exp for each config this.master[problem_name][instance_name][alg_name].forEach(function(exp){ var exp_data = self._load_experiment(exp.filename, exp.runs, exp.steps); var rqd = Analyzer._runtime_quality_distribution(exp_data); var final_quality = rqd[rqd.length-1]; //simply record the best solution quality across all parameter settings if(stat[instance_name][alg_name] == undefined) stat[instance_name][alg_name] = final_quality; else{ var isBetter = instance.minimization ? (final_quality < stat[instance_name][alg_name]) : (final_quality > stat[instance_name][alg_name]); if(isBetter) stat[instance_name][alg_name] = final_quality; } }); } } return stat; }, /** return an array of runtime solvability data { 'config' : algorith configuration dat 'data' : a vector containing the solvability distribution over local search steps } **/ get_runtime_solvability : function(problem, instance, alg, config, threshold){ if(!this.loaded) return []; var self = this; var stat = []; this._match_experiment(problem, instance, alg, config, function(exp){ var exp_data = self._load_experiment(exp.filename, exp.runs, exp.steps); var rqd = Analyzer._runtime_solvability_distribution(exp_data, threshold, problem); stat.push({ 'config' : exp.config, 'data' : rqd }); }); return stat; }, /** get runtime quality distribution result for an algorithm running on a specific problem instance - note tat there might be multiple experiemts with different parameter settings, so if config is provided, attemp to fetch all experiments matching the config settings! here as long as the config is a subset of that algorithm's actual run setting, we will include the experiment in the result **/ get_runtime_quality : function(problem, instance, alg, config){ if(!this.loaded) return []; var self = this; var stat = []; this._match_experiment(problem, instance, alg, config, function(exp){ var exp_data = self._load_experiment(exp.filename, exp.runs, exp.steps); var rqd = Analyzer._runtime_quality_distribution(exp_data); stat.push({ 'config' : exp.config, 'data' : rqd }); }); return stat; }, /** find experiments matching the specified criteria . invoked fn callback on every matched experiment fn - function(experiment) experiment comes from master's experiment object **/ _match_experiment : function(problem, instance, alg, config, fn){ if(!this.loaded) return; if(this.master[problem] && this.master[problem][instance] && this.master[problem][instance][alg]){ var exps = this.master[problem][instance][alg]; exps.forEach(function(exp){ //if config provided, check if we shall include it var shouldadd = true; if(config){ for(var cfgname in config){ if(exp.config[cfgname] == undefined){ console.log("WARNING: user specified a non-existing parameter config name: [" + cfgname + "] exp.config: " + exp.config); shouldadd = false; break; } //if any user-specified config is not matched agains the experiment's run setting, should not add this experiment if(exp.config[cfgname] != config[cfgname]){ shouldadd = false; break; } } } //load the experiment data and compute runtie quality if(shouldadd) fn(exp); }); } }, //=== loader functions /** loading content of an experiment session data return true if loading success, false otherwise **/ _load : function(){ this._loaded = false; this.config = null; this.master = null; this.config = this._load_config(); if(!this.config){ console.log("WARNING: failed to load config file"); return; } this.master = this._load_master(); if(!this.config){ console.log("WARNING: failed to load master file"); return; } this.loaded = true; }, /** load a raw experiment file and return the run data runtime data is simply a matrix N * M - N is total number of indepedent run. M is max local search steps - each row containing a single indepedent run's fitness level at each local search step **/ _load_experiment : function(filename, runs, steps){ var raw = fs.readFileSync(filename, {'encoding' : "utf8"}); if(!raw) return null; var data = []; var lines = raw.trim().split(/[\s]*\n[\s]*/); //should contain M + 1 fields var M = lines[0].trim().split(/[\s]+/).length - 1; assert.ok(M==steps, "ls steps not matching in exp file: "); for(var i=1;i<lines.length;i++){ var line = lines[i].trim(); if(line.length==0) continue; var vals = line.split(/[\s]+/); if(vals.length != M+1){ console.log("invalid # of columns in " + filename + " at line " + i); return null; } var row = []; vals.forEach(function(val){ var fitness = parseFloat(val); assert.ok(!isNaN(fitness),"invalid fitness value"); row.push(fitness); }); assert.ok(row.length==M+1); data.push(row); } assert.ok(data.length == runs, "# of runs not matching in exp file"); return data; }, /** a returned master file contains the content of the index master[problem_category][problem_instance_name][algorithm_name] is an array of object having following field [config] : run configuration string [filename] : the experiment run data [runs] : total number of independent runs [steps] : local step uppoer bounds **/ _load_master : function(){ var raw = fs.readFileSync(this.exp_dir+"/master.tsv", {'encoding' : "utf8"}); if(!raw) return null; var lines = raw.trim().split('\n'); var version = lines[0].match(/(#version)[\s]+([\d.]+)/)[2]; var fields = lines[1].substr(1).trim().split(/[\s]+/); var master = {}; for(var i=2;i<lines.length;i++){ var line = lines[i].trim(); if(line.length==0) continue; var entries = line.split(/[\s]/); var problem = entries[0]; var instance = entries[1]; var algs = entries[2]; var config = JSON.parse(entries[3]); assert(config!=undefined); var filename = entries[4]; var runs = parseInt(entries[5]); assert(!isNaN(runs)); var steps = parseInt(entries[6]); assert(!isNaN(steps)); if(!master[problem]) master[problem] = {}; if(!master[problem][instance]) master[problem][instance] = {}; if(!master[problem][instance][algs]) master[problem][instance][algs] = []; master[problem][instance][algs].push({ 'config' : config, 'filename' : filename, 'runs' : runs, 'steps' : steps }); } return master; }, _load_config : function(){ //load configuration var configStr = fs.readFileSync(this.exp_dir+"/config.json", {'encoding' : "utf8"}); if(!configStr) return null; var config = JSON.parse(configStr); return config; }, }, statics : { /** solvability is percentage of runs that found solution better than the threshold fitness return a runtime array with each value being the averaged solvability at that local search step **/ _runtime_solvability_distribution : function(raw_data, threshold, problem_name){ //find the problem clas var problem = loadProblemClass(problem_name); var instance = new problem(null); var rqd = new Array(raw_data[0].length); // containing N+1 entries where N is max local search step for(var i=0;i<rqd.length;i++) rqd[i] = 0; for(var run=0;run<raw_data.length;run++){ for(var step=0;step<raw_data[run].length;step++){ var isBetter = instance.minimization ? (raw_data[run][step] < threshold) : (raw_data[run][step] > threshold); if(isBetter) rqd[step] += 1; } } for(var i=0;i<rqd.length;i++) { rqd[i] = rqd[i]/raw_data.length; assert.ok(rqd[i]>=0 && rqd[i] <= 1); } return rqd; }, /** given the loaded raw experiment data, compute runtime quality distribution array return a simple array containing average solution quality at each local search step **/ _runtime_quality_distribution : function(raw_data){ var rqd = new Array(raw_data[0].length); // containing N+1 entries where N is max local search step for(var i=0;i<rqd.length;i++) rqd[i] = 0; for(var run=0;run<raw_data.length;run++){ for(var step=0;step<raw_data[run].length;step++){ rqd[step] += raw_data[run][step]; } } for(var i=0;i<rqd.length;i++) rqd[i] = rqd[i]/raw_data.length; return rqd; } } }); function loadAlgorithmClass(problem_name, alg_name){ //load algorithm class var Algorithm = require('example/'+problem_name+"/Algorithms.js")[alg_name.toUpperCase()]; assert.ok(Algorithm && typeof(Algorithm)=="function", "failed to load " + 'example/'+problem_name+"/Algorithms.js - " + alg_name.toUpperCase()); return Algorithm; } function loadProblemClass(problem_name){ var Problem = require('example/'+problem_name.toLowerCase()+"/Problem.js"); assert(Problem && typeof(Problem)=="function"); return Problem; } module.exports.Experiment = Experiment; module.exports.Analyzer = Analyzer;