Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ executors:
docker:
- image: cimg/node:18.20.5

go-executor:
docker:
- image: cimg/go:1.23

jobs:
checkout:
executor: node-executor
Expand Down Expand Up @@ -110,6 +114,37 @@ jobs:
- store_artifacts:
path: dist

optimiser:
executor: go-executor
working_directory: ~/nusmods/website/api/optimiser
steps:
- attach_workspace:
at: ~/nusmods
- run:
name: Prepare venues data
command: cp ../../src/data/venues.json _constants/venues.json
- run:
name: Install golangci-lint
command: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.7.2
- run:
name: Check linting
command: $(go env GOPATH)/bin/golangci-lint run --timeout=5m
- run:
name: Check formatting
command: |
if [ -n "$($(go env GOPATH)/bin/golangci-lint fmt --diff)" ]; then
echo "Formatting issues found. Run 'golangci-lint fmt' locally to fix."
exit 1
fi
- run:
name: Run tests
command: |
go run ./_test/server/main.go -port 8020 &
SERVER_PID=$!
sleep 5
go test ./_test/. -v
kill $SERVER_PID

workflows:
build_and_test:
jobs:
Expand All @@ -123,6 +158,9 @@ workflows:
- export:
requires:
- checkout
- optimiser:
requires:
- checkout
- website:
requires:
- checkout
471 changes: 471 additions & 0 deletions website/api/optimiser/.golangci.yaml

Large diffs are not rendered by default.

99 changes: 81 additions & 18 deletions website/api/optimiser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ The optimiser uses a **Beam Search algorithm** to efficiently explore the vast s
2. **Beam Width**: Maintains top N (=100) most promising states at each step (=2500) (configurable)
3. **Branching Factor**: Limits the number of options considered per lesson type (=100) (configurable)
4. **Scoring Function**: Evaluates states based on:
- Total walking distance between consecutive classes
- Lunch break timing
- Total walking distance between consecutive classes using haversine formula
- Having a one-hour break within provided lunch time window
- <= Maximum hours of consecutive live lessons
- <= 2 hours max gap between classes (configurable)

## API Reference
Expand All @@ -56,6 +57,7 @@ The optimiser uses a **Beam Search algorithm** to efficiently explore the vast s
"modules": ["CS1010S", "CS2030S", "MA1521"],
"recordings": ["CS1010S Lecture", "CS2030S Laboratory"],
"freeDays": ["Monday", "Friday"],
"maxConsecutiveHours": 4,
"earliestTime": "0900",
"latestTime": "1800",
"acadYear": "2024-2025",
Expand All @@ -77,7 +79,51 @@ The optimiser uses a **Beam Search algorithm** to efficiently explore the vast s
"MA1521|Tutorial": "01"
},
"DaySlots": [
[ /* Monday slots */ ],
[ /* Monday slots */
{
"classNo": "05",
"day": "Monday",
"endTime": "1600",
"lessonType": "Laboratory",
"startTime": "1400",
"venue": "COM1-B108",
"coordinates": {
"x": 103.773994,
"y": 1.2948803
},
"weeks": [
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13
],
"StartMin": 840,
"EndMin": 960,
"DayIndex": 0,
"LessonKey": "CS2040S|Laboratory",
"WeeksSet": {
"10": true,
"11": true,
"12": true,
"13": true,
"3": true,
"4": true,
"5": true,
"6": true,
"7": true,
"8": true,
"9": true
},
"WeeksString": "3,4,5,6,7,8,9,10,11,12,13"
},
],
[ /* Tuesday slots */ ],
[ /* Wednesday slots */ ],
[ /* Thursday slots */ ],
Expand All @@ -99,17 +145,18 @@ The optimiser uses a **Beam Search algorithm** to efficiently explore the vast s

#### Parameters

| Field | Type | Description |
| -------------- | ---------- | -------------------------------------------------------------------------------------- |
| `modules` | `[]string` | Module codes to include in optimisation in Upper case (e.g. "CS1010S") |
| `recordings` | `[]string` | Lessons marked as recorded/online (format: "MODULE LessonType") e.g. "CS1010S Lecture" |
| `freeDays` | `[]string` | Days to keep free of physical classes e.g. "Monday" |
| `earliestTime` | `string` | Earliest acceptable class time (HHMM format) |
| `latestTime` | `string` | Latest acceptable class time (HHMM format) |
| `acadYear` | `string` | Academic year (format: "YYYY-YYYY") e.g. "2024-2025" |
| `acadSem` | `int` | Semester number (1 or 2) |
| `lunchStart` | `string` | Preferred lunch break start time (HHMM) |
| `lunchEnd` | `string` | Preferred lunch break end time (HHMM) |
| Field | Type | Description |
| --------------------- | ---------- | -------------------------------------------------------------------------------------- |
| `modules` | `[]string` | Module codes to include in optimisation in Upper case (e.g. "CS1010S") |
| `recordings` | `[]string` | Lessons marked as recorded/online (format: "MODULE LessonType") e.g. "CS1010S Lecture" |
| `freeDays` | `[]string` | Days to keep free of physical classes e.g. "Monday" |
| `earliestTime` | `string` | Earliest acceptable class time (HHMM format) |
| `latestTime` | `string` | Latest acceptable class time (HHMM format) |
| `acadYear` | `string` | Academic year (format: "YYYY-YYYY") e.g. "2024-2025" |
| `acadSem` | `int` | Semester number (1 or 2) |
| `lunchStart` | `string` | Preferred lunch break start time (HHMM) |
| `lunchEnd` | `string` | Preferred lunch break end time (HHMM) |
| `maxConsecutiveHours` | `int` | Maximum consecutive live lesson hours allowed |

## Getting Started

Expand All @@ -130,17 +177,33 @@ The optimiser uses a **Beam Search algorithm** to efficiently explore the vast s
go mod tidy
```

3. **Update the API in the frontend (change back after testing)**
- In `website/src/apis/optimiser.ts`, change the api from `/api/optimiser/optimise` to `http://localhost:8020/optimise`
3. **Run test server**
```bash
yarn start:optimiser
```

4. **Run test server**
4. **Run frontend**(if needed)
```bash
yarn start:optimiser -port 8020
yarn start:local
```

5. **Test the API**
- Send a POST request following the request body format above to `http://localhost:8020/optimise`

## Linting and Formatting
- Lint the code using:
```bash
golangci-lint run
```
**Auto fix issues where possible:**
```bash
golangci-lint run --fix
```
- Format the code using:
```bash
golangci-lint fmt
```
- The golangci-lint configuration is defined in `.golangci.yaml`

## Dependencies

Expand Down
2 changes: 1 addition & 1 deletion website/api/optimiser/_models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type ModuleSlot struct {
StartTime string `json:"startTime"`
Venue string `json:"venue"`
Coordinates Coordinates `json:"coordinates"`
Weeks any `json:"weeks"`
Weeks any `json:"weeks"`

// Parsed fields
StartMin int // Minutes from 00:00 (e.g., 540 for 09:00)
Expand Down
11 changes: 9 additions & 2 deletions website/api/optimiser/_modules/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import (
- Get all module slots that pass conditions in optimiserRequest for all modules.
- Reduces search space by merging slots of the same lesson type happening at the same day and time and building.
*/
func GetAllModuleSlots(optimiserRequest models.OptimiserRequest) (map[string]map[string]map[string][]models.ModuleSlot, error) {
func GetAllModuleSlots(
optimiserRequest models.OptimiserRequest,
) (map[string]map[string]map[string][]models.ModuleSlot, error) {
venues, err := client.GetVenues()
if err != nil {
return nil, err
Expand Down Expand Up @@ -78,7 +80,12 @@ func GetAllModuleSlots(optimiserRequest models.OptimiserRequest) (map[string]map
return moduleSlots, nil
}

func mergeAndFilterModuleSlots(timetable []models.ModuleSlot, venues map[string]models.Location, optimiserRequest models.OptimiserRequest, module string) map[string]map[string][]models.ModuleSlot {
func mergeAndFilterModuleSlots(
timetable []models.ModuleSlot,
venues map[string]models.Location,
optimiserRequest models.OptimiserRequest,
module string,
) map[string]map[string][]models.ModuleSlot {

recordingsMap := make(map[string]bool, len(optimiserRequest.Recordings))
for _, recording := range optimiserRequest.Recordings {
Expand Down
Loading