Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
pedroslopez committed Jan 23, 2019
1 parent f742a34 commit 48ebe37
Show file tree
Hide file tree
Showing 5 changed files with 319 additions and 0 deletions.
101 changes: 101 additions & 0 deletions Population.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const Schedule = require('./Schedule');

function mapNum (n, in_min, in_max, out_min, out_max) {
return (n - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}

function getRndInteger(min, max) {
return Math.floor(Math.random() * (max - min) ) + min;
}

module.exports = function Population(classCodes, mutationRate, num) {
this.population = [];
this.matingPool = [];
this.generations = 0;
this.finished = false;

this.mutationRate = mutationRate;
this.perfectScore = 1;

for(var i=0; i<num; i++) {
this.population[i] = new Schedule(classCodes, true);
}

// Calculate fitness of each schedule
this.calcFitness = function() {
for(var i=0; i<this.population.length;i++) {
this.population[i].calcFitness();
}
}
this.calcFitness();

this.naturalSelection = function() {
this.matingPool = [];

var maxFitness = 0;
for(var i=0; i<this.population.length;i++) {
if(this.population[i].fitness > maxFitness) {
maxFitness = this.population[i].fitness;
}
}

for(var i=0; i<this.population.length;i++) {
var fitness = mapNum(this.population[i].fitness,0,maxFitness,0,1);
var n = Math.floor(fitness*100);
for(var j=0;j<n;j++) {
this.matingPool.push(i);
}
}
}

this.generate = function() {
var newPop = [];
for(var i=0; i<this.population.length;i++) {
var a = getRndInteger(0,this.matingPool.length);
var b = getRndInteger(0, this.matingPool.length);
var partnerA = this.population[this.matingPool[a]];
var partnerB = this.population[this.matingPool[b]];
var child = partnerA.crossover(partnerB);
child.mutate(this.mutationRate);
newPop.push(child);
}
this.population = newPop;
this.generations++;
}

this.getBest = function() {
return this.best;
}

this.evaluate = function() {
var record = 0.0;
var index = 0;

for(var i=0; i < this.population.length; i++) {
if(this.population[i].fitness > record) {
index = i;
record = this.population[i].fitness;
}
}

this.best = this.population[index];
if(record === this.perfectScore) {
this.finished = true;
}
}

this.isFinished = function() {
return this.finished;
}

this.getGenerations = function() {
return this.generations;
}

this.print = function() {
for(var i=0; i<this.population.length;i++) {
this.population[i].print();
}
}

}
163 changes: 163 additions & 0 deletions Schedule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
const oferta = require('./oferta.json')['oferta']; // oferta is auto generated by scraper

// format {
// CBF210: {
// name: "",
// schedule: {
// lunes: {
// desde: 0,
// hasta: 1
// }
// ...
// }
// }
// ...
// }

function getRndInteger(min, max) {
return Math.floor(Math.random() * (max - min) ) + min;
}

function selectRandomSection(code) {
if(!oferta[code]) {

throw "COULD NOT FIND OFFER FOR " + code;
}
if(oferta[code].secciones.length > 0) {
return oferta[code].secciones[getRndInteger(0, oferta[code].secciones.length)];
}
return null;
}

function checkDesiredSectionConflict(curr, desiredSect) {
if(!desiredSect || (desiredSect && desiredSect.includes(curr.sec))) {
return true;
}

return false;
}

function checkDayConflict(day, curr, pool, log) {
let myTime = curr.schedule[day];
if(myTime) {
for(let i=0; i<pool.length; i++) {
let otherTime = pool[i].schedule[day];
if(log) {
console.log('CHECK CONFLICTS');
console.log('me',myTime);
console.log('oth',otherTime);
}
if(otherTime) {
if(myTime.desde < otherTime.hasta && otherTime.desde < myTime.hasta) {
return true;
}
}
}
}

return false;
}

const days = ['lunes', 'martes', 'miercoles', 'jueves', 'viernes', 'sabado'];

module.exports = function Schedule(classCodes, prefill) {
this.classCodes = classCodes;
this.sections = [];
this.fitness = 0;

if(prefill) {
for(let i=0; i<classCodes.length; i++) {

this.sections[i] = selectRandomSection(classCodes[i].code);
}
}

this.getCredits = function() {
let credits = 0;
for(let i=0; i<this.classCodes.length; i++) {
let classData = oferta[this.classCodes[i].code];
if(classData.credits) {
credits+= classData.credits;
}
}

return credits;
}

this.calcFitness = function(log) {
let conflicts = 0;

for(let i=0; i<this.sections.length; i++) {
let sect = this.sections[i];
let desiredSect = this.classCodes[i].section;

let hasDesiredSect = checkDesiredSectionConflict(sect, desiredSect);
if(!hasDesiredSect) {
conflicts++;
}

for(let j=0; j<days.length;j++) {
if(log) {
console.log('check if has conflict for day ' + days[j]);
}
let hasConflict = checkDayConflict(days[j], sect, this.sections.slice(i+1), log);

if(hasConflict)
conflicts++;

}
}

// Calculate score based on MAX number of conflicts - this one's number of conficts
let score = this.sections.length*2 - conflicts;
this.fitness = score/(this.sections.length*2);

if(log) {
console.log('CONFLICTS', conflicts);
console.log('sections', this.sections.length);
console.log('score', score);
console.log('fitness', this.fitness);
}
}

this.crossover = function(partner) {
let child = new Schedule(this.classCodes);

// For crossover, for each class we will have
// a 50% chance of picking the section from either parent.
let swapProbability = 0.5;
for(let i=0; i<this.sections.length;i++) {
if(Math.random() < swapProbability) {
child.sections[i] = this.sections[i];
} else {
child.sections[i] = partner.sections[i];
}
}

return child;
}

this.mutate = function(mutationRate) {
// For each section, there's a mutationRate chance that it will get
// replaced by another random section (of the same class)
for(let i=0; i<this.sections.length; i++) {
if(Math.random() < mutationRate) {
this.sections[i] = selectRandomSection(this.classCodes[i].code);
}
}
}

this.print = function() {
for(let i=0; i<this.sections.length; i++) {
let sect = this.sections[i];
console.log(oferta[this.classCodes[i].code].name + '\r\n-- ' + sect.sec + ' ' + sect.prof);
for(let j=0;j<days.length;j++) {
if(sect.schedule[days[j]]) {
console.log('---- ' + days[j] + ' ' + sect.schedule[days[j]].desde + '-' + sect.schedule[days[j]].hasta);
}

}
console.log();
}
}
}
42 changes: 42 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const Population = require('./Population');

var selection = [
// {code: "CBM203", section: ["06"]}, Force a section
// {code: "ING210", section: ["01", "06", "07", "08"]}, Select from specific sections
// {code: "IDS336"}, // Select from available sections

{code: "ADM315"}, // Administracion y Gestion empresarial
{code: "IDS335"}, // Diseño de software
{code: "INS380"}, // Base de datos II
{code: "INS380L"}, // Lab base de datos II
{code: "INS314", section: ["01"]}, // Comunicacion de Datos I
{code: "INS314", section: ["71"]} // Lab comun datos I
];

const mutationRate = 0.01;
const popMax = 50;
const maxGenerations = 1000; // When this number is reached, the best to date will be selected
// (it usually means the schedule was impossible to generate without conflicts)

var population = new Population(selection, mutationRate, popMax);

while(!population.isFinished()) {
population.naturalSelection();
population.generate();
population.calcFitness();
population.evaluate();
console.log('Generation ' + population.getGenerations());

if(maxGenerations && population.getGenerations() == maxGenerations) {
console.log('FAILED. REACHED MAX NUMBER OF GENERATIONS. THE BEST FOUND TO DATE WILL BE PRINTED.');
break;
}
}

console.log('DONE!');
population.getBest().print();
console.log('Credits: ' + population.getBest().getCredits());
console.log('Winning fitness: ' + population.getBest().fitness);



1 change: 1 addition & 0 deletions oferta.json

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "schedule-builder",
"version": "1.0.0",
"description": "Tool to build INTEC schedule based on offer generated by the scraper",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node app.js"
},
"author": "Pedro Lopez",
"license": "ISC"
}

0 comments on commit 48ebe37

Please sign in to comment.