UNPKG

@zfunction/genetics-js

Version:

Genetic and evolutionary algorithms framework for the web

336 lines 14.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; var _a, _b; Object.defineProperty(exports, "__esModule", { value: true }); var _1 = require("./"); var random_js_1 = require("random-js"); var fs_1 = __importDefault(require("fs")); var yargs_1 = __importDefault(require("yargs")); var xml_io_1 = require("./xml-io"); var executor_1 = require("./executor"); var converter_1 = require("./converter"); var path_1 = __importDefault(require("path")); var chalk_1 = __importDefault(require("chalk")); console.time("execution"); /** * Program arguments and flags. */ var argv = yargs_1.default .usage("$0 -n <netfile> -r <routefile> [-p] [-s <savepath>]") .help() .options({ p: { type: "boolean", alias: "play", demandOption: false, description: "Executes simulation after evolutive alg. ends", }, n: { type: "string", alias: "network", demandOption: true, description: "Network file", }, r: { type: "string", alias: "routes", demandOption: true, description: "Route file", array: true, }, s: { type: "string", alias: "save", demandOption: true, description: "Filepath to save best network candidate", }, c: { type: "string", alias: "crossover", demandOption: true, description: "Type of crossover to employ", }, i: { type: "number", alias: "population", demandOption: true, description: "Size of the population", }, g: { type: "boolean", alias: "savegenotype", demandOption: false, description: "Saves genotype instead of net.xml", }, }) .argv; // Since all this arguments are mandatory they won't be undefined, but typescript doesn't know this, // so usage of ! operator is required var netFilepath = argv.n; var routesFilepath = argv.r; var saveFilepath = argv.s; var crossoverStr = argv.c; var populationSize = Number(argv.i); var saveGenotype = argv.g; // Reads and parses network file var originalTl = xml_io_1.parseTlLogic(netFilepath); // The reason I made this is because originalTL contains the original data of the traffic light system // (phases config and such) and this information is never modified in the evolutionary algorithm. The thing is // I need that information to generate the new file that will contain the solution generated thanks to the // EA, and the only values that change are durations and offsets, but not the phase config. That's why // is set only once, so I don't need to call every time genotypeToTlLogic() with this argument. converter_1.setOriginalTl(originalTl); // TODO: This should be program arguments var maxGenerations = 2; var genotypeLength = originalTl.reduce(function (a, b) { return a + b.phases.length; }, 0) + originalTl.length; // total phases + offset of every traffic light // TODO: this value should be 1 / (amount of phases and offsets), though more investigation is needed. // maybe consider it as an argument? var mutationRate = 1 / genotypeLength; var yellowPhaseDuration = 4; // Used to print info about what individual and generation is being executed. var iteration = 0; var executionInfo = { execution: "1", generations: [], }; for (var i = 0; i < maxGenerations; i++) { executionInfo.generations.push({ individuals: [] }); } /** * Calculates the fitness value of the individual. * @param individual is a NumericIndividual, meaning it's just an array of numbers. The individual is composed * of phase durations and offsets. Both values are indistinguishable from each other, the only way to know which * is which is to know the original order they are arranged in originalTl. */ var fitnessFunction = function (individual) { // First, convert the number array to an array of TLLogic objects var tl = converter_1.genotypeToTlLogic(individual.genotype); // We write that array to a temporal file, with the only purpose of using it as an argument to SUMO var networkFilename = xml_io_1.writeTlLogic(tl); // this will execute a simulation and return a SumoAggregatedData object, that contains // info about how the simulation went var data = executor_1.executeSumo({ flags: [ "--no-warnings", "--no-step-log", "--end 5", "--time-to-teleport 120", // `--seed ${}`, // define seed "--duration-log.statistics", "--tripinfo-output.write-unfinished", ], files: { network: networkFilename, routes: routesFilepath, }, }); var vehicles = data.vehicles, statistics = data.statistics, performance = data.performance; // At the end, the fitness function is // // (vehicles that reached their destination)^2 // ------------------------------------------------------------------------------------------------------------------ // avg travel duration + avg time car is not moving + (vehicles that didn't reach their destination) * simulated time var maximize = Math.pow(vehicles.inserted - (vehicles.running + vehicles.waiting), 2); // vehicles that completed their travel var minimize = statistics.duration + statistics.timeLoss + (vehicles.running + vehicles.waiting) * ((performance.duration / 1000) * performance.realTimeFactor); // We are interested in maximizing the numerator and reducing the denominator for obvious reasons. var fitness = maximize / minimize; // This is only to show info about what individual/generation are we simulating var generation = Math.floor(iteration / populationSize); var individualNumber = Math.abs(populationSize * generation - iteration) + 1; console.log("FITNESS: Gen " + generation + ", Ind " + individualNumber + ": " + fitness + "\n"); iteration++; if (saveGenotype && (generation % 1 === 0)) { executionInfo.generations[generation].individuals.push({ fitness: fitness, genotype: individual.genotype, }); } return fitness; }; var crossover; if (crossoverStr === "UniformCrossover") { crossover = new _1.UniformCrossover(); } else if (crossoverStr === "OnePointCrossover") { crossover = new _1.OnePointCrossover(); } else { throw new Error("Crossover type not recognized"); } // This gigantic object is just the EA configuration. Bunch of types and objects. For reference // see Abrante's Dissertation on the subject at https://riull.ull.es/xmlui/handle/915/14535 var params = { populationSize: populationSize, generator: new _1.IntegerGenerator(), generatorParams: { engine: random_js_1.nativeMath, length: originalTl.reduce(function (a, b) { return a + b.phases.length; }, 0) + originalTl.length, range: new _1.NumericRange(10, 120), particularValue: function (index) { if (doesPhaseContainsYellow(index)) { return yellowPhaseDuration; } else { return undefined; } }, }, selection: new _1.FitnessProportionalSelection(), selectionParams: { engine: random_js_1.nativeMath, selectionCount: populationSize, subSelection: new _1.RouletteWheel(), }, crossover: crossover, crossoverParams: { engine: random_js_1.nativeMath, individualConstructor: _1.IntegerIndividual, // @ts-ignore selectionThreshold: 0.5, }, mutation: new _1.RandomResetting(), mutationParams: { engine: random_js_1.nativeMath, mutationRate: mutationRate, particularValue: function (index) { if (doesPhaseContainsYellow(index)) { return yellowPhaseDuration; } else { return undefined; } }, }, replacement: new _1.FitnessBased(), replacementParams: { selectionCount: populationSize, }, fitnessFunction: fitnessFunction, terminationCondition: new _1.MaxGenerations(maxGenerations), }; /* * Provided an index of a NumericIndividual, is capable of detecting whether that index refer to an offset * or a phase duration. Then, if the index refers to a phase, the function returns whether that phase * contains a yellow traffic light. */ function doesPhaseContainsYellow(index) { // tlSize = TL offset + amount of phases var tlSizes = originalTl.map(function (tl) { return tl.size; }); // what traffic light junction "index" refers to var tlPos = 0; // Let's say we have the next TLLogic[] that we have converted into a NumericIndividual (array of numbers) // [10, 60, 4, 70, 4, 5, 80, 4, 50, 4] // where | TLLogic[0] |, | TLLogic[1] | while (index >= 0) { if ((index + 1) - tlSizes[tlPos] <= 0) { // In this case, "index" refers to an unknown element located at TLLogic[tlPos]. // TLLogic[tlPos] have one offset and several phases. They are indistinguishable in NumericIndividual, // given that they are just numbers. However, we know that the first element of every TLLogic is the offset, // the rest are phase durations. // In this case, TLLogic[0] = [10, 60, 4, 70, 4] and TLLogic[1] = [5, 80, 4, 50, 4] where // the first element of each array is the offset and the rest are phase durations, as we just stated. // If index = 3, then it refers to this ↓↓ element (70) of TLLogic[1]. // [10, 60, 4, 70, 4, 5, 80, 4, 50, 4] // given that 3 is less than TLLogic[0] length. break; } // In this case, given that index is greater than the amount of elements that are in TLLogic[0], we would skip the // conditional and calculate index for TLLogic[1], which is why we increment tlPos and substract the length of // TLLogic[0] to index. // If index = 7, then it refers to this ↓ element of the individual // [10, 60, 4, 70, 4, 5, 80, 4, 50, 4] // which in turn would be the third element (pos 2, we start counting at 0) in TLLogic[1] = [5, 80, 4, 50, 4]. // ^ // And so on. index -= tlSizes[tlPos]; tlPos++; } if (index === 0) { // offset values are always at the start of the array, then the phase durations return false; } else { // phase duration var phase = originalTl[tlPos].phases[index - 1]; return phase.state.includes("y"); } } var evolutionaryAlgorithm = new _1.EvolutionaryAlgorithm(params); // Finally executes the EA evolutionaryAlgorithm.run(); // Once the EA it's done, get the fittest individual var bestCandidate = (_a = evolutionaryAlgorithm.population.getFittestIndividualItem()) === null || _a === void 0 ? void 0 : _a.individual; var fitness = (_b = evolutionaryAlgorithm.population.getFittestIndividualItem()) === null || _b === void 0 ? void 0 : _b.fitness; if (bestCandidate === undefined) { throw "Not fittest individual found"; } // function writeToFile(values: number[], filepath: string) { // console.log("Checking ", filepath); // if (!fs.existsSync(path.dirname(filepath))) { // fs.mkdirSync(path.dirname(filepath), { recursive: true }); // } // // console.log("Writing..."); // fs.writeFile(filepath, values.toString() + "\n", { // encoding: "utf8", // flag: "a" // },(err) => { // if (err) return console.log(err); // console.log(c.green(path.basename(filepath), "has been saved")); // }); // } var stringify = function (obj, indent) { if (indent === void 0) { indent = 2; } return JSON.stringify(obj, function (key, value) { if (Array.isArray(value) && !value.some(function (x) { return x && typeof x === 'object'; })) { return "\uE000" + JSON.stringify(value.map(function (v) { return typeof v === 'string' ? v.replace(/"/g, '\uE001') : v; })) + "\uE000"; } return value; }, indent).replace(/"\uE000([^\uE000]+)\uE000"/g, function (match) { return match.substr(2, match.length - 4).replace(/\\"/g, '"').replace(/\uE001/g, '\\\"'); }); }; console.log("Checking ", saveFilepath); if (!fs_1.default.existsSync(path_1.default.dirname(saveFilepath))) { fs_1.default.mkdirSync(path_1.default.dirname(saveFilepath), { recursive: true }); } console.log("Writing..."); fs_1.default.writeFile(saveFilepath, stringify(executionInfo), { encoding: "utf8", flag: "a" }, function (err) { if (err) return console.log(err); console.log(chalk_1.default.green(path_1.default.basename(saveFilepath), "has been saved")); }); // Convert the array of numbers that is the individual to a network file recognizable by SUMO var tl = converter_1.genotypeToTlLogic(bestCandidate); var networkFilename = xml_io_1.writeTlLogic(tl); // Copy that file to the location the used specified fs_1.default.renameSync(networkFilename, saveFilepath.concat(".net.xml")); console.log("Fittest candidate located at ", saveFilepath); console.log("Best fitness achieved", fitness); console.timeEnd("execution"); // // If the flag is provided, SUMO-GUI will be executed with the fittest solution to see how it behaves // if (argv.play) { // console.log("\nExecuting simulation"); // executeSumo({ // command_name: "sumo-gui", // flags: [ // "--no-warnings", // don't log warnings // "--no-step-log", // don't log step info // "--time-to-teleport -1", // disable teleports // "--seed 23432", // define seed // "--duration-log.statistics", // log aggregated information about trips // "--tripinfo-output.write-unfinished", // include info about vehicles that don't reach their destination // ], // files: { // network: `"${saveFilepath}"`, // routes: routesFilepath, // // additional: ['./assets/anchieta_pedestrians.rou.xml'] // }, // }, // ); // } //# sourceMappingURL=genetics.js.map