@zfunction/genetics-js
Version:
Genetic and evolutionary algorithms framework for the web
336 lines • 14.8 kB
JavaScript
"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