-
Notifications
You must be signed in to change notification settings - Fork 344
[WIP] NUSMods Optimizer Prototyping #3296
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 15 commits
6daeea0
39ae230
c18487a
3d3407c
6858299
2642863
0fb26c0
c7dfbd1
edfbbe2
69d7376
298d179
e4a23cc
8b83870
b92f711
3ffd9e6
c212883
75ad08e
a01d69f
cc548b1
befafca
0ddb1c7
c5bb543
62f0acc
43c8927
17529bb
212cf22
4b2cfc2
7594da1
3fb9d34
c6104c2
392302a
b1b1abb
cfd1ad0
caabee3
d9153ab
66456ff
525d604
fdb4972
de387b2
947dffe
2ba7c34
41c09a0
2527a48
54ea81e
ccc5497
5809287
fe120de
513cd21
d839ae4
72efcc4
81119bf
3d50400
7ff3b04
4f6857a
2e75c66
952ff8c
978acb1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export const TOGGLE_OPTIMIZER_DISPLAY = 'TOGGLE_OPTIMIZER_DISPLAY' as const; | ||
export function toggleOptimizerDisplay() { | ||
return { | ||
type: TOGGLE_OPTIMIZER_DISPLAY, | ||
payload: null, | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { OptimizerState } from 'types/reducers'; | ||
import { Actions } from 'types/actions'; | ||
|
||
import { TOGGLE_OPTIMIZER_DISPLAY } from 'actions/optimizer'; | ||
|
||
export const defaultOptimizerState: OptimizerState = { | ||
isOptimizerShown: false, | ||
}; | ||
|
||
function optimizer(state: OptimizerState = defaultOptimizerState, action: Actions): OptimizerState { | ||
switch (action.type) { | ||
case TOGGLE_OPTIMIZER_DISPLAY: | ||
return { | ||
...state, | ||
isOptimizerShown: !state.isOptimizerShown, | ||
}; | ||
default: | ||
return state; | ||
} | ||
} | ||
|
||
export default optimizer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
// Intermediate types used to contain timetable information relevant to the optimizer | ||
import { mapValues, groupBy } from 'lodash' | ||
import { Module, RawLesson } from 'types/modules' | ||
import { SemTimetableConfig } from 'types/timetables' | ||
|
||
// {module id}__{lesson type}__{lesson id} e.g., CS3203__Lecture__1 | ||
export type UniqueLessonID = string; | ||
export type Z3LessonID = number; | ||
|
||
/** | ||
* Main input format into the optimizer layers | ||
* */ | ||
export type OptimizerInput = { | ||
moduleInfo: ModuleInfoWithConstraints[]; | ||
constraints: GlobalConstraints; | ||
} | ||
|
||
|
||
/** | ||
* Callbacks to communicate with the caller of TimetableOptimizer | ||
* */ | ||
export interface OptimizerCallbacks { | ||
onOptimizerInitialized: any; | ||
onSmtlib2InputCreated(s: string): any; | ||
onOutput(s: string): any; | ||
// onTimetableOutput(timetable: TimetableOutput): any; | ||
onTimetableOutput(timetable: OptimizerOutput): any; | ||
} | ||
|
||
/** | ||
* Final timetable output to the optimizer caller | ||
* */ | ||
export type OptimizerOutput = SemTimetableConfig; | ||
|
||
/** | ||
* Modules for optimizer to consider. | ||
* required is a constraint indicating if the module can be dropped to fulfil other constraints | ||
* */ | ||
export type ModuleInfoWithConstraints = { | ||
mod: Module; | ||
required: boolean; | ||
lessonsGrouped: LessonsByGroupsByClassNo | ||
} | ||
|
||
// Mapping between lesson types -> classNo -> lessons. | ||
// We have to take one classNo of each lessonType, so this indicates all the slots to be filled | ||
// per classNo per lessonType | ||
export type LessonsByGroupsByClassNo = { | ||
[lessonType: string]: { [classNo: string]: readonly RawLesson[] }; | ||
} | ||
|
||
|
||
// User-selected constraints to pass to optimizer | ||
export interface GlobalConstraints { | ||
frizensami marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Min/max number of MCs + whether the constraint is active | ||
workloadActive: boolean; | ||
minWorkload: number; | ||
maxWorkload: number; | ||
// Find exactly N free days + whether the constraint is active | ||
freeDayActive: boolean; | ||
numRequiredFreeDays: number; | ||
// Force these exact free days + whether the constraint is active | ||
specificFreeDaysActive: boolean; | ||
specificFreeDays: Array<string>; | ||
// When lessons should start and end + whether the constraint is active | ||
timeConstraintActive: boolean; | ||
startTime: string; | ||
endTime: string; | ||
// The hours where a lunch break should be allocated, | ||
// how many half-hour slots to allocate, and whether the constraint is active | ||
lunchStart: string; | ||
lunchEnd: string; | ||
lunchHalfHours: number; | ||
lunchBreakActive: boolean; | ||
// Ask optimizer to compact timetable to leave as few gaps between lessons as possible | ||
preferCompactTimetable: boolean; | ||
} | ||
|
||
/** | ||
* Defs for communicating between Optimizer <-> WebWorker <-> WASM wrapper | ||
* */ | ||
export enum Z3MessageKind { | ||
|
||
// Request to init | ||
INIT = 'INIT', | ||
// Z3 initialized | ||
INITIALIZED = 'INITIALIZED', | ||
// Run the optimizer | ||
OPTIMIZE = 'OPTIMIZE', | ||
// Print output | ||
PRINT = 'PRINT', | ||
// Error | ||
ERR = 'ERR', | ||
// Z3 finished runnung | ||
EXIT = 'EXIT', | ||
// Z3 aborted | ||
ABORT = 'ABORT', | ||
} | ||
|
||
/** | ||
* Message to be sent back and forth between a Z3 webworker and any callers | ||
* */ | ||
export interface Z3Message { | ||
kind: Z3MessageKind; | ||
msg: string; | ||
} | ||
|
||
|
||
|
||
// TODO Shouldn't be here | ||
export const defaultConstraints: GlobalConstraints = { | ||
workloadActive: false, | ||
minWorkload: 0, | ||
maxWorkload: 30, | ||
freeDayActive: false, | ||
numRequiredFreeDays: 1, | ||
specificFreeDaysActive: false, | ||
specificFreeDays: [], | ||
startTime: '0800', | ||
endTime: '2200', | ||
lunchStart: '1100', | ||
lunchEnd: '1500', | ||
lunchHalfHours: 2, | ||
lunchBreakActive: false, | ||
timeConstraintActive: false, | ||
preferCompactTimetable: false, | ||
}; | ||
|
||
|
||
/** | ||
* TODO move to utils | ||
* Transforms a module's lessons into a mapping from | ||
* lessonType ==> (classNo ==> list of lessons) | ||
* The optimizer cares that a classNo contains all the slots that should be filled. | ||
* */ | ||
export function lessonByGroupsByClassNo(lessons: readonly RawLesson[]): LessonsByGroupsByClassNo { | ||
const lessonByGroups: { [lessonType: string]: readonly RawLesson[] } = groupBy( | ||
lessons, | ||
(lesson) => lesson.lessonType, | ||
); | ||
const lessonByGroupsByClassNo = mapValues(lessonByGroups, (lessonsOfSamelessonType: readonly RawLesson[]) => | ||
groupBy(lessonsOfSamelessonType, (lesson) => lesson.classNo), | ||
); | ||
return lessonByGroupsByClassNo; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// import { TimetableOutput, TimetableSmtlib2Converter } from './timetable_to_smtlib2'; | ||
import { OptimizerInputSmtlibConverter } from 'utils/optimizer/converter' | ||
import { OptimizerInput, OptimizerCallbacks, Z3Message, Z3MessageKind } from 'types/optimizer'; | ||
import Z3WebWorker from 'worker-loader!utils/optimizer/z3WebWorker'; | ||
|
||
import { | ||
DAYS, | ||
HOURS_PER_DAY, | ||
DAY_START_HOUR, | ||
DAY_END_HOUR, | ||
NUM_WEEKS, | ||
HOURS_PER_WEEK, | ||
} from 'utils/optimizer/constants' | ||
/** | ||
* The TimetableOptimizer takes a generic timetable as input and manages the lifecycle of running the | ||
* Z3 system to find a timetable solution. | ||
* Runs the web worker, receives input/output, communicates through callbacks to the calling class. | ||
* | ||
* Only one TimetableOptimizer per application - allows stateless components to run manager methods | ||
* */ | ||
export class TimetableOptimizer { | ||
static optInput: OptimizerInput; | ||
static converter: OptimizerInputSmtlibConverter; | ||
static smtString: string; | ||
static callbacks: OptimizerCallbacks; | ||
static printBuffer: string; | ||
static errBuffer: string; | ||
static worker?: Z3WebWorker = null; | ||
static completedStage1Solve: boolean; // Need to complete week-solving before timetable-solving | ||
|
||
static initOptimizer(callbacks: OptimizerCallbacks) { | ||
console.log("Starting to initialize Z3...") | ||
TimetableOptimizer.callbacks = callbacks; | ||
TimetableOptimizer.resetBuffers(); | ||
TimetableOptimizer.completedStage1Solve = false; | ||
// Set up worker if it's not set up | ||
if (!TimetableOptimizer.worker) { | ||
TimetableOptimizer.worker = new Z3WebWorker(); | ||
TimetableOptimizer.worker.onmessage = TimetableOptimizer.receiveWorkerMessage; | ||
} | ||
TimetableOptimizer.managerPostMessage(Z3MessageKind.INIT, ''); | ||
} | ||
|
||
/** | ||
* Register a generic timetable a set of callbacks to be called for different states in the Z3 solver lifecycle | ||
* */ | ||
static loadTimetable(optInput: OptimizerInput) { | ||
console.log('Loaded optimizer input'); | ||
console.log(optInput); | ||
TimetableOptimizer.optInput = optInput; | ||
TimetableOptimizer.converter = new OptimizerInputSmtlibConverter( | ||
TimetableOptimizer.optInput, | ||
NUM_WEEKS * HOURS_PER_WEEK * 2, // Number of "half-hour" slots | ||
DAY_START_HOUR, // Start at 0800 (8 am) | ||
DAY_END_HOUR /// End at 2200 (10 pm) | ||
); | ||
} | ||
|
||
static solve() { | ||
TimetableOptimizer.resetBuffers(); | ||
const weekSolveStr = TimetableOptimizer.converter.generateWeekSolveSmtLib2String(); | ||
TimetableOptimizer.managerPostMessage(Z3MessageKind.OPTIMIZE, weekSolveStr); | ||
} | ||
|
||
static receiveWorkerMessage(e: any) { | ||
const message: Z3Message = e.data; | ||
// console.log("Kind: %s, Message: %s", message.kind, message.msg) | ||
switch (message.kind) { | ||
case Z3MessageKind.INITIALIZED: | ||
// Call the initialization callback | ||
console.log("Manager initialized Z3!") | ||
TimetableOptimizer.callbacks.onOptimizerInitialized(); | ||
break; | ||
// case Z3MessageKind.PRINT: | ||
// TimetableOptimizer.printBuffer += message.msg + '\n'; | ||
// break; | ||
// case Z3MessageKind.ERR: | ||
// TimetableOptimizer.errBuffer += message.msg + '\n'; | ||
// break; | ||
// case Z3MessageKind.EXIT: | ||
// // Z3 Initialization exit | ||
// console.log('Z3 messages on exit: '); | ||
// if (TimetableOptimizer.printBuffer === '' && TimetableOptimizer.errBuffer === '') { | ||
// console.log('Premature exit - Z3 was initializing (this is normal)'); | ||
// return; // Premature exit (probably initialization) | ||
// } | ||
|
||
// // Print buffers generically | ||
// if (TimetableOptimizer.printBuffer !== '') { | ||
// console.log(TimetableOptimizer.printBuffer); | ||
// } | ||
// if (TimetableOptimizer.errBuffer !== '') { | ||
// console.error(TimetableOptimizer.errBuffer); | ||
// } | ||
|
||
// if (!TimetableOptimizer.completedStage1Solve) { | ||
// // Indicate that next time we call this callback, we have the timetable result | ||
// TimetableOptimizer.completedStage1Solve = true; | ||
// // Update the converter with the week-solve result | ||
// // TODO: enable | ||
// TimetableOptimizer.conv.update_z3_weeksolve_output(TimetableOptimizer.printBuffer); | ||
// // Generate the SMTLIB2 string based on the week-solve:w | ||
// TimetableOptimizer.smtString = TimetableOptimizer.conv.generateTimetableSolveSmtLib2String(); | ||
// // Run callback to update the generated smtlib2 string | ||
// TimetableOptimizer.callbacks.onSmtlib2InputCreated(TimetableOptimizer.smtString); | ||
// // Reset state for our next optimization run | ||
// TimetableOptimizer.resetBuffers(); | ||
// // Two stage solve: first solve for the week constraints, then solve for the actual timetable | ||
// TimetableOptimizer.managerPostMessage(Z3MessageKind.OPTIMIZE, TimetableOptimizer.smtString); | ||
// } else { | ||
// // Reset solve state | ||
// TimetableOptimizer.completedStage1Solve = false; | ||
// // Deal with real solve state | ||
// // Call the output callback | ||
// TimetableOptimizer.callbacks.onOutput( | ||
// TimetableOptimizer.printBuffer + '\n' + TimetableOptimizer.errBuffer | ||
// ); | ||
// // Process the output text we just got from the Z3 solver | ||
// const timetable: TimetableOutput = TimetableOptimizer.conv.z3_output_to_timetable( | ||
// TimetableOptimizer.printBuffer | ||
// ); | ||
// TimetableOptimizer.callbacks.onTimetableOutput(timetable); | ||
// } | ||
|
||
// break; | ||
default: | ||
break; | ||
} | ||
} | ||
|
||
/** | ||
* Generically post a message to the worker | ||
* */ | ||
static managerPostMessage(kind: Z3MessageKind, msg: string) { | ||
const message: Z3Message = { kind, msg }; | ||
TimetableOptimizer.worker.postMessage(message); | ||
} | ||
|
||
static resetBuffers() { | ||
TimetableOptimizer.printBuffer = ''; | ||
TimetableOptimizer.errBuffer = ''; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.