Skip to content

Commit

Permalink
Merge pull request #38 from nikele2001/Branch-Algo
Browse files Browse the repository at this point in the history
Integrate algorithm into workflow
  • Loading branch information
sopa301 authored Dec 15, 2023
2 parents 4205219 + 9daa680 commit 10a2fb3
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 20 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "Blue Elephant Grouping Bot",
"main": "src/index.ts",
"author": "Ryan Poon (https://github.com/sopa301)\nNicholas Chia (https://github.com/nikele2001)",
"author": "The Careless Llama (https://github.com/CarelessLlama)",
"homepage": "https://github.com/nikele2001/Blue-Elephant",
"dependencies": {
"@vercel/node": "^3.0.12",
Expand Down
71 changes: 71 additions & 0 deletions src/commands/generateGroupingsScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import createDebug from 'debug';

import { Scenes, Markup } from 'telegraf';

import { AlgorithmRunner } from '../models/AlgorithmRunner';

import { UnknownError, InvalidInputTypeError } from '../exceptions';

import { BotContext, updateSessionDataBetweenScenes } from '../BotContext';

import { saveProject } from '../db/functions';

const debug = createDebug('bot:generate_groupings_command');

const getNumGroups = async (ctx: BotContext) => {
updateSessionDataBetweenScenes(ctx);
try {
debug(`Entering getNumGroups scene.`);
updateSessionDataBetweenScenes(ctx);
await ctx.reply(
`How many groups do you want to generate?`,
Markup.removeKeyboard(),
);
return ctx.wizard.next();
} catch (error) {
const errorMessage = (error as Error).message;
debug(errorMessage);
await ctx.reply(errorMessage);
return ctx.scene.reenter();
}
};

const handleNumGroups = async (ctx: BotContext) => {
debug('User entered number of groups.');
if (!ctx.message || !ctx.from) {
throw new UnknownError(
'An unknown error occurred. Please try again later.',
);
}

if (!('text' in ctx.message)) {
throw new InvalidInputTypeError(
'Invalid input type. Please enter a text message.',
);
}

const numGroups = parseInt(ctx.message.text);

if (isNaN(numGroups)) {
throw new InvalidInputTypeError(
'Invalid input. Please enter a number.',
);
}

const logic = new AlgorithmRunner(ctx.scene.session.project, numGroups);
const groupings = logic.prettyPrintGroupings();
logic.updateInteractionsBasedOnGeneratedGroupings();
saveProject(ctx.scene.session.project);
const out = `Here are the groupings:\n${groupings}.
Do not delete this message as you will need it to view the groupings again.`;
await ctx.reply(out);
return ctx.scene.enter('mainMenu');
};

const generateGroupingsScene = new Scenes.WizardScene(
'generateGroupings',
getNumGroups,
handleNumGroups,
);

export { generateGroupingsScene };
2 changes: 2 additions & 0 deletions src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ export * from './deleteProjectScene';
export * from './viewMainMenuScene';
export * from './generateExistingProjectsScene';
export * from './viewProjectScene';
export * from './generateGroupingsScene';
export * from './resetInteractionsScene';
15 changes: 12 additions & 3 deletions src/commands/manageProjectScene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ const manageProject = async (ctx: BotContext) => {
await ctx.reply(
`Project retrieved. What do you want to do?`,
Markup.keyboard([
['View Project Details', 'Edit Project'],
['Delete Project', 'Back'],
[
'View Project Details',
'Generate Groupings',
'Reset Interactions',
],
['Edit Project', 'Delete Project', 'Back'],
]).resize(),
);
return ctx.wizard.next();
Expand All @@ -43,9 +47,14 @@ const handleManageProjectOption = async (ctx: BotContext) => {
}
if (ctx.message?.text === 'View Project Details') {
return ctx.scene.enter('viewProject', ctx.scene.session);
} else if (ctx.message?.text === 'Generate Groupings') {
debug('User selected "Generate Groupings"');
return ctx.scene.enter('generateGroupings', ctx.scene.session);
} else if (ctx.message?.text === 'Reset Interactions') {
debug('User selected "Reset Interactions"');
return ctx.scene.enter('resetInteractions', ctx.scene.session);
} else if (ctx.message?.text === 'Edit Project') {
debug('User selected "Edit Project"');
// to do: edit project scene
return ctx.scene.enter('editProject', ctx.scene.session);
} else if (ctx.message?.text === 'Delete Project') {
debug('User selected "Delete Project"');
Expand Down
61 changes: 61 additions & 0 deletions src/commands/resetInteractionsScene.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import createDebug from 'debug';

import { Scenes, Markup } from 'telegraf';

import { UnknownError, InvalidInputTypeError } from '../exceptions';

import { BotContext, updateSessionDataBetweenScenes } from '../BotContext';

import { saveProject } from '../db/functions';

const debug = createDebug('bot:reset_interactions_command');

const resetInteractions = async (ctx: BotContext) => {
updateSessionDataBetweenScenes(ctx);
try {
debug(`Entering resetInteractions scene.`);
updateSessionDataBetweenScenes(ctx);
await ctx.reply(
`Are you sure you want to reset the groupings?`,
Markup.keyboard([['Yes', 'No']]).resize(),
);
return ctx.wizard.next();
} catch (error) {
const errorMessage = (error as Error).message;
debug(errorMessage);
await ctx.reply(errorMessage);
return ctx.scene.reenter();
}
};

const handleResetInteractions = async (ctx: BotContext) => {
debug('User entered reset interactions option.');
if (!ctx.message || !ctx.from) {
throw new UnknownError(
'An unknown error occurred. Please try again later.',
);
}

if (!('text' in ctx.message)) {
throw new InvalidInputTypeError(
'Invalid input type. Please enter a text message.',
);
}

if (ctx.message?.text === 'Yes') {
ctx.scene.session.project.resetInteractions();
saveProject(ctx.scene.session.project);
await ctx.reply('Interactions have been reset.');
} else if (ctx.message?.text === 'No') {
await ctx.reply('Interactions have not been reset.');
}
return ctx.scene.enter('mainMenu');
};

const resetInteractionsScene = new Scenes.WizardScene(
'resetInteractions',
resetInteractions,
handleResetInteractions,
);

export { resetInteractionsScene };
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
viewMainMenuScene,
viewProjectScene,
generateExistingProjectsScene,
generateGroupingsScene,
resetInteractionsScene,
} from './commands';
import { VercelRequest, VercelResponse } from '@vercel/node';
import { development, production } from './core';
Expand Down Expand Up @@ -45,6 +47,8 @@ const stage = new Scenes.Stage<BotContext>([
editProjectScene,
editProjectNameScene,
editProjectDescriptionScene,
generateGroupingsScene,
resetInteractionsScene,
]);

bot.use(session());
Expand Down
45 changes: 41 additions & 4 deletions src/models/AlgorithmRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,21 @@ import { SortedEdgeGenerator } from './SortedEdgeGenerator';
* Class that runs the algorithm to generate optimal groupings for a project
*/
class AlgorithmRunner {
public constructor() {}
private groupings: number[][];
private project: Project;

public constructor(project: Project, numGroups: number) {
this.project = project;
this.groupings = this.run(project, numGroups);
}

/**
* Run the algorithm
* @param project - Project to run the algorithm on
* @param numGroups - Number of groups to split the project into
* @returns array of groups
*/
public static run(project: Project, numGroups: number) {
public run(project: Project, numGroups: number): number[][] {
const adjMatrix = project.getAdjMatrix();
const numPeople = adjMatrix.length;
const groupSize = Math.floor(numPeople / numGroups);
Expand Down Expand Up @@ -43,10 +49,41 @@ class AlgorithmRunner {
);
groupings[groupIndex].push(node);
});
console.log(groupings);
this.groupings = groupings;
this.project = project;
return groupings;
}

public mapPersonIdToName(): string[][] {
return this.groupings.map((group) => {
return group.map((personId) => {
return this.project.getPersons()[personId];
});
});
}

public prettyPrintGroupings(): string {
return this.mapPersonIdToName()
.map((index, group) => {
return `Group ${group + 1}: ${index.join(', ')}`;
})
.join('\n');
}

public updateInteractionsBasedOnGeneratedGroupings(): void {
this.groupings.map((group) => {
for (let i = 0; i < group.length; i++) {
for (let j = i + 1; j < group.length; j++) {
this.project.incrementInteractions(group[i], group[j]);
}
}
});
}

public incrementInteractions(person1: number, person2: number): void {
this.project.incrementInteractions(person1, person2);
}

/**
* Retrieve the index of the group that minimizes the cost of adding a node to it
* @param adjMatrix - Adjacency matrix of the project
Expand Down Expand Up @@ -78,7 +115,7 @@ class AlgorithmRunner {
groupings[i],
node,
);
if (cost < minCost) {
if (cost < minCost && groupings[i].length < groupSize + 1) {
minCost = cost;
minIndex = i;
}
Expand Down
12 changes: 12 additions & 0 deletions src/models/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,16 @@ export class Project {
public setAdjMatrix(adjMatrix: number[][]): void {
this.adjMatrix = adjMatrix;
}

public incrementInteractions(person1: number, person2: number): void {
this.adjMatrix[person1][person2]++;
this.adjMatrix[person2][person1]++;
}

public resetInteractions(): void {
const n = this.personArr.length;
this.adjMatrix = Array(n)
.fill(null)
.map(() => Array(n).fill(0));
}
}
36 changes: 29 additions & 7 deletions src/models/TestAlgorithmUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,40 @@ class AlgorithmTester {
}

/**
* Generate a project
* Generate a project with random weights
* @param n - Size of the project
* @returns a project
* @returns a project with random weights
*/
public static generateProject(n: number): Project {
public static generateProjectWithRandomWeights(n: number): Project {
return new Project(
'1',
'projid1',
1,
'test',
'test',
'projname',
'projdesc',
this.generateSymmetricalMatrix(n),
['test1', 'test2'],
Array(n)
.fill(null)
.map((_, i) => 'name ' + i.toString()),
);
}

/**
* Generate a project with no weights
* @param n - Size of the project
* @returns a project with no weights
*/
public static generateProjectWithNoWeights(n: number): Project {
return new Project(
'projid1',
1,
'projname',
'projdesc',
Array(n)
.fill(null)
.map(() => Array(n).fill(0)),
Array(n)
.fill(null)
.map((_, i) => 'name ' + i.toString()),
);
}
}
Expand Down
15 changes: 10 additions & 5 deletions src/models/sampleTest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// import { AlgorithmRunner } from './AlgorithmRunner';
import { AlgorithmRunner } from './AlgorithmRunner';
import { AlgorithmTester } from './TestAlgorithmUtil';

const project = AlgorithmTester.generateProject(9);
console.log(project.getAdjMatrix());
// const groupings = AlgorithmRunner.run(project, 3);
// console.log(groupings);
// Random project with 27 nodes and 10 groups and random weights
const project = AlgorithmTester.generateProjectWithRandomWeights(27);
const logic = new AlgorithmRunner(project, 10); // 3 is the number of groups desired
console.log(logic.prettyPrintGroupings());

// Project with 27 nodes and 10 groups and 0 weights
const project2 = AlgorithmTester.generateProjectWithNoWeights(27);
const logic2 = new AlgorithmRunner(project2, 10); // 3 is the number of groups desired
console.log(logic2.prettyPrintGroupings());

0 comments on commit 10a2fb3

Please sign in to comment.