Extract solver into library package with delta-scoring optimization
This commit is contained in:
339
cmd/solver-tune/main.go
Normal file
339
cmd/solver-tune/main.go
Normal file
@@ -0,0 +1,339 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"rooms/solver"
|
||||
)
|
||||
|
||||
type tripData struct {
|
||||
RoomSize int `json:"room_size"`
|
||||
PreferNotMultiple int `json:"prefer_not_multiple"`
|
||||
NoPreferCost int `json:"no_prefer_cost"`
|
||||
}
|
||||
|
||||
type studentData struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type constraintsData struct {
|
||||
Overalls []struct {
|
||||
StudentAID int64 `json:"student_a_id"`
|
||||
StudentBID int64 `json:"student_b_id"`
|
||||
Kind string `json:"kind"`
|
||||
} `json:"overalls"`
|
||||
}
|
||||
|
||||
func normalizeKey(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()
|
||||
}
|
||||
|
||||
type runResult struct {
|
||||
score int
|
||||
solutions [][]int
|
||||
elapsed time.Duration
|
||||
}
|
||||
|
||||
func printStats(label string, results []runResult, runs int) {
|
||||
scores := map[int]int{}
|
||||
solutionSets := map[string]int{}
|
||||
var totalTime time.Duration
|
||||
var totalSolutions int
|
||||
|
||||
for _, r := range results {
|
||||
totalTime += r.elapsed
|
||||
scores[r.score]++
|
||||
totalSolutions += len(r.solutions)
|
||||
for _, sol := range r.solutions {
|
||||
key := normalizeKey(sol)
|
||||
solutionSets[key]++
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("--- %s ---\n", label)
|
||||
fmt.Printf(" avg time: %v\n", totalTime/time.Duration(runs))
|
||||
|
||||
var scoreList []struct {
|
||||
score int
|
||||
count int
|
||||
}
|
||||
for s, c := range scores {
|
||||
scoreList = append(scoreList, struct {
|
||||
score int
|
||||
count int
|
||||
}{s, c})
|
||||
}
|
||||
sort.Slice(scoreList, func(i, j int) bool { return scoreList[i].score > scoreList[j].score })
|
||||
|
||||
fmt.Printf(" score distribution:\n")
|
||||
for _, sc := range scoreList {
|
||||
fmt.Printf(" score %d: %d/%d runs (%.0f%%)\n", sc.score, sc.count, runs, float64(sc.count)/float64(runs)*100)
|
||||
}
|
||||
|
||||
fmt.Printf(" unique solutions seen: %d\n", len(solutionSets))
|
||||
fmt.Printf(" avg solutions per run: %.1f\n", float64(totalSolutions)/float64(runs))
|
||||
|
||||
var solFreqs []struct {
|
||||
key string
|
||||
count int
|
||||
}
|
||||
for k, c := range solutionSets {
|
||||
solFreqs = append(solFreqs, struct {
|
||||
key string
|
||||
count int
|
||||
}{k, c})
|
||||
}
|
||||
sort.Slice(solFreqs, func(i, j int) bool { return solFreqs[i].count > solFreqs[j].count })
|
||||
|
||||
stableCount := 0
|
||||
for _, sf := range solFreqs {
|
||||
if sf.count == runs {
|
||||
stableCount++
|
||||
}
|
||||
}
|
||||
fmt.Printf(" solutions found in all runs: %d\n", stableCount)
|
||||
if len(solFreqs) > 0 {
|
||||
topN := min(5, len(solFreqs))
|
||||
fmt.Printf(" top %d solution frequencies: ", topN)
|
||||
for i := range topN {
|
||||
if i > 0 {
|
||||
fmt.Print(", ")
|
||||
}
|
||||
fmt.Printf("%d/%d", solFreqs[i].count, runs)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func main() {
|
||||
dir := flag.String("dir", "tmp", "directory with trip/students/constraints JSON files")
|
||||
runs := flag.Int("runs", 20, "number of solver runs per parameter set")
|
||||
algo := flag.String("algo", "both", "algorithm: hillclimb, fast, sa, hybrid, both, all")
|
||||
numRandom := flag.String("random", "30", "comma-separated random placement counts (hillclimb)")
|
||||
numPerturb := flag.String("perturb", "200", "comma-separated perturbation counts (hillclimb)")
|
||||
perturbMin := flag.Int("pmin", 2, "perturbation min groups (hillclimb)")
|
||||
perturbMax := flag.Int("pmax", 5, "perturbation max groups (hillclimb)")
|
||||
saRestarts := flag.String("restarts", "20", "comma-separated SA/hybrid restart counts")
|
||||
saSteps := flag.String("steps", "10000", "comma-separated SA step counts")
|
||||
hybridSteps := flag.String("hsteps", "5000", "comma-separated hybrid SA step counts")
|
||||
saTempHigh := flag.Float64("thigh", 5.0, "SA initial temperature")
|
||||
saTempLow := flag.Float64("tlow", 0.01, "SA final temperature")
|
||||
hybridTempHigh := flag.Float64("hthigh", 10.0, "hybrid SA initial temperature")
|
||||
hybridTempLow := flag.Float64("htlow", 0.1, "hybrid SA final temperature")
|
||||
flag.Parse()
|
||||
|
||||
tripBytes, err := os.ReadFile(*dir + "/1")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "reading trip: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var trip tripData
|
||||
json.Unmarshal(tripBytes, &trip)
|
||||
|
||||
studentsBytes, err := os.ReadFile(*dir + "/students")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "reading students: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var students []studentData
|
||||
json.Unmarshal(studentsBytes, &students)
|
||||
|
||||
constraintsBytes, err := os.ReadFile(*dir + "/constraints")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "reading constraints: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
var cd constraintsData
|
||||
json.Unmarshal(constraintsBytes, &cd)
|
||||
|
||||
idx := map[int64]int{}
|
||||
for i, s := range students {
|
||||
idx[s.ID] = i
|
||||
}
|
||||
n := len(students)
|
||||
|
||||
var constraints []solver.Constraint
|
||||
for _, o := range cd.Overalls {
|
||||
ai, aOk := idx[o.StudentAID]
|
||||
bi, bOk := idx[o.StudentBID]
|
||||
if !aOk || !bOk {
|
||||
continue
|
||||
}
|
||||
constraints = append(constraints, solver.Constraint{
|
||||
StudentA: ai,
|
||||
StudentB: bi,
|
||||
Kind: o.Kind,
|
||||
})
|
||||
}
|
||||
|
||||
fmt.Printf("Students: %d, Room size: %d, Constraints: %d\n", n, trip.RoomSize, len(constraints))
|
||||
fmt.Printf("Prefer Not multiple: %d, No Prefer cost: %d\n", trip.PreferNotMultiple, trip.NoPreferCost)
|
||||
fmt.Printf("Runs per config: %d\n\n", *runs)
|
||||
|
||||
if *algo == "hillclimb" || *algo == "both" {
|
||||
randomCounts := parseIntList(*numRandom)
|
||||
perturbCounts := parseIntList(*numPerturb)
|
||||
for _, nr := range randomCounts {
|
||||
for _, np := range perturbCounts {
|
||||
params := solver.Params{
|
||||
NumRandom: nr,
|
||||
NumPerturb: np,
|
||||
PerturbMin: *perturbMin,
|
||||
PerturbMax: *perturbMax,
|
||||
}
|
||||
var results []runResult
|
||||
for run := range *runs {
|
||||
rng := rand.New(rand.NewSource(int64(run * 31337)))
|
||||
start := time.Now()
|
||||
sols := solver.Solve(n, trip.RoomSize, trip.PreferNotMultiple, trip.NoPreferCost, constraints, params, rng)
|
||||
elapsed := time.Since(start)
|
||||
if len(sols) > 0 {
|
||||
var assignments [][]int
|
||||
for _, s := range sols {
|
||||
assignments = append(assignments, s.Assignment)
|
||||
}
|
||||
results = append(results, runResult{sols[0].Score, assignments, elapsed})
|
||||
}
|
||||
}
|
||||
label := fmt.Sprintf("hillclimb random=%d perturb=%d pmin=%d pmax=%d", nr, np, *perturbMin, *perturbMax)
|
||||
printStats(label, results, *runs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *algo == "fast" || *algo == "both" || *algo == "all" {
|
||||
randomCounts := parseIntList(*numRandom)
|
||||
perturbCounts := parseIntList(*numPerturb)
|
||||
for _, nr := range randomCounts {
|
||||
for _, np := range perturbCounts {
|
||||
params := solver.Params{
|
||||
NumRandom: nr,
|
||||
NumPerturb: np,
|
||||
PerturbMin: *perturbMin,
|
||||
PerturbMax: *perturbMax,
|
||||
}
|
||||
var results []runResult
|
||||
for run := range *runs {
|
||||
rng := rand.New(rand.NewSource(int64(run * 31337)))
|
||||
start := time.Now()
|
||||
sols := solver.SolveFast(n, trip.RoomSize, trip.PreferNotMultiple, trip.NoPreferCost, constraints, params, rng)
|
||||
elapsed := time.Since(start)
|
||||
if len(sols) > 0 {
|
||||
var assignments [][]int
|
||||
for _, s := range sols {
|
||||
assignments = append(assignments, s.Assignment)
|
||||
}
|
||||
results = append(results, runResult{sols[0].Score, assignments, elapsed})
|
||||
}
|
||||
}
|
||||
label := fmt.Sprintf("fast random=%d perturb=%d pmin=%d pmax=%d", nr, np, *perturbMin, *perturbMax)
|
||||
printStats(label, results, *runs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *algo == "sa" || *algo == "all" {
|
||||
restartCounts := parseIntList(*saRestarts)
|
||||
stepCounts := parseIntList(*saSteps)
|
||||
for _, nr := range restartCounts {
|
||||
for _, ns := range stepCounts {
|
||||
params := solver.SAParams{
|
||||
Restarts: nr,
|
||||
Steps: ns,
|
||||
TempHigh: *saTempHigh,
|
||||
TempLow: *saTempLow,
|
||||
}
|
||||
var results []runResult
|
||||
for run := range *runs {
|
||||
rng := rand.New(rand.NewSource(int64(run * 31337)))
|
||||
start := time.Now()
|
||||
sols := solver.SolveSA(n, trip.RoomSize, trip.PreferNotMultiple, trip.NoPreferCost, constraints, params, rng)
|
||||
elapsed := time.Since(start)
|
||||
if len(sols) > 0 {
|
||||
var assignments [][]int
|
||||
for _, s := range sols {
|
||||
assignments = append(assignments, s.Assignment)
|
||||
}
|
||||
results = append(results, runResult{sols[0].Score, assignments, elapsed})
|
||||
}
|
||||
}
|
||||
label := fmt.Sprintf("sa restarts=%d steps=%d thigh=%.1f tlow=%.3f", nr, ns, *saTempHigh, *saTempLow)
|
||||
printStats(label, results, *runs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *algo == "hybrid" || *algo == "both" || *algo == "all" {
|
||||
restartCounts := parseIntList(*saRestarts)
|
||||
stepCounts := parseIntList(*hybridSteps)
|
||||
for _, nr := range restartCounts {
|
||||
for _, ns := range stepCounts {
|
||||
params := solver.HybridParams{
|
||||
SARestarts: nr,
|
||||
SASteps: ns,
|
||||
TempHigh: *hybridTempHigh,
|
||||
TempLow: *hybridTempLow,
|
||||
}
|
||||
var results []runResult
|
||||
for run := range *runs {
|
||||
rng := rand.New(rand.NewSource(int64(run * 31337)))
|
||||
start := time.Now()
|
||||
sols := solver.SolveHybrid(n, trip.RoomSize, trip.PreferNotMultiple, trip.NoPreferCost, constraints, params, rng)
|
||||
elapsed := time.Since(start)
|
||||
if len(sols) > 0 {
|
||||
var assignments [][]int
|
||||
for _, s := range sols {
|
||||
assignments = append(assignments, s.Assignment)
|
||||
}
|
||||
results = append(results, runResult{sols[0].Score, assignments, elapsed})
|
||||
}
|
||||
}
|
||||
label := fmt.Sprintf("hybrid restarts=%d steps=%d thigh=%.1f tlow=%.3f", nr, ns, *hybridTempHigh, *hybridTempLow)
|
||||
printStats(label, results, *runs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseIntList(s string) []int {
|
||||
parts := strings.Split(s, ",")
|
||||
var result []int
|
||||
for _, p := range parts {
|
||||
v, err := strconv.Atoi(strings.TrimSpace(p))
|
||||
if err == nil {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user