Skip to content

Commit 88d51fd

Browse files
committed
Merge branch 'main' into docs/lcg
2 parents 8e13259 + 34b9bd2 commit 88d51fd

7 files changed

Lines changed: 1195 additions & 0 deletions

File tree

3.12 KB
Loading
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { randf } from '@typegpu/noise';
2+
import tgpu, { d, std } from 'typegpu';
3+
import type { TgpuRoot, TgpuUniform } from 'typegpu';
4+
5+
export const MAX_POP = 65536;
6+
export const DEFAULT_POP = 8192;
7+
8+
export const CarState = d.struct({
9+
position: d.vec2f,
10+
angle: d.f32,
11+
alive: d.u32,
12+
progress: d.f32,
13+
speed: d.f32,
14+
angVel: d.f32,
15+
aliveSteps: d.u32,
16+
stallSteps: d.u32,
17+
});
18+
19+
export const FitnessArray = d.arrayOf(d.f32, MAX_POP);
20+
21+
export const InputLayer = d.struct({
22+
wA: d.mat4x4f, // inputs[0..3]
23+
wB: d.mat4x4f, // inputs[4..7]
24+
wC: d.mat4x4f, // inputs[8..11]
25+
bias: d.vec4f,
26+
});
27+
28+
export const DenseLayer = d.struct({
29+
w: d.mat4x4f,
30+
bias: d.vec4f,
31+
});
32+
33+
export const OutputLayer = d.struct({
34+
steer: d.vec4f,
35+
throttle: d.vec4f,
36+
bias: d.vec2f,
37+
});
38+
39+
export const Genome = d.struct({
40+
h1: InputLayer,
41+
h2: DenseLayer,
42+
out: OutputLayer,
43+
});
44+
45+
export const SimParams = d.struct({
46+
dt: d.f32,
47+
aspect: d.f32,
48+
generation: d.f32,
49+
population: d.u32,
50+
maxSpeed: d.f32,
51+
accel: d.f32,
52+
turnRate: d.f32,
53+
drag: d.f32,
54+
sensorDistance: d.f32,
55+
mutationRate: d.f32,
56+
mutationStrength: d.f32,
57+
carSize: d.f32,
58+
trackScale: d.f32,
59+
trackLength: d.f32,
60+
spawnX: d.f32,
61+
spawnY: d.f32,
62+
spawnAngle: d.f32,
63+
stepsPerDispatch: d.u32,
64+
});
65+
66+
export const CarStateArray = d.arrayOf(CarState, MAX_POP);
67+
export const GenomeArray = d.arrayOf(Genome, MAX_POP);
68+
export const CarStateLayout = d.arrayOf(CarState);
69+
70+
export const paramsAccess = tgpu.accessor(SimParams);
71+
72+
const fitLayout = tgpu.bindGroupLayout({
73+
state: { storage: CarStateArray },
74+
fitness: { storage: FitnessArray, access: 'mutable' },
75+
});
76+
77+
const initLayout = tgpu.bindGroupLayout({
78+
state: { storage: CarStateArray, access: 'mutable' },
79+
genome: { storage: GenomeArray, access: 'mutable' },
80+
});
81+
82+
const evolveLayout = tgpu.bindGroupLayout({
83+
fitness: { storage: FitnessArray },
84+
genome: { storage: GenomeArray },
85+
nextState: { storage: CarStateArray, access: 'mutable' },
86+
nextGenome: { storage: GenomeArray, access: 'mutable' },
87+
bestIdx: { storage: d.u32 },
88+
});
89+
90+
const randSignedVec4 = () => {
91+
'use gpu';
92+
return (d.vec4f(randf.sample(), randf.sample(), randf.sample(), randf.sample()) * 2 - 1) * 0.8;
93+
};
94+
95+
const randSignedMat4x4 = () => {
96+
'use gpu';
97+
return d.mat4x4f(randSignedVec4(), randSignedVec4(), randSignedVec4(), randSignedVec4());
98+
};
99+
100+
const makeSpawnState = () => {
101+
'use gpu';
102+
const spawn = d.vec2f(paramsAccess.$.spawnX, paramsAccess.$.spawnY) * paramsAccess.$.trackScale;
103+
return CarState({
104+
position: spawn,
105+
angle: paramsAccess.$.spawnAngle,
106+
speed: 0,
107+
alive: 1,
108+
progress: 0,
109+
angVel: 0,
110+
aliveSteps: 0,
111+
stallSteps: 0,
112+
});
113+
};
114+
115+
const tournamentSelect = () => {
116+
'use gpu';
117+
const population = d.f32(paramsAccess.$.population);
118+
let best = d.u32(0);
119+
let bestFitness = d.f32(-1);
120+
for (let j = 0; j < 8; j++) {
121+
const idx = d.u32(randf.sample() * population);
122+
const f = evolveLayout.$.fitness[idx];
123+
const better = f > bestFitness;
124+
bestFitness = std.select(bestFitness, f, better);
125+
best = std.select(best, idx, better);
126+
}
127+
return best;
128+
};
129+
130+
const evolveVec = <T extends d.v2f | d.v4f>(a: T, b: T): T => {
131+
'use gpu';
132+
const strength = paramsAccess.$.mutationStrength;
133+
const crossed = std.select(a, b, randf.sample() > 0.5);
134+
const doMutate = randf.sample() < paramsAccess.$.mutationRate;
135+
if (a.kind === 'vec2f') {
136+
const delta = d.vec2f(randf.normal(0, strength), randf.normal(0, strength));
137+
return ((crossed as d.v2f) + std.select(d.vec2f(0), delta, doMutate)) as T;
138+
} else {
139+
const delta = d.vec4f(
140+
randf.normal(0, strength),
141+
randf.normal(0, strength),
142+
randf.normal(0, strength),
143+
randf.normal(0, strength),
144+
);
145+
return ((crossed as d.v4f) + std.select(d.vec4f(0), delta, doMutate)) as T;
146+
}
147+
};
148+
149+
const evolveMat4x4 = (a: d.m4x4f, b: d.m4x4f) => {
150+
'use gpu';
151+
return d.mat4x4f(
152+
evolveVec(a.columns[0], b.columns[0]),
153+
evolveVec(a.columns[1], b.columns[1]),
154+
evolveVec(a.columns[2], b.columns[2]),
155+
evolveVec(a.columns[3], b.columns[3]),
156+
);
157+
};
158+
159+
const evolveInputLayer = (a: d.InferGPU<typeof InputLayer>, b: d.InferGPU<typeof InputLayer>) => {
160+
'use gpu';
161+
return InputLayer({
162+
wA: evolveMat4x4(a.wA, b.wA),
163+
wB: evolveMat4x4(a.wB, b.wB),
164+
wC: evolveMat4x4(a.wC, b.wC),
165+
bias: evolveVec(a.bias, b.bias),
166+
});
167+
};
168+
169+
const evolveDenseLayer = (a: d.InferGPU<typeof DenseLayer>, b: d.InferGPU<typeof DenseLayer>) => {
170+
'use gpu';
171+
return DenseLayer({ w: evolveMat4x4(a.w, b.w), bias: evolveVec(a.bias, b.bias) });
172+
};
173+
174+
const evolveOutputLayer = (
175+
a: d.InferGPU<typeof OutputLayer>,
176+
b: d.InferGPU<typeof OutputLayer>,
177+
) => {
178+
'use gpu';
179+
return OutputLayer({
180+
steer: evolveVec(a.steer, b.steer),
181+
throttle: evolveVec(a.throttle, b.throttle),
182+
bias: evolveVec(a.bias, b.bias),
183+
});
184+
};
185+
186+
const fitShader = (i: number) => {
187+
'use gpu';
188+
if (d.u32(i) >= paramsAccess.$.population) {
189+
return;
190+
}
191+
const s = CarState(fitLayout.$.state[i]);
192+
fitLayout.$.fitness[i] = s.progress * 10 + d.f32(s.aliveSteps) * 0.003;
193+
};
194+
195+
const initShader = (i: number) => {
196+
'use gpu';
197+
if (d.u32(i) >= paramsAccess.$.population) {
198+
return;
199+
}
200+
randf.seed2(d.vec2f(d.f32(i) + 1, paramsAccess.$.generation + 11));
201+
202+
initLayout.$.genome[i] = Genome({
203+
h1: {
204+
wA: randSignedMat4x4(),
205+
wB: randSignedMat4x4(),
206+
wC: randSignedMat4x4(),
207+
bias: d.vec4f(),
208+
},
209+
h2: { w: randSignedMat4x4(), bias: d.vec4f() },
210+
out: { steer: randSignedVec4(), throttle: randSignedVec4(), bias: d.vec2f() },
211+
});
212+
initLayout.$.state[i] = makeSpawnState();
213+
};
214+
215+
const evolveShader = (i: number) => {
216+
'use gpu';
217+
if (d.u32(i) >= paramsAccess.$.population) {
218+
return;
219+
}
220+
221+
// Elitism: champion always lives at index 0, copied unchanged
222+
if (d.u32(i) === 0) {
223+
evolveLayout.$.nextGenome[0] = Genome(evolveLayout.$.genome[evolveLayout.$.bestIdx]);
224+
evolveLayout.$.nextState[0] = makeSpawnState();
225+
return;
226+
}
227+
228+
randf.seed2(d.vec2f(d.f32(i) + 3, paramsAccess.$.generation + 19));
229+
230+
const parentA = Genome(evolveLayout.$.genome[tournamentSelect()]);
231+
const parentB = Genome(evolveLayout.$.genome[tournamentSelect()]);
232+
233+
evolveLayout.$.nextGenome[i] = Genome({
234+
h1: evolveInputLayer(parentA.h1, parentB.h1),
235+
h2: evolveDenseLayer(parentA.h2, parentB.h2),
236+
out: evolveOutputLayer(parentA.out, parentB.out),
237+
});
238+
239+
evolveLayout.$.nextState[i] = makeSpawnState();
240+
};
241+
242+
export function createGeneticPopulation(root: TgpuRoot, params: TgpuUniform<typeof SimParams>) {
243+
const stateBuffers = [0, 1].map(() =>
244+
root.createBuffer(CarStateArray).$usage('storage', 'vertex'),
245+
);
246+
const genomeBuffers = [0, 1].map(() => root.createBuffer(GenomeArray).$usage('storage'));
247+
const fitnessBuffer = root.createBuffer(FitnessArray).$usage('storage');
248+
const bestIdxBuffer = root.createBuffer(d.u32).$usage('storage');
249+
250+
const initBindGroups = [0, 1].map((i) =>
251+
root.createBindGroup(initLayout, {
252+
state: stateBuffers[i],
253+
genome: genomeBuffers[i],
254+
}),
255+
);
256+
257+
const fitBindGroups = [0, 1].map((i) =>
258+
root.createBindGroup(fitLayout, {
259+
state: stateBuffers[i],
260+
fitness: fitnessBuffer,
261+
}),
262+
);
263+
264+
const evolveBindGroups = [0, 1].map((i) =>
265+
root.createBindGroup(evolveLayout, {
266+
fitness: fitnessBuffer,
267+
genome: genomeBuffers[i],
268+
nextState: stateBuffers[1 - i],
269+
nextGenome: genomeBuffers[1 - i],
270+
bestIdx: bestIdxBuffer,
271+
}),
272+
);
273+
274+
const initPipeline = root.with(paramsAccess, params).createGuardedComputePipeline(initShader);
275+
const fitPipeline = root.with(paramsAccess, params).createGuardedComputePipeline(fitShader);
276+
const evolvePipeline = root.with(paramsAccess, params).createGuardedComputePipeline(evolveShader);
277+
278+
let current = 0;
279+
let generation = 0;
280+
281+
return {
282+
stateBuffers,
283+
genomeBuffers,
284+
fitnessBuffer,
285+
bestIdxBuffer,
286+
get current() {
287+
return current;
288+
},
289+
get generation() {
290+
return generation;
291+
},
292+
get currentStateBuffer() {
293+
return stateBuffers[current];
294+
},
295+
get currentGenomeBuffer() {
296+
return genomeBuffers[current];
297+
},
298+
299+
init() {
300+
current = 0;
301+
generation = 0;
302+
initPipeline.with(initBindGroups[0]).dispatchThreads(MAX_POP);
303+
initPipeline.with(initBindGroups[1]).dispatchThreads(MAX_POP);
304+
},
305+
306+
reinitCurrent(population: number) {
307+
initPipeline.with(initBindGroups[current]).dispatchThreads(population);
308+
},
309+
310+
precomputeFitness(population: number) {
311+
fitPipeline.with(fitBindGroups[current]).dispatchThreads(population);
312+
},
313+
314+
evolve(population: number) {
315+
evolvePipeline.with(evolveBindGroups[current]).dispatchThreads(population);
316+
current = 1 - current;
317+
generation++;
318+
},
319+
};
320+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<canvas data-fit-to-container></canvas>
2+
<div class="stats"></div>
3+
<style>
4+
.stats {
5+
position: absolute;
6+
top: 8px;
7+
left: 8px;
8+
color: rgba(255, 255, 255, 0.85);
9+
font-family: monospace;
10+
font-size: 13px;
11+
pointer-events: none;
12+
white-space: pre;
13+
}
14+
</style>

0 commit comments

Comments
 (0)