Return all top-scoring solver configurations and display as tabs
This commit is contained in:
87
main.go
87
main.go
@@ -1152,7 +1152,7 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
|
|||||||
|
|
||||||
if len(studentIDs) == 0 {
|
if len(studentIDs) == 0 {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]any{"rooms": []any{}, "score": 0})
|
json.NewEncoder(w).Encode(map[string]any{"solutions": []any{}})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1360,9 +1360,47 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
|
|||||||
initialAssignment := make([]int, n)
|
initialAssignment := make([]int, n)
|
||||||
copy(initialAssignment, assignment)
|
copy(initialAssignment, assignment)
|
||||||
|
|
||||||
bestAssignment := make([]int, n)
|
|
||||||
copy(bestAssignment, assignment)
|
|
||||||
bestScore := score(assignment)
|
bestScore := score(assignment)
|
||||||
|
var bestSolutions [][]int
|
||||||
|
seen := map[string]bool{}
|
||||||
|
normalizeKey := func(a []int) string {
|
||||||
|
rm := map[int][]int{}
|
||||||
|
for i, room := range a {
|
||||||
|
rm[room] = append(rm[room], i)
|
||||||
|
}
|
||||||
|
var gs [][]int
|
||||||
|
for _, members := range rm {
|
||||||
|
slices.Sort(members)
|
||||||
|
gs = append(gs, members)
|
||||||
|
}
|
||||||
|
slices.SortFunc(gs, func(a, b []int) int { return a[0] - b[0] })
|
||||||
|
var buf strings.Builder
|
||||||
|
for _, g := range gs {
|
||||||
|
for i, m := range g {
|
||||||
|
if i > 0 {
|
||||||
|
buf.WriteByte(',')
|
||||||
|
}
|
||||||
|
buf.WriteString(strconv.Itoa(m))
|
||||||
|
}
|
||||||
|
buf.WriteByte(';')
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
addSolution := func(a []int, s int) {
|
||||||
|
if s > bestScore {
|
||||||
|
bestScore = s
|
||||||
|
bestSolutions = nil
|
||||||
|
seen = map[string]bool{}
|
||||||
|
}
|
||||||
|
if s == bestScore {
|
||||||
|
key := normalizeKey(a)
|
||||||
|
if !seen[key] {
|
||||||
|
seen[key] = true
|
||||||
|
bestSolutions = append(bestSolutions, slices.Clone(a))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addSolution(assignment, bestScore)
|
||||||
|
|
||||||
roomCount := func(a []int, room int) int {
|
roomCount := func(a []int, room int) int {
|
||||||
c := 0
|
c := 0
|
||||||
@@ -1498,37 +1536,32 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
copy(assignment, initialAssignment)
|
copy(assignment, initialAssignment)
|
||||||
s := hillClimb(assignment)
|
addSolution(assignment, hillClimb(assignment))
|
||||||
if s > bestScore {
|
|
||||||
bestScore = s
|
|
||||||
copy(bestAssignment, assignment)
|
|
||||||
}
|
|
||||||
|
|
||||||
for range 30 {
|
for range 30 {
|
||||||
if randomPlacement() {
|
if randomPlacement() {
|
||||||
s := hillClimb(assignment)
|
addSolution(assignment, hillClimb(assignment))
|
||||||
if s > bestScore {
|
|
||||||
bestScore = s
|
|
||||||
copy(bestAssignment, assignment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for range 200 {
|
for range 200 {
|
||||||
perturb(bestAssignment, 2+rand.Intn(3))
|
src := bestSolutions[rand.Intn(len(bestSolutions))]
|
||||||
s := hillClimb(assignment)
|
perturb(src, 2+rand.Intn(3))
|
||||||
if s > bestScore {
|
addSolution(assignment, hillClimb(assignment))
|
||||||
bestScore = s
|
|
||||||
copy(bestAssignment, assignment)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type roomMember struct {
|
type roomMember struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
type solutionResult struct {
|
||||||
|
Rooms [][]roomMember `json:"rooms"`
|
||||||
|
Score int `json:"score"`
|
||||||
|
}
|
||||||
|
var results []solutionResult
|
||||||
|
for _, sol := range bestSolutions {
|
||||||
roomMap := map[int][]roomMember{}
|
roomMap := map[int][]roomMember{}
|
||||||
for i, room := range bestAssignment {
|
for i, room := range sol {
|
||||||
sid := studentIDs[i]
|
sid := studentIDs[i]
|
||||||
roomMap[room] = append(roomMap[room], roomMember{ID: sid, Name: studentName[sid]})
|
roomMap[room] = append(roomMap[room], roomMember{ID: sid, Name: studentName[sid]})
|
||||||
}
|
}
|
||||||
@@ -1540,8 +1573,20 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slices.SortFunc(rooms, func(a, b []roomMember) int { return strings.Compare(a[0].Name, b[0].Name) })
|
slices.SortFunc(rooms, func(a, b []roomMember) int { return strings.Compare(a[0].Name, b[0].Name) })
|
||||||
|
results = append(results, solutionResult{Rooms: rooms, Score: bestScore})
|
||||||
|
}
|
||||||
|
slices.SortFunc(results, func(a, b solutionResult) int {
|
||||||
|
for i := range min(len(a.Rooms), len(b.Rooms)) {
|
||||||
|
for j := range min(len(a.Rooms[i]), len(b.Rooms[i])) {
|
||||||
|
if c := strings.Compare(a.Rooms[i][j].Name, b.Rooms[i][j].Name); c != 0 {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]any{"rooms": rooms, "score": bestScore})
|
json.NewEncoder(w).Encode(map[string]any{"solutions": results})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -422,7 +422,9 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
const result = await api('POST', '/api/trips/' + tripID + '/solve');
|
const result = await api('POST', '/api/trips/' + tripID + '/solve');
|
||||||
const container = document.getElementById('solver-results');
|
const container = document.getElementById('solver-results');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
for (let i = 0; i < result.rooms.length; i++) {
|
|
||||||
|
const renderSolution = (sol, parent) => {
|
||||||
|
for (let i = 0; i < sol.rooms.length; i++) {
|
||||||
const card = document.createElement('wa-card');
|
const card = document.createElement('wa-card');
|
||||||
card.className = 'room-card';
|
card.className = 'room-card';
|
||||||
const label = document.createElement('div');
|
const label = document.createElement('div');
|
||||||
@@ -431,10 +433,10 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
card.appendChild(label);
|
card.appendChild(label);
|
||||||
const tags = document.createElement('div');
|
const tags = document.createElement('div');
|
||||||
tags.className = 'tags';
|
tags.className = 'tags';
|
||||||
const roomIDs = result.rooms[i].map(m => m.id);
|
const roomIDs = sol.rooms[i].map(m => m.id);
|
||||||
const violations = [];
|
const violations = [];
|
||||||
for (const a of result.rooms[i]) {
|
for (const a of sol.rooms[i]) {
|
||||||
for (const b of result.rooms[i]) {
|
for (const b of sol.rooms[i]) {
|
||||||
if (a.id === b.id) continue;
|
if (a.id === b.id) continue;
|
||||||
const eff = lastOveralls[a.id]?.[b.id];
|
const eff = lastOveralls[a.id]?.[b.id];
|
||||||
if (eff && eff.kind === 'prefer_not') {
|
if (eff && eff.kind === 'prefer_not') {
|
||||||
@@ -442,7 +444,7 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const member of result.rooms[i]) {
|
for (const member of sol.rooms[i]) {
|
||||||
const tag = document.createElement('wa-tag');
|
const tag = document.createElement('wa-tag');
|
||||||
tag.size = 'small';
|
tag.size = 'small';
|
||||||
tag.style.cursor = 'pointer';
|
tag.style.cursor = 'pointer';
|
||||||
@@ -453,11 +455,11 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
else if (hasPrefers && !gotPrefer) tag.variant = 'warning';
|
else if (hasPrefers && !gotPrefer) tag.variant = 'warning';
|
||||||
tag.textContent = member.name;
|
tag.textContent = member.name;
|
||||||
tag.addEventListener('click', () => {
|
tag.addEventListener('click', () => {
|
||||||
const card = document.querySelector('[data-student-id="' + member.id + '"]');
|
const studentCard = document.querySelector('[data-student-id="' + member.id + '"]');
|
||||||
if (!card) return;
|
if (!studentCard) return;
|
||||||
const cDet = [...card.querySelectorAll('wa-details')].find(d => d.summary === 'Constraints');
|
const cDet = [...studentCard.querySelectorAll('wa-details')].find(d => d.summary === 'Constraints');
|
||||||
if (cDet) cDet.open = true;
|
if (cDet) cDet.open = true;
|
||||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
studentCard.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
});
|
});
|
||||||
tags.appendChild(tag);
|
tags.appendChild(tag);
|
||||||
}
|
}
|
||||||
@@ -471,11 +473,33 @@ document.getElementById('solve-btn').addEventListener('click', async () => {
|
|||||||
} else {
|
} else {
|
||||||
card.appendChild(tags);
|
card.appendChild(tags);
|
||||||
}
|
}
|
||||||
container.appendChild(card);
|
parent.appendChild(card);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const solutions = result.solutions;
|
||||||
|
if (solutions.length === 1) {
|
||||||
|
renderSolution(solutions[0], container);
|
||||||
|
} else if (solutions.length > 1) {
|
||||||
|
const tabGroup = document.createElement('wa-tab-group');
|
||||||
|
for (let si = 0; si < solutions.length; si++) {
|
||||||
|
const tab = document.createElement('wa-tab');
|
||||||
|
tab.slot = 'nav';
|
||||||
|
tab.panel = 'sol-' + si;
|
||||||
|
tab.textContent = 'Option ' + (si + 1);
|
||||||
|
tabGroup.appendChild(tab);
|
||||||
|
}
|
||||||
|
for (let si = 0; si < solutions.length; si++) {
|
||||||
|
const panel = document.createElement('wa-tab-panel');
|
||||||
|
panel.name = 'sol-' + si;
|
||||||
|
renderSolution(solutions[si], panel);
|
||||||
|
tabGroup.appendChild(panel);
|
||||||
|
}
|
||||||
|
container.appendChild(tabGroup);
|
||||||
}
|
}
|
||||||
const scoreDiv = document.createElement('div');
|
const scoreDiv = document.createElement('div');
|
||||||
scoreDiv.className = 'solver-score';
|
scoreDiv.className = 'solver-score';
|
||||||
scoreDiv.textContent = 'Score: ' + result.score;
|
scoreDiv.textContent = 'Score: ' + (solutions[0]?.score ?? 0) + (solutions.length > 1 ? ' (' + solutions.length + ' options)' : '');
|
||||||
container.appendChild(scoreDiv);
|
container.appendChild(scoreDiv);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const container = document.getElementById('solver-results');
|
const container = document.getElementById('solver-results');
|
||||||
|
|||||||
Reference in New Issue
Block a user