Skip to content

Commit 733770c

Browse files
committed
Optimize fitness evaluation with delta-based computation and improved hash
Two algorithm-level optimizations: 1. Delta-based fitness evaluation: When evaluating mutated population members, instead of iterating all N triangles and checking a hash cache, track which triangles are added/removed during Delaunay Insert/Remove and compute fitness incrementally from the base's known totals. This reduces per-evaluation work from O(N) to O(k) where k is the number of affected triangles (~6-12 per mutation vs ~400 total for a typical 200-point configuration). 2. Improved triangle cache hash function: Replace the weak commutative hash (sum of coordinates) with a polynomial hash using distinct prime multipliers per coordinate, reducing cache collisions and unnecessary rasterization on the full-evaluation path. Changes: - Add change tracking (BeginTrack/EndTrack) to Delaunay triangulation - Add triangleKey type with normalized vertex ordering for variance map - Implement dual-path Calculate(): delta path for mutations, full path for initial/base evaluations - Propagate base fitness data (difference, area, variance map) via SetBase - Use counter-based net-change computation to handle intermediate triangles created during Remove/Insert sequences https://claude.ai/code/session_01EQzoAqbMNJ22TpcMNRU31d
1 parent a1f8f58 commit 733770c

4 files changed

Lines changed: 471 additions & 52 deletions

File tree

fitness/cache.go

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
package fitness
22

3+
// triangleKey is a normalized representation of a triangle's vertices, used as a map key
4+
// for delta-based fitness evaluation. Vertices are sorted lexicographically (by X then Y)
5+
// so the same geometric triangle always maps to the same key.
6+
type triangleKey struct {
7+
aX, aY, bX, bY, cX, cY int16
8+
}
9+
10+
// makeTriangleKey creates a triangleKey with vertices sorted lexicographically.
11+
func makeTriangleKey(ax, ay, bx, by, cx, cy int16) triangleKey {
12+
// Sort three vertices by X, then Y
13+
if ax > bx || (ax == bx && ay > by) {
14+
ax, ay, bx, by = bx, by, ax, ay
15+
}
16+
if bx > cx || (bx == cx && by > cy) {
17+
bx, by, cx, cy = cx, cy, bx, by
18+
}
19+
if ax > bx || (ax == bx && ay > by) {
20+
ax, ay, bx, by = bx, by, ax, ay
21+
}
22+
return triangleKey{aX: ax, aY: ay, bX: bx, bY: by, cX: cx, cY: cy}
23+
}
24+
25+
// triangleVarData stores per-triangle variance and area for delta-based fitness.
26+
type triangleVarData struct {
27+
variance float64
28+
area float64
29+
}
30+
331
// A CacheFunction represents a fitness function that caches data for efficiency.
432
type CacheFunction interface {
533
Function
@@ -39,11 +67,17 @@ func (t TriangleCacheData) Equals(other CacheData) bool {
3967
}
4068

4169
// Hash calculates the hash code of a TriangleCacheData.
70+
// Uses a polynomial hash with distinct prime multipliers per coordinate
71+
// to reduce collisions compared to the previous commutative sum-based hash.
4272
func (t TriangleCacheData) Hash() uint64 {
43-
x := int(t.aX) + int(t.bX) + int(t.cX)
44-
y := int(t.aY) + int(t.bY) + int(t.cY)
45-
46-
return uint64((97+x)*97 + y)
73+
h := uint64(17)
74+
h = h*31 + uint64(uint16(t.aX))
75+
h = h*37 + uint64(uint16(t.aY))
76+
h = h*41 + uint64(uint16(t.bX))
77+
h = h*43 + uint64(uint16(t.bY))
78+
h = h*47 + uint64(uint16(t.cX))
79+
h = h*53 + uint64(uint16(t.cY))
80+
return h
4781
}
4882

4983
func (t TriangleCacheData) CachedHash() uint32 {

fitness/delta_test.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package fitness
2+
3+
import (
4+
image2 "github.com/RH12503/Triangula/image"
5+
"github.com/RH12503/Triangula/mutation"
6+
"github.com/RH12503/Triangula/normgeom"
7+
"github.com/RH12503/Triangula/random"
8+
"image"
9+
"image/color"
10+
"math"
11+
"testing"
12+
)
13+
14+
// TestDeltaFitnessMatchesFull verifies that delta-based fitness evaluation produces
15+
// the same result as a full evaluation for the same set of points.
16+
func TestDeltaFitnessMatchesFull(t *testing.T) {
17+
random.Seed(42)
18+
19+
const w, h = 80, 80
20+
const bs = 3
21+
22+
// Create a random target image
23+
img := image.NewRGBA(image.Rect(0, 0, w, h))
24+
for x := 0; x < w; x++ {
25+
for y := 0; y < h; y++ {
26+
img.Set(x, y, color.RGBA{
27+
R: uint8(random.Intn(math.MaxUint8)),
28+
G: uint8(random.Intn(math.MaxUint8)),
29+
B: uint8(random.Intn(math.MaxUint8)),
30+
A: math.MaxUint8,
31+
})
32+
}
33+
}
34+
35+
target := image2.ToData(img)
36+
37+
// Create base points
38+
basePoints := normgeom.NormPointGroup{
39+
{0.1, 0.1},
40+
{0.9, 0.1},
41+
{0.5, 0.5},
42+
{0.1, 0.9},
43+
{0.9, 0.9},
44+
{0.3, 0.3},
45+
{0.7, 0.7},
46+
{0.2, 0.6},
47+
{0.8, 0.4},
48+
{0.5, 0.2},
49+
}
50+
51+
// Compute full fitness of the base points
52+
baseEval := NewTrianglesImageFunction(target, bs)
53+
baseFitness := baseEval.Calculate(PointsData{
54+
Points: basePoints,
55+
Mutations: nil,
56+
})
57+
t.Logf("Base fitness: %v", baseFitness)
58+
59+
// Create mutated points: move point 2 and point 5
60+
mutatedPoints := basePoints.Copy()
61+
oldP2 := mutatedPoints[2]
62+
mutatedPoints[2] = normgeom.NormPoint{X: 0.55, Y: 0.45}
63+
newP2 := mutatedPoints[2]
64+
65+
oldP5 := mutatedPoints[5]
66+
mutatedPoints[5] = normgeom.NormPoint{X: 0.35, Y: 0.25}
67+
newP5 := mutatedPoints[5]
68+
69+
mutations := []mutation.Mutation{
70+
{Index: 2, Old: oldP2, New: newP2},
71+
{Index: 5, Old: oldP5, New: newP5},
72+
}
73+
74+
// Method 1: Full evaluation of mutated points from scratch
75+
fullEval := NewTrianglesImageFunction(target, bs)
76+
fullFitness := fullEval.Calculate(PointsData{
77+
Points: mutatedPoints.Copy(),
78+
Mutations: nil,
79+
})
80+
81+
// Method 2: Delta evaluation from base
82+
deltaEval := NewTrianglesImageFunction(target, bs)
83+
// First, compute the base to populate triangleVariances
84+
deltaEval.Calculate(PointsData{
85+
Points: basePoints.Copy(),
86+
Mutations: nil,
87+
})
88+
// Now set this as the base and compute via delta path
89+
deltaEval2 := NewTrianglesImageFunction(target, bs)
90+
deltaEval2.SetBase(deltaEval)
91+
deltaFitness := deltaEval2.Calculate(PointsData{
92+
Points: mutatedPoints.Copy(),
93+
Mutations: mutations,
94+
})
95+
96+
t.Logf("Full fitness: %v", fullFitness)
97+
t.Logf("Delta fitness: %v", deltaFitness)
98+
99+
// They should match within floating-point tolerance
100+
epsilon := 1e-10
101+
diff := math.Abs(fullFitness - deltaFitness)
102+
if diff > epsilon {
103+
t.Errorf("Delta fitness (%v) does not match full fitness (%v), diff=%v", deltaFitness, fullFitness, diff)
104+
}
105+
}
106+
107+
// TestDeltaFitnessSingleMutation tests with a single point mutation.
108+
func TestDeltaFitnessSingleMutation(t *testing.T) {
109+
random.Seed(99)
110+
111+
const w, h = 60, 60
112+
const bs = 3
113+
114+
img := image.NewRGBA(image.Rect(0, 0, w, h))
115+
for x := 0; x < w; x++ {
116+
for y := 0; y < h; y++ {
117+
img.Set(x, y, color.RGBA{
118+
R: uint8(random.Intn(math.MaxUint8)),
119+
G: uint8(random.Intn(math.MaxUint8)),
120+
B: uint8(random.Intn(math.MaxUint8)),
121+
A: math.MaxUint8,
122+
})
123+
}
124+
}
125+
126+
target := image2.ToData(img)
127+
128+
basePoints := normgeom.NormPointGroup{
129+
{0.15, 0.15},
130+
{0.85, 0.15},
131+
{0.5, 0.5},
132+
{0.15, 0.85},
133+
{0.85, 0.85},
134+
}
135+
136+
// Compute base
137+
baseEval := NewTrianglesImageFunction(target, bs)
138+
baseEval.Calculate(PointsData{
139+
Points: basePoints.Copy(),
140+
Mutations: nil,
141+
})
142+
143+
// Single mutation on point 2
144+
mutatedPoints := basePoints.Copy()
145+
oldP := mutatedPoints[2]
146+
mutatedPoints[2] = normgeom.NormPoint{X: 0.52, Y: 0.48}
147+
newP := mutatedPoints[2]
148+
149+
mutations := []mutation.Mutation{
150+
{Index: 2, Old: oldP, New: newP},
151+
}
152+
153+
// Full evaluation
154+
fullEval := NewTrianglesImageFunction(target, bs)
155+
fullFitness := fullEval.Calculate(PointsData{
156+
Points: mutatedPoints.Copy(),
157+
Mutations: nil,
158+
})
159+
160+
// Delta evaluation
161+
deltaEval := NewTrianglesImageFunction(target, bs)
162+
deltaEval.SetBase(baseEval)
163+
deltaFitness := deltaEval.Calculate(PointsData{
164+
Points: mutatedPoints.Copy(),
165+
Mutations: mutations,
166+
})
167+
168+
t.Logf("Full: %v, Delta: %v", fullFitness, deltaFitness)
169+
170+
epsilon := 1e-10
171+
diff := math.Abs(fullFitness - deltaFitness)
172+
if diff > epsilon {
173+
t.Errorf("Delta fitness (%v) does not match full fitness (%v), diff=%v", deltaFitness, fullFitness, diff)
174+
}
175+
}

0 commit comments

Comments
 (0)