From 59a71b10b496ca1115dcc26cfcecc94d83b32c30 Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Sun, 6 Sep 2026 17:16:23 +0200 Subject: [PATCH 01/18] fix: collect PPO rollouts and preserve truncation bootstrap (#7) --- README.md | 27 ++++++ packages/backend-tfjs/src/agents/dqn.ts | 4 +- packages/backend-tfjs/src/agents/ppo.ts | 41 +++++--- packages/backend-tfjs/src/agents/qtable.ts | 4 +- packages/backend-tfjs/src/schemas.ts | 1 + packages/backend-tfjs/src/types.ts | 2 + .../backend-tfjs/test/dqn-truncation.test.ts | 31 +++++++ .../backend-tfjs/test/ppo-bootstrap.test.ts | 93 +++++++++++++++++++ packages/backend-tfjs/test/ppo-public.test.ts | 44 +++++++++ .../test/qtable-truncation.test.ts | 13 +++ packages/core/src/env-validation.ts | 3 + packages/core/src/ignition-env.ts | 20 ++-- packages/core/src/types.ts | 8 ++ packages/core/test/episode-end.test.ts | 39 ++++++++ 14 files changed, 306 insertions(+), 24 deletions(-) create mode 100644 packages/backend-tfjs/test/dqn-truncation.test.ts create mode 100644 packages/backend-tfjs/test/ppo-bootstrap.test.ts create mode 100644 packages/backend-tfjs/test/ppo-public.test.ts create mode 100644 packages/backend-tfjs/test/qtable-truncation.test.ts create mode 100644 packages/core/test/episode-end.test.ts diff --git a/README.md b/README.md index 8ce3c74..65d300f 100644 --- a/README.md +++ b/README.md @@ -354,3 +354,30 @@ The codebase follows: Built by [@salim4n](https://github.com/salim4n) / [@IgnitionAI](https://github.com/IgnitionAI) **Star the repo** ⭐ if you think creative JS devs deserve proper RL tooling. + +## PPO rollout and episode boundaries + +The public training loop collects **128 transitions** before an automatic PPO +update. Configure this with `env.train('ppo', { rolloutSize: 256 })`. +`batchSize` controls optimizer minibatches, independently of `rolloutSize`. +Calling `agent.train()` explicitly still updates all currently collected data, +including a partial rollout. A singleton or constant-advantage batch keeps its +raw advantages rather than removing its learning signal through normalization. + +`TrainingEnv.done()` remains required for compatibility. Existing environments +continue to treat `done()` as a terminal condition. To distinguish external time +limits, optionally provide `truncated(): boolean`. In that case termination +falls back to `done() && !truncated()`. An optional `terminated(): boolean` +overrides that fallback; when both explicit flags are true, termination takes +precedence for value bootstrapping. Either flag resets the episode, retaining +its final observation in the returned transition. + +PPO bootstraps nonterminal rollout ends and truncations from their actual next +observation; its advantage trace stops at either episode boundary. DQN and +Q-table also bootstrap truncations. Inference never trains. Manual environment +reset and inference steps discard incomplete PPO rollouts; stop/resume alone +retains them. Loading a PPO checkpoint discards pending transitions. + +Custom agents can optionally implement `shouldTrain()` to control automatic +update cadence and `discardRollout()` to discard on-policy data when the training +trajectory is interrupted. Agents without these hooks retain per-step updates. diff --git a/packages/backend-tfjs/src/agents/dqn.ts b/packages/backend-tfjs/src/agents/dqn.ts index 4e11d1f..25fc71e 100644 --- a/packages/backend-tfjs/src/agents/dqn.ts +++ b/packages/backend-tfjs/src/agents/dqn.ts @@ -103,8 +103,8 @@ export class DQNAgent implements AgentInterface { const nextQArray = nextQValues.arraySync() as number[][]; const updatedQ = qArray.map((q, i) => { - const { action, reward, terminated, truncated } = batch[i]; - const done = terminated || truncated; + const { action, reward, terminated } = batch[i]; + const done = terminated; const a = action as number; q[a] = done ? reward : reward + this.gamma * Math.max(...nextQArray[i]); return q; diff --git a/packages/backend-tfjs/src/agents/ppo.ts b/packages/backend-tfjs/src/agents/ppo.ts index edda2a8..2465103 100644 --- a/packages/backend-tfjs/src/agents/ppo.ts +++ b/packages/backend-tfjs/src/agents/ppo.ts @@ -214,12 +214,16 @@ export class PPOAgent implements AgentInterface { }); } - /** - * Mettre à jour l'acteur et le critic sur les données collectées. - * Vide le buffer de rollout à la fin (algorithme on-policy). - * - * Appelé typiquement à la fin de chaque épisode ou après N steps. - */ + /** Discard data when the training trajectory is interrupted. */ + discardRollout(): void { + this.rollout = []; + } + + shouldTrain(): boolean { + return this.rollout.length >= (this.config.rolloutSize ?? 128); + } + + /** Explicitly update all collected transitions, including partial rollouts. */ async train(): Promise { const n = this.rollout.length; if (n === 0) return; @@ -254,6 +258,7 @@ export class PPOAgent implements AgentInterface { // Vider le rollout — PPO est on-policy this.rollout = []; + this.trainStepCounter++; } // ------------------------------------------------------------------------- @@ -279,21 +284,26 @@ export class PPOAgent implements AgentInterface { const advantages = new Array(n); const returns = new Array(n); - let nextValue = 0; // V(s_{T+1}) = 0 pour l'état terminal + // Evaluate the actual successor before any update, including truncated + // episodes and the end of a nonterminal rollout. + const nextValues = tf.tidy(() => { + const states = tf.tensor2d(this.rollout.map(e => e.nextState)); + return Array.from((this.criticNet.predict(states) as tf.Tensor).dataSync()); + }); let lastGAE = 0; for (let t = n - 1; t >= 0; t--) { const { reward, terminated, truncated, value } = this.rollout[t]; - const mask = (terminated || truncated) ? 0 : 1; + const bootstrapMask = terminated ? 0 : 1; + const traceMask = (terminated || truncated) ? 0 : 1; // Erreur TD - const delta = reward + this.gamma * nextValue * mask - value; + const delta = reward + this.gamma * nextValues[t] * bootstrapMask - value; // Accumulation GAE - lastGAE = delta + this.gamma * this.gaeLambda * mask * lastGAE; + lastGAE = delta + this.gamma * this.gaeLambda * traceMask * lastGAE; advantages[t] = lastGAE; returns[t] = lastGAE + value; - nextValue = value; } // Normalisation des avantages @@ -304,7 +314,9 @@ export class PPOAgent implements AgentInterface { return { returns, - advantages: advantages.map(a => (a - mean) / std), + advantages: n > 1 && std > 1e-8 + ? advantages.map(a => (a - mean) / std) + : advantages, }; } @@ -456,6 +468,9 @@ export class PPOAgent implements AgentInterface { } const actor = await provider.load(`${modelId}/actor`); const critic = await provider.load(`${modelId}/critic`); + this.actorNet.dispose(); + this.criticNet.dispose(); + this.rollout = []; this.actorNet = actor as tf.Sequential; this.criticNet = critic as tf.Sequential; console.log(`[PPO] ✅ Loaded model ${modelId}`); @@ -464,6 +479,8 @@ export class PPOAgent implements AgentInterface { dispose(): void { this.actorNet?.dispose(); this.criticNet?.dispose(); + this.actorOptimizer.dispose(); + this.criticOptimizer.dispose(); this.rollout = []; } } diff --git a/packages/backend-tfjs/src/agents/qtable.ts b/packages/backend-tfjs/src/agents/qtable.ts index 0efc7c0..1f7d984 100644 --- a/packages/backend-tfjs/src/agents/qtable.ts +++ b/packages/backend-tfjs/src/agents/qtable.ts @@ -140,8 +140,8 @@ export class QTableAgent implements AgentInterface { async train(): Promise { if (!this.lastExperience) return; - const { state, action, reward, nextState, terminated, truncated } = this.lastExperience; - const done = terminated || truncated; + const { state, action, reward, nextState, terminated } = this.lastExperience; + const done = terminated; const a = action as number; const sIdx = this.stateToIndex(state); const sNextIdx = this.stateToIndex(nextState); diff --git a/packages/backend-tfjs/src/schemas.ts b/packages/backend-tfjs/src/schemas.ts index 11a8aa2..8677cb6 100644 --- a/packages/backend-tfjs/src/schemas.ts +++ b/packages/backend-tfjs/src/schemas.ts @@ -73,6 +73,7 @@ export type DQNConfig = z.infer; // ─── PPOConfig ──────────────────────────────────────────────────────────────── export const PPOConfigSchema = z.object({ + rolloutSize: z.number().int().positive().optional(), backend: TFBackendSchema, inputSize: z .number() diff --git a/packages/backend-tfjs/src/types.ts b/packages/backend-tfjs/src/types.ts index d10c952..1521d1f 100644 --- a/packages/backend-tfjs/src/types.ts +++ b/packages/backend-tfjs/src/types.ts @@ -21,6 +21,8 @@ export interface DQNConfig { } export interface PPOConfig { + /** Transitions collected before automatic training (default: 128). */ + rolloutSize?: number; backend?: TFBackend; // TF.js backend to use (default: 'auto') inputSize: number; // Dimension du vecteur d'état actionSize: number; // Nombre d'actions discrètes diff --git a/packages/backend-tfjs/test/dqn-truncation.test.ts b/packages/backend-tfjs/test/dqn-truncation.test.ts new file mode 100644 index 0000000..55709de --- /dev/null +++ b/packages/backend-tfjs/test/dqn-truncation.test.ts @@ -0,0 +1,31 @@ +import { expect, it } from 'vitest'; +import * as tf from '@tensorflow/tfjs'; +import { DQNAgent } from '../src/agents/dqn'; +import type { ModelStorageProvider } from '@ignitionai/storage'; + +it.each([false, true])('DQN bootstraps unless terminated=%s', async (terminated) => { + await tf.setBackend('cpu'); + const net = tf.sequential(); + net.add(tf.layers.dense({ inputShape: [1], units: 8, activation: 'relu', kernelInitializer: 'zeros' })); + net.add(tf.layers.dense({ units: 2, kernelInitializer: 'zeros', + biasInitializer: tf.initializers.constant({ value: 2 }) })); + net.compile({ optimizer: 'sgd', loss: 'meanSquaredError' }); + let learned = 0; + const provider: ModelStorageProvider = { + async load() { return net; }, + async save(_id, model) { + learned = tf.tidy(() => (model.predict(tf.tensor2d([[0]])) as tf.Tensor).dataSync()[0]); + return 'memory'; + }, async list() { return []; }, async exists() { return true; }, async delete() {}, + }; + const agent = new DQNAgent({ inputSize: 1, actionSize: 2, hiddenLayers: [8], batchSize: 1, + memorySize: 2, gamma: 0.9, backend: 'cpu', storageProvider: provider }); + try { + await agent.load('fixture'); + agent.remember({ state: [0], action: 0, nextState: [0], reward: 0.5, terminated, truncated: !terminated }); + await agent.train(); + await agent.save('result'); + if (terminated) expect(learned).toBeLessThan(2); + else expect(learned).toBeGreaterThan(2); + } finally { agent.dispose(); } +}); diff --git a/packages/backend-tfjs/test/ppo-bootstrap.test.ts b/packages/backend-tfjs/test/ppo-bootstrap.test.ts new file mode 100644 index 0000000..f636b76 --- /dev/null +++ b/packages/backend-tfjs/test/ppo-bootstrap.test.ts @@ -0,0 +1,93 @@ +import { expect, it } from 'vitest'; +import * as tf from '@tensorflow/tfjs'; +import { PPOAgent } from '../src/agents/ppo'; +import type { ModelStorageProvider } from '@ignitionai/storage'; + +// A persisted policy with equal logits and a critic predicting V(s)=2. +function storage(): ModelStorageProvider { + return { + async load(id) { + const net = tf.sequential(); + net.add(tf.layers.dense({ inputShape: [1], units: id.endsWith('/actor') ? 2 : 1, + kernelInitializer: 'zeros', biasInitializer: tf.initializers.constant({ value: id.endsWith('/actor') ? 0 : 2 }) })); + return net; + }, + async save() { return 'memory'; }, async list() { return []; }, + async delete() {}, async exists() { return true; }, + }; +} + +it.each([ + { terminated: false, truncated: false, expected: 0 }, + { terminated: false, truncated: true, expected: 0 }, + { terminated: true, truncated: false, expected: 1 }, +])('single-transition update bootstraps only nonterminal states: %j', async ({ terminated, truncated, expected }) => { + const agent = new PPOAgent({ inputSize: 1, actionSize: 2, hiddenLayers: [8], + gamma: 0.9, epochs: 1, entropyCoef: 0, storageProvider: storage() }); + try { + await agent.load('fixture'); + const action = await agent.getAction([0], true); // tie chooses 0 + agent.remember({ state: [0], nextState: [0], action, reward: 0.5, terminated, truncated }); + // Nonterminal advantage: .5 + .9*2 - 2 = .3; terminal: .5 - 2 = -1.5. + await agent.train(); + expect(await agent.getAction([0], true)).toBe(expected); + } finally { agent.dispose(); } +}); + +it.each([false, true])('GAE does not carry rewards across an episode boundary (truncated=%s)', async (truncated) => { + let firstValue = 0; + const provider = storage(); + provider.load = async (id) => { + const net = tf.sequential(); + net.add(tf.layers.dense({ inputShape: [2], units: id.endsWith('/actor') ? 2 : 1, + useBias: false, kernelInitializer: tf.initializers.constant({ value: id.endsWith('/actor') ? 0 : 2 }) })); + return net; + }; + provider.save = async (id, model) => { + if (id.endsWith('/critic')) firstValue = tf.tidy(() => (model.predict(tf.tensor2d([[1, 0]])) as tf.Tensor).dataSync()[0]); + return 'memory'; + }; + const agent = new PPOAgent({ inputSize: 2, actionSize: 2, hiddenLayers: [8], + gamma: 0.9, gaeLambda: 1, epochs: 1, batchSize: 2, storageProvider: provider }); + try { + await agent.load('fixture'); + const action = await agent.getAction([1, 0], true); + agent.remember({ state: [1, 0], nextState: [1, 0], action, reward: 3, + terminated: !truncated, truncated }); + const nextAction = await agent.getAction([0, 1], true); + agent.remember({ state: [0, 1], nextState: [0, 1], action: nextAction, reward: -100, + terminated: true, truncated: false }); + await agent.train(); + await agent.save('result'); + // First target is 3 (terminal) or 4.8 (truncated), both above V=2. + // Leaking the next episode's -100 reward would instead decrease it. + expect(firstValue).toBeGreaterThan(2); + } finally { agent.dispose(); } +}); + +it('bootstraps the actual successor rather than the current observation', async () => { + const provider = storage(); + let probability = 0.5; + provider.load = async (id) => { + const net = tf.sequential(); + net.add(tf.layers.dense({ inputShape: [1], units: id.endsWith('/actor') ? 2 : 1, + kernelInitializer: tf.initializers.constant({ value: id.endsWith('/actor') ? 0 : 2 }), + biasInitializer: 'zeros' })); + return net; + }; + provider.save = async (id, model) => { + if (id.endsWith('/actor')) probability = tf.tidy(() => tf.softmax(model.predict(tf.tensor2d([[0]])) as tf.Tensor).dataSync()[0]); + return 'memory'; + }; + const agent = new PPOAgent({ inputSize: 1, actionSize: 2, hiddenLayers: [8], gamma: 0.9, + epochs: 1, entropyCoef: 0, storageProvider: provider }); + try { + await agent.load('fixture'); + const action = await agent.getAction([0], true); + agent.remember({ state: [0], nextState: [1], action, reward: -0.5, terminated: false, truncated: false }); + await agent.train(); + await agent.save('result'); + // -.5 + .9*V(1) - V(0) = 1.3. Using V(0) gives -.5 instead. + expect(probability).toBeGreaterThan(0.5); + } finally { agent.dispose(); } +}); diff --git a/packages/backend-tfjs/test/ppo-public.test.ts b/packages/backend-tfjs/test/ppo-public.test.ts new file mode 100644 index 0000000..033df38 --- /dev/null +++ b/packages/backend-tfjs/test/ppo-public.test.ts @@ -0,0 +1,44 @@ +import { expect, it, vi } from 'vitest'; +import * as tf from '@tensorflow/tfjs'; +import { IgnitionEnvTFJS } from '../src/ignition-env-tfjs'; + +it.each([11, 29, 47])('PPO learns rewarded actions through the public loop (seed %i)', async (seed) => { + await tf.setBackend('cpu'); + let rng = seed; + const random = vi.spyOn(Math, 'random').mockImplementation(() => { + rng = (Math.imul(rng, 1664525) + 1013904223) >>> 0; + return rng / 4294967296; + }); + let action = 0; + const env = new IgnitionEnvTFJS({ + actions: 2, observe: () => [0], step: (a: number | number[]) => { action = a as number; }, + reward: () => action === 1 ? 1 : -1, done: () => true, reset: () => {}, + }); + try { + env.train('ppo', { hiddenLayers: [8], lr: 0.01, entropyCoef: 0, batchSize: 32 }); + env.stop(); + for (let i = 0; i < 512; i++) await env.step(); + let correct = 0; + for (let i = 0; i < 200; i++) correct += (await env.agent!.getAction([0])) as number; + expect(correct / 200).toBeGreaterThan(0.8); + } finally { + env.stop(); + env.agent?.dispose?.(); + random.mockRestore(); + } +}, 30000); + +it.each(['reset', 'inferStep'] as const)('discards a partial rollout on %s', async (operation) => { + const env = new IgnitionEnvTFJS({ actions: 2, observe: () => [0], step: () => {}, + reward: () => 1, done: () => false, reset: () => {} }); + try { + env.train('ppo', { hiddenLayers: [8], rolloutSize: 2 }); + env.stop(); + await env.step(); + await env[operation](); + await env.step(); + expect(env.agent!.getState!().trainStepCounter).toBe(0); + await env.step(); + expect(env.agent!.getState!().trainStepCounter).toBe(1); + } finally { env.stop(); env.agent?.dispose?.(); } +}); diff --git a/packages/backend-tfjs/test/qtable-truncation.test.ts b/packages/backend-tfjs/test/qtable-truncation.test.ts new file mode 100644 index 0000000..26f9de4 --- /dev/null +++ b/packages/backend-tfjs/test/qtable-truncation.test.ts @@ -0,0 +1,13 @@ +import { it, expect } from 'vitest'; +import { QTableAgent } from '../src/agents/qtable'; + +it('Q-table bootstraps a truncated transition from its actual successor', async () => { + const agent = new QTableAgent({ inputSize: 1, actionSize: 2, lr: 0.9, gamma: 0.9, epsilon: 0 }); + agent.remember({ state: [1], action: 0, reward: 2, nextState: [1], terminated: true, truncated: false }); + await agent.train(); + agent.remember({ state: [0], action: 0, reward: 0.5, nextState: [0], terminated: true, truncated: false }); + await agent.train(); + agent.remember({ state: [0], action: 1, reward: 0, nextState: [1], terminated: false, truncated: true }); + await agent.train(); + expect(await agent.getAction([0], true)).toBe(1); +}); diff --git a/packages/core/src/env-validation.ts b/packages/core/src/env-validation.ts index eb5d2fb..78819e2 100644 --- a/packages/core/src/env-validation.ts +++ b/packages/core/src/env-validation.ts @@ -18,6 +18,9 @@ export function validateTrainingEnv(env: unknown): asserts env is TrainingEnv { assertFn(e, 'reward', 'TrainingEnv'); assertFn(e, 'done', 'TrainingEnv'); assertFn(e, 'reset', 'TrainingEnv'); + for (const name of ['terminated', 'truncated']) { + if (e[name] !== undefined) assertFn(e, name, 'TrainingEnv'); + } if (e.actions === undefined || e.actions === null) { throw new Error('[IgnitionAI] TrainingEnv must have an actions property (string[] or number)'); diff --git a/packages/core/src/ignition-env.ts b/packages/core/src/ignition-env.ts index 48c83af..a45e989 100644 --- a/packages/core/src/ignition-env.ts +++ b/packages/core/src/ignition-env.ts @@ -80,7 +80,8 @@ export class IgnitionEnv { const observation = this.env.observe(); const reward = this.env.reward(); - const terminated = this.env.done(); + const truncated = this.env.truncated?.() ?? false; + const terminated = this.env.terminated?.() ?? (this.env.done() && !truncated); const experience: Experience = { state: this.currentState, @@ -88,15 +89,15 @@ export class IgnitionEnv { reward, nextState: observation, terminated, - truncated: false, + truncated, }; this._agent.remember(experience); - await this._agent.train(); + if (this._agent.shouldTrain?.() ?? true) await this._agent.train(); - const result: StepResult = { observation, reward, terminated, truncated: false }; + const result: StepResult = { observation, reward, terminated, truncated }; - if (terminated) { + if (terminated || truncated) { this.env.reset(); this.currentState = this.env.observe(); } else { @@ -113,16 +114,18 @@ export class IgnitionEnv { this.stepCount++; + this._agent.discardRollout?.(); const action = await this._agent.getAction(this.currentState, true); this.env.step(action); const observation = this.env.observe(); const reward = this.env.reward(); - const terminated = this.env.done(); + const truncated = this.env.truncated?.() ?? false; + const terminated = this.env.terminated?.() ?? (this.env.done() && !truncated); - const result: StepResult = { observation, reward, terminated, truncated: false }; + const result: StepResult = { observation, reward, terminated, truncated }; - if (terminated) { + if (terminated || truncated) { this.env.reset(); this.currentState = this.env.observe(); } else { @@ -173,6 +176,7 @@ export class IgnitionEnv { } public reset(): void { + this._agent?.discardRollout?.(); this.env.reset(); this.currentState = this.env.observe(); this.stepCount = 0; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 701f99f..f09dc4c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -54,6 +54,10 @@ export interface AgentInterface { getAction(observation: number[], greedy?: boolean): Promise; remember(experience: Experience): void; train(): Promise; + /** Whether the collected transitions are ready for an automatic update. */ + shouldTrain?(): boolean; + /** Discard on-policy data when execution leaves the training trajectory. */ + discardRollout?(): void; /** Release TF/GPU/WASM resources held by the agent */ dispose?(): void; /** Reset agent internal state (epsilon, memory, counters…) */ @@ -99,6 +103,10 @@ export interface TrainingEnv { reward(): number; /** Return true if the episode is over */ done(): boolean; + /** Optional explicit terminal condition; overrides legacy done(). */ + terminated?(): boolean; + /** Optional external episode limit (default: false). */ + truncated?(): boolean; /** Reset the environment for a new episode */ reset(): void; } diff --git a/packages/core/test/episode-end.test.ts b/packages/core/test/episode-end.test.ts new file mode 100644 index 0000000..c831461 --- /dev/null +++ b/packages/core/test/episode-end.test.ts @@ -0,0 +1,39 @@ +import { expect, it, vi } from 'vitest'; +import { IgnitionEnv } from '../src/ignition-env'; + +it.each(['step', 'inferStep'] as const)('%s reports truncation and resets without losing the last observation', async (method) => { + let position = 0; + const world = { + actions: 2, observe: () => [position], step: () => { position = 5; }, + reward: () => 1, done: () => true, truncated: () => true, + reset: () => { position = 0; }, + }; + const loop = new IgnitionEnv(world); + const remember = vi.fn(); + loop.agent = { getAction: async () => 0, remember, train: async () => {} }; + expect(await loop[method]()).toEqual({ observation: [5], reward: 1, terminated: false, truncated: true }); + expect(position).toBe(0); + if (method === 'step') expect(remember).toHaveBeenCalledWith(expect.objectContaining({ nextState: [5], terminated: false, truncated: true })); +}); + +it.each([ + { explicit: undefined, truncated: undefined, terminal: true, cut: false }, + { explicit: true, truncated: true, terminal: true, cut: true }, + { explicit: false, truncated: false, terminal: false, cut: false }, +])('preserves legacy done and explicit terminal precedence: %j', async ({ explicit, truncated, terminal, cut }) => { + const reset = vi.fn(); + const loop = new IgnitionEnv({ actions: 2, observe: () => [0], step: () => {}, + reward: () => 0, done: () => true, reset, + ...(explicit === undefined ? {} : { terminated: () => explicit }), + ...(truncated === undefined ? {} : { truncated: () => truncated }), + }); + loop.agent = { getAction: async () => 0, remember: () => {}, train: async () => {} }; + expect(await loop.step()).toMatchObject({ terminated: terminal, truncated: cut }); + expect(reset).toHaveBeenCalledTimes(terminal || cut ? 1 : 0); +}); + +it.each(['terminated', 'truncated'])('rejects a non-function %s callback at construction', (name) => { + expect(() => new IgnitionEnv({ actions: 2, observe: () => [0], step: () => {}, + reward: () => 0, done: () => false, reset: () => {}, [name]: true, + })).toThrow(new RegExp(name)); +}); From 513cf0f753ef1f716ba1a3eb9a5eed0e6892c08e Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Sun, 6 Sep 2026 17:23:52 +0200 Subject: [PATCH 02/18] feat: evaluate fixed Circuit policies on held-out tracks (#8) --- README.md | 26 +++++++ packages/demo-car-circuit/src/circuit-env.ts | 75 ++++++++++++++++--- packages/demo-car-circuit/src/evaluation.ts | 74 ++++++++++++++++++ .../demo-car-circuit/test/circuit-env.test.ts | 20 +++++ .../demo-car-circuit/test/evaluation.test.ts | 53 +++++++++++++ packages/demo-car-circuit/test/track.test.ts | 5 ++ 6 files changed, 242 insertions(+), 11 deletions(-) create mode 100644 packages/demo-car-circuit/src/evaluation.ts create mode 100644 packages/demo-car-circuit/test/evaluation.test.ts diff --git a/README.md b/README.md index 65d300f..e311cda 100644 --- a/README.md +++ b/README.md @@ -381,3 +381,29 @@ retains them. Loading a PPO checkpoint discards pending transitions. Custom agents can optionally implement `shouldTrain()` to control automatic update cadence and `discardRollout()` to discard on-policy data when the training trajectory is interrupted. Agents without these hooks retain per-step updates. + +## Circuit evaluation (local demo API) + +The Circuit demo exposes `evaluateCircuit(policy, { policyId, circuit })` from +its evaluation module. Pass a versioned policy identifier and `training` or +`test`. The evaluator requests greedy actions through `IgnitionEnv.inferStep()` +and never invokes the supplied policy's `train()` or `remember()`. Evaluate a +checkpoint that is not being trained concurrently. + +Protocol `circuit-evaluation-v1` fixes two distinct oval geometries, three starting +waypoints, a 1,500-transition limit per episode and a three-lap success criterion. +It returns a JSON-serializable versioned report with per-episode outcomes, +transition counts, completed laps and simulated lap times (50 ms per step). +These are simulated times, not browser execution times. Preserve the report with +its checkpoint and avoid training or selecting models on the reserved test track. + +The existing three-argument `CircuitEnv` constructor remains supported. Its +optional fourth argument configures `maxSteps`, `targetLaps` and `startWaypoint`. +Terminal failure/success and external time limits are distinct; `lastEpisode` +retains the final metrics after an automatic reset. Completed laps require net +forward progress from the selected starting position, rather than merely +crossing the start line. + +This protocol evaluates the existing driving environment. The planned Circuit +Racing experience (imported 3D vehicles, AI races and human-versus-trained-AI) +will require its own versioned driving and evaluation contract. diff --git a/packages/demo-car-circuit/src/circuit-env.ts b/packages/demo-car-circuit/src/circuit-env.ts index a512287..676ee9a 100644 --- a/packages/demo-car-circuit/src/circuit-env.ts +++ b/packages/demo-car-circuit/src/circuit-env.ts @@ -4,6 +4,22 @@ import { OvalTrack } from './track'; const SPEED = 0.15; const STEER_DELTA = 0.08; // radians per step const MAX_STEPS = 500; +export const SIMULATION_STEP_SECONDS = 0.05; + +export interface CircuitOptions { + maxSteps?: number; + targetLaps?: number; + startWaypoint?: number; +} + +export interface CircuitEpisode { + transitions: number; + completedLaps: number; + lapTimesSeconds: number[]; + simulationSeconds: number; + endReason: 'success' | 'off-track' | 'time-limit'; +} + const LAP_BONUS = 50; const OFF_TRACK_PENALTY = -10; const LAPS_TO_WIN = 3; @@ -23,6 +39,14 @@ export class CircuitEnv implements TrainingEnv { stepCount = 0; laps = 0; private lastProgress = 0; + private readonly maxSteps: number; + private readonly targetLaps: number; + private readonly startWaypoint: number; + private forwardProgress = 0; + private lastLapStep = 0; + private lapTimesSeconds: number[] = []; + /** Most recent completed episode; retained across automatic resets. */ + lastEpisode: CircuitEpisode | null = null; private offTrack = false; private justCompletedLap = false; @@ -31,13 +55,22 @@ export class CircuitEnv implements TrainingEnv { private alignment = 0; private lateralNorm = 0; - constructor(straightLen = 10, radius = 4, halfWidth = 2) { + constructor(straightLen = 10, radius = 4, halfWidth = 2, options: CircuitOptions = {}) { this.track = new OvalTrack(straightLen, radius, halfWidth); - // Start on top straight, facing right - const start = this.track.waypoints[0]; + this.maxSteps = options.maxSteps ?? MAX_STEPS; + this.targetLaps = options.targetLaps ?? LAPS_TO_WIN; + this.startWaypoint = options.startWaypoint ?? 0; + if (!Number.isInteger(this.maxSteps) || this.maxSteps < 1 || + !Number.isInteger(this.targetLaps) || this.targetLaps < 1 || + !Number.isInteger(this.startWaypoint) || this.startWaypoint < 0 || + this.startWaypoint >= this.track.waypoints.length - 1) { + throw new Error('Invalid Circuit episode options'); + } + const start = this.track.waypoints[this.startWaypoint]; this.carX = start.x; this.carY = start.y; - this.carAngle = 0; + this.carAngle = this.track.nearestPoint(start.x, start.y).trackAngle; + this.lastProgress = this.track.nearestPoint(start.x, start.y).progress; } observe(): number[] { @@ -87,9 +120,12 @@ export class CircuitEnv implements TrainingEnv { // Lap completion — fire bonus only once per crossing this.justCompletedLap = false; - if (result.progress < 0.1 && this.lastProgress > 0.9) { + this.forwardProgress += delta; + if (!this.offTrack && this.forwardProgress >= this.laps + 1 - 1e-9) { this.laps++; this.justCompletedLap = true; + this.lapTimesSeconds.push((this.stepCount + 1 - this.lastLapStep) * SIMULATION_STEP_SECONDS); + this.lastLapStep = this.stepCount + 1; } this.lastProgress = result.progress; @@ -110,20 +146,37 @@ export class CircuitEnv implements TrainingEnv { return this.justCompletedLap ? base + LAP_BONUS : base; } + terminated(): boolean { + return this.offTrack || this.laps >= this.targetLaps; + } + + truncated(): boolean { + return !this.terminated() && this.stepCount >= this.maxSteps; + } + done(): boolean { - if (this.offTrack) return true; - if (this.laps >= LAPS_TO_WIN) return true; - return this.stepCount >= MAX_STEPS; + return this.terminated() || this.truncated(); } reset(): void { - const start = this.track.waypoints[0]; + if (this.done()) { + this.lastEpisode = { + transitions: this.stepCount, completedLaps: this.laps, + lapTimesSeconds: [...this.lapTimesSeconds], + simulationSeconds: this.stepCount * SIMULATION_STEP_SECONDS, + endReason: this.offTrack ? 'off-track' : this.laps >= this.targetLaps ? 'success' : 'time-limit', + }; + } + const start = this.track.waypoints[this.startWaypoint]; this.carX = start.x; this.carY = start.y; - this.carAngle = 0; + this.carAngle = this.track.nearestPoint(start.x, start.y).trackAngle; this.stepCount = 0; this.laps = 0; - this.lastProgress = 0; + this.lastProgress = this.track.nearestPoint(start.x, start.y).progress; + this.forwardProgress = 0; + this.lastLapStep = 0; + this.lapTimesSeconds = []; this.offTrack = false; this.justCompletedLap = false; this.progressDelta = 0; diff --git a/packages/demo-car-circuit/src/evaluation.ts b/packages/demo-car-circuit/src/evaluation.ts new file mode 100644 index 0000000..231d94d --- /dev/null +++ b/packages/demo-car-circuit/src/evaluation.ts @@ -0,0 +1,74 @@ +import { IgnitionEnv, type AgentInterface } from '@ignitionai/core'; +import { CircuitEnv, SIMULATION_STEP_SECONDS, type CircuitEpisode } from './circuit-env'; + +/** Fixed before checkpoint selection. Changing any setting requires a new id. */ +export const CIRCUIT_PROTOCOL = Object.freeze({ + id: 'circuit-evaluation-v1', + circuits: Object.freeze({ + training: Object.freeze({ straightLength: 10, radius: 4, halfWidth: 2 }), + test: Object.freeze({ straightLength: 6, radius: 6, halfWidth: 2 }), + }), + startWaypoints: Object.freeze([0, 16, 32]), + maxSteps: 1500, + targetLaps: 3, + stepSeconds: SIMULATION_STEP_SECONDS, +}); + +export interface CircuitEvaluationReport { + schemaVersion: 1; + protocolId: string; + policyId: string; + circuit: keyof typeof CIRCUIT_PROTOCOL.circuits; + protocol: typeof CIRCUIT_PROTOCOL; + episodes: (CircuitEpisode & { startWaypoint: number })[]; + totalTransitions: number; + completedLaps: number; + successfulEpisodes: number; + offTrackEpisodes: number; + truncatedEpisodes: number; +} + +/** Uses only the policy's greedy action interface; never calls train/remember. */ +export async function evaluateCircuit( + policy: Pick, + options: { policyId: string; circuit: keyof typeof CIRCUIT_PROTOCOL.circuits }, +): Promise { + if (!options.policyId.trim()) throw new Error('A versioned policyId is required'); + const track = CIRCUIT_PROTOCOL.circuits[options.circuit]; + if (!track) throw new Error('Unknown evaluation circuit'); + const episodes: CircuitEvaluationReport['episodes'] = []; + for (const startWaypoint of CIRCUIT_PROTOCOL.startWaypoints) { + const world = new CircuitEnv(track.straightLength, track.radius, track.halfWidth, { + startWaypoint, maxSteps: CIRCUIT_PROTOCOL.maxSteps, targetLaps: CIRCUIT_PROTOCOL.targetLaps, + }); + const loop = new IgnitionEnv(world); + loop.agent = { + async getAction(observation) { + const action = await policy.getAction(observation, true); + if (typeof action !== 'number' || !Number.isInteger(action) || action < 0 || action > 2) { + throw new Error('Circuit policy must return a discrete action in [0, 2]'); + } + return action; + }, + remember() { throw new Error('Evaluation cannot record training experience'); }, + async train() { throw new Error('Evaluation cannot train'); }, + }; + for (let step = 0; step < CIRCUIT_PROTOCOL.maxSteps; step++) { + const result = await loop.inferStep(); + if (result.terminated || result.truncated) break; + // Resolved policy promises alone do not yield to rendering or input. + if ((step + 1) % 32 === 0) await new Promise(resolve => setTimeout(resolve, 0)); + } + if (!world.lastEpisode) throw new Error('Evaluation did not produce a completed episode'); + episodes.push({ ...world.lastEpisode, startWaypoint }); + } + return { + schemaVersion: 1, protocolId: CIRCUIT_PROTOCOL.id, policyId: options.policyId, + circuit: options.circuit, protocol: CIRCUIT_PROTOCOL, episodes, + totalTransitions: episodes.reduce((sum, e) => sum + e.transitions, 0), + completedLaps: episodes.reduce((sum, e) => sum + e.completedLaps, 0), + successfulEpisodes: episodes.filter(e => e.endReason === 'success').length, + offTrackEpisodes: episodes.filter(e => e.endReason === 'off-track').length, + truncatedEpisodes: episodes.filter(e => e.endReason === 'time-limit').length, + }; +} diff --git a/packages/demo-car-circuit/test/circuit-env.test.ts b/packages/demo-car-circuit/test/circuit-env.test.ts index 8d38fd2..29c643b 100644 --- a/packages/demo-car-circuit/test/circuit-env.test.ts +++ b/packages/demo-car-circuit/test/circuit-env.test.ts @@ -73,3 +73,23 @@ describe('CircuitEnv', () => { expect(env.done()).toBe(true); }); }); + +it('records a time limit separately from failure and preserves its transition count on reset', () => { + const env = new CircuitEnv(10, 4, 2, { maxSteps: 2 }); + env.step(1); + env.step(1); + expect(env.terminated()).toBe(false); + expect(env.truncated()).toBe(true); + env.reset(); + expect(env.lastEpisode).toEqual({ transitions: 2, completedLaps: 0, lapTimesSeconds: [], + simulationSeconds: 0.1, endReason: 'time-limit' }); + expect(env.stepCount).toBe(0); +}); + +it('returns to the configured starting position and direction', () => { + const env = new CircuitEnv(6, 6, 2, { startWaypoint: 32 }); + const initial = env.observe(); + env.step(0); + env.reset(); + expect(env.observe()).toEqual(initial); +}); diff --git a/packages/demo-car-circuit/test/evaluation.test.ts b/packages/demo-car-circuit/test/evaluation.test.ts new file mode 100644 index 0000000..98120e9 --- /dev/null +++ b/packages/demo-car-circuit/test/evaluation.test.ts @@ -0,0 +1,53 @@ +import { expect, it, vi } from 'vitest'; +import { evaluateCircuit, CIRCUIT_PROTOCOL } from '../src/evaluation'; + +it('evaluates a fixed policy reproducibly without training or recording experience', async () => { + const policy = { getAction: vi.fn(async (_observation: number[], _greedy?: boolean) => 0), train: vi.fn(), remember: vi.fn() }; + const options = { policyId: 'left-only-v1', circuit: 'test' as const }; + const first = await evaluateCircuit(policy, options); + const second = await evaluateCircuit(policy, options); + expect(second).toEqual(first); + expect(first.schemaVersion).toBe(1); + expect(first.protocolId).toBe(CIRCUIT_PROTOCOL.id); + expect(first.episodes).toHaveLength(3); + expect(first.episodes.every(e => e.endReason === 'off-track')).toBe(true); + expect(first.totalTransitions).toBeGreaterThan(0); + expect(first.offTrackEpisodes).toBe(3); + expect(first.completedLaps).toBe(0); + expect(policy.train).not.toHaveBeenCalled(); + expect(policy.remember).not.toHaveBeenCalled(); + expect(policy.getAction.mock.calls.every(call => call[1] === true)).toBe(true); + expect(JSON.parse(JSON.stringify(first))).toEqual(first); +}); + +it('declares different training and held-out circuits before evaluation', () => { + expect(CIRCUIT_PROTOCOL.circuits.training).not.toEqual(CIRCUIT_PROTOCOL.circuits.test); +}); + +it('reports complete laps and simulated lap times for a lane-following reference controller', async () => { + const report = await evaluateCircuit({ async getAction(observation) { + return observation[1] > 0.008 ? 2 : observation[1] < -0.008 ? 0 : 1; + } }, { policyId: 'reference-controller-v1', circuit: 'training' }); + expect(report.successfulEpisodes).toBe(3); + expect(report.completedLaps).toBe(9); + for (const episode of report.episodes) { + expect(episode.lapTimesSeconds).toHaveLength(3); + expect(episode.simulationSeconds).toBeCloseTo(episode.lapTimesSeconds.reduce((a, b) => a + b, 0)); + } +}); + +it('rejects invalid policy actions instead of silently treating them as straight', async () => { + await expect(evaluateCircuit({ getAction: async () => 42 }, { + policyId: 'invalid', circuit: 'test', + })).rejects.toThrow('discrete action'); +}); + +it('yields to browser tasks while evaluating without changing the report', async () => { + let browserTaskRan = false; + const task = new Promise(resolve => setTimeout(() => { browserTaskRan = true; resolve(); }, 0)); + await evaluateCircuit({ async getAction(observation) { + return observation[1] > 0.008 ? 2 : observation[1] < -0.008 ? 0 : 1; + } }, { policyId: 'reference-controller-v1', circuit: 'training' }); + expect(browserTaskRan).toBe(true); + await task; +}); diff --git a/packages/demo-car-circuit/test/track.test.ts b/packages/demo-car-circuit/test/track.test.ts index ab8fd45..add2bd9 100644 --- a/packages/demo-car-circuit/test/track.test.ts +++ b/packages/demo-car-circuit/test/track.test.ts @@ -54,3 +54,8 @@ describe('OvalTrack', () => { expect(track.totalLength).toBeLessThan(60); }); }); + +it('rejects points far beyond the end of a straight even when collinear', () => { + const track = new OvalTrack(); + expect(track.isOnTrack(100, 4)).toBe(false); +}); From 6c91096faa858c9fea28f22602609a46abb4770c Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Sun, 6 Sep 2026 17:53:33 +0200 Subject: [PATCH 03/18] feat: introduce licensed 3D vehicles and arcade driving (#13) --- packages/demo-car-circuit/README.md | 9 + packages/demo-car-circuit/src/main.tsx | 2 +- .../src/public/models/License.txt | 28 ++ .../src/public/models/PROVENANCE.md | 10 + .../src/public/models/Textures/colormap.png | Bin 0 -> 12371 bytes .../src/public/models/race.glb | Bin 0 -> 167272 bytes .../src/public/models/sedan-sports.glb | Bin 0 -> 177676 bytes .../demo-car-circuit/src/racing/RacingApp.tsx | 316 ++++++++++++++ .../src/racing/RacingScene.tsx | 220 ++++++++++ .../demo-car-circuit/src/racing/driving.ts | 145 +++++++ .../demo-car-circuit/src/racing/racing.css | 399 ++++++++++++++++++ packages/demo-car-circuit/src/vite-env.d.ts | 1 + .../test/racing-driving.test.ts | 33 ++ 13 files changed, 1162 insertions(+), 1 deletion(-) create mode 100644 packages/demo-car-circuit/README.md create mode 100644 packages/demo-car-circuit/src/public/models/License.txt create mode 100644 packages/demo-car-circuit/src/public/models/PROVENANCE.md create mode 100644 packages/demo-car-circuit/src/public/models/Textures/colormap.png create mode 100644 packages/demo-car-circuit/src/public/models/race.glb create mode 100644 packages/demo-car-circuit/src/public/models/sedan-sports.glb create mode 100644 packages/demo-car-circuit/src/racing/RacingApp.tsx create mode 100644 packages/demo-car-circuit/src/racing/RacingScene.tsx create mode 100644 packages/demo-car-circuit/src/racing/driving.ts create mode 100644 packages/demo-car-circuit/src/racing/racing.css create mode 100644 packages/demo-car-circuit/src/vite-env.d.ts create mode 100644 packages/demo-car-circuit/test/racing-driving.test.ts diff --git a/packages/demo-car-circuit/README.md b/packages/demo-car-circuit/README.md new file mode 100644 index 0000000..513648e --- /dev/null +++ b/packages/demo-car-circuit/README.md @@ -0,0 +1,9 @@ +# Circuit Racing + +Run `pnpm --filter demo-car-circuit dev` from the workspace root, then open the displayed local URL. Run tests from the root with `pnpm exec vitest run packages/demo-car-circuit/test` (the Vite root is `src`). + +The first racing slice provides two imported Kenney Car Kit vehicles (CC0), Alpine Park, a following camera and deterministic arcade driving at 60 simulation ticks per second. Accelerate with arrows/WASD/ZQSD, brake with Down/S, steer with Left/Right or A/Q/D. Escape, losing focus and changing vehicle pause the session. Both vehicles share identical physics. + +Asset license and provenance are in `src/public/models/`. The versioned nine-action driving contract is in `src/racing/driving.ts`. The legacy constant-speed environment and its evaluation tests remain available; its checkpoints are not compatible with the racing contract. + +Training, race rules, saved drivers and player-versus-trained-driver inference are subsequent slices, not yet exposed by this first slice. diff --git a/packages/demo-car-circuit/src/main.tsx b/packages/demo-car-circuit/src/main.tsx index c5bac04..1dde2ed 100644 --- a/packages/demo-car-circuit/src/main.tsx +++ b/packages/demo-car-circuit/src/main.tsx @@ -1,6 +1,6 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App'; +import App from './racing/RacingApp'; import './styles.css'; createRoot(document.getElementById('root')!).render(); diff --git a/packages/demo-car-circuit/src/public/models/License.txt b/packages/demo-car-circuit/src/public/models/License.txt new file mode 100644 index 0000000..60bca66 --- /dev/null +++ b/packages/demo-car-circuit/src/public/models/License.txt @@ -0,0 +1,28 @@ + + + Car Kit (3.1) + + Created/distributed by Kenney (www.kenney.nl) + Creation date: 02-04-2026 14:03 + + ------------------------------ + + License: (Creative Commons Zero, CC0) + http://creativecommons.org/publicdomain/zero/1.0/ + + You can use this content for personal, educational, and commercial purposes. + + Support by crediting 'Kenney' or 'www.kenney.nl' (this is not a requirement) + + ------------------------------ + + • Website : www.kenney.nl + • Donate : www.kenney.nl/donate + + • Patreon : patreon.com/kenney + + Follow on social media for updates: + + • Twitter: twitter.com/KenneyNL + • BlueSky: kenney.bsky.social + • Instagram: instagram.com/kenney_nl \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/models/PROVENANCE.md b/packages/demo-car-circuit/src/public/models/PROVENANCE.md new file mode 100644 index 0000000..f5b337f --- /dev/null +++ b/packages/demo-car-circuit/src/public/models/PROVENANCE.md @@ -0,0 +1,10 @@ +# Kenney Car Kit 3.1 + +Source: https://kenney.nl/assets/car-kit + +Archive: https://kenney.nl/media/pages/assets/car-kit/1a312ec241-1775131960/kenney_car-kit.zip + +License: CC0; original License.txt included. GLB files copied without modification. + +- race.glb: SHA256 e1408c4c6e135f5a39c5f34cbfbcac9ae21c5c491e793fdee74bccac8334d942 +- sedan-sports.glb: SHA256 2889c428ca1dd9c975c3ff760eb8757883490410555ab60b818f712f583826d6 \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/models/Textures/colormap.png b/packages/demo-car-circuit/src/public/models/Textures/colormap.png new file mode 100644 index 0000000000000000000000000000000000000000..5c4b1857dafd7abd1ca9836285f1bd589f75ccb0 GIT binary patch literal 12371 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 G)&Bw&@gJf9 literal 0 HcmV?d00001 diff --git a/packages/demo-car-circuit/src/public/models/sedan-sports.glb b/packages/demo-car-circuit/src/public/models/sedan-sports.glb new file mode 100644 index 0000000000000000000000000000000000000000..07442cbbcab7ea5967a00922409c9d4d9f359b56 GIT binary patch literal 177676 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 E0K6hsyZ`_I literal 0 HcmV?d00001 diff --git a/packages/demo-car-circuit/src/racing/RacingApp.tsx b/packages/demo-car-circuit/src/racing/RacingApp.tsx new file mode 100644 index 0000000..53bdad3 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/RacingApp.tsx @@ -0,0 +1,316 @@ +import { Component, useEffect, useRef, useState, type ReactNode } from "react"; +import { useProgress } from "@react-three/drei"; +import { DrivingWorld } from "./driving"; +import { RacingScene } from "./RacingScene"; +import "./racing.css"; + +const copy = { + fr: { + garage: "Garage", + drive: "Essais libres", + training: "Entraîner", + race: "Course", + tag: "LE LABORATOIRE DES PILOTES", + title: "Votre voiture.\nVotre trajectoire.", + intro: + "Prenez le volant sur Alpine Park. Familiarisez-vous avec le circuit avant d’entraîner votre premier pilote.", + vehicle: "VÉHICULE", + racecar: "Formule R", + sedan: "Sport GT", + equal: "Même physique. Deux caractères.", + start: "Prendre le volant", + pause: "Pause", + resume: "Reprendre", + reset: "Retour aux stands", + controls: "COMMANDES", + gas: "Accélérer", + brake: "Freiner", + steer: "Tourner", + track: "ALPINE PARK", + layout: "10 virages · Circuit école", + mode: "ESSAIS LIBRES", + hint: "Accélérez avec ↑ ou Z. Freinez avant le virage.", + loading: "Préparation du véhicule…", + error: "Le véhicule n’a pas pu être chargé.", + retry: "Réessayer", + coming: "Disponible après les essais", + paused: "Session en pause", + pausedHint: "Reprenez quand vous êtes prêt.", + speed: "VITESSE", + local: "LOCAL · NAVIGATEUR", + }, + en: { + garage: "Garage", + drive: "Free practice", + training: "Train", + race: "Race", + tag: "THE DRIVER LAB", + title: "Your car.\nYour racing line.", + intro: + "Take the wheel at Alpine Park. Learn the circuit before training your first driver.", + vehicle: "VEHICLE", + racecar: "Formula R", + sedan: "Sport GT", + equal: "Same physics. Two characters.", + start: "Start driving", + pause: "Pause", + resume: "Resume", + reset: "Back to pits", + controls: "CONTROLS", + gas: "Accelerate", + brake: "Brake", + steer: "Steer", + track: "ALPINE PARK", + layout: "10 corners · Training circuit", + mode: "FREE PRACTICE", + hint: "Accelerate with ↑ or W. Brake before the corner.", + loading: "Preparing your vehicle…", + error: "The vehicle could not be loaded.", + retry: "Retry", + coming: "Available after practice", + paused: "Session paused", + pausedHint: "Resume when you are ready.", + speed: "SPEED", + local: "LOCAL · BROWSER", + }, +}; +class SceneBoundary extends Component< + { children: ReactNode; message: string; retry: string }, + { error: boolean } +> { + state = { error: false }; + static getDerivedStateFromError() { + return { error: true }; + } + render() { + return this.state.error ? ( +
+

{this.props.message}

+ +
+ ) : ( + this.props.children + ); + } +} +function Loading({ label }: { label: string }) { + const { active, progress } = useProgress(); + return active ? ( +
+ {label} + +
+ ) : null; +} +export default function RacingApp() { + const [lang, setLang] = useState<"fr" | "en">("fr"), + t = copy[lang]; + const [world] = useState(() => new DrivingWorld()); + const keys = useRef(new Set()).current; + const [started, setStarted] = useState(false), + [paused, setPaused] = useState(true), + [model, setModel] = useState("race"), + [speed, setSpeed] = useState(0); + useEffect(() => { + const handled = new Set([ + "ArrowUp", + "ArrowDown", + "ArrowLeft", + "ArrowRight", + "KeyW", + "KeyA", + "KeyS", + "KeyD", + "KeyZ", + "KeyQ", + ]); + const down = (e: KeyboardEvent) => { + if (e.code === "Escape") { + keys.clear(); + setPaused(true); + return; + } + if ( + e.target instanceof HTMLElement && + ["INPUT", "SELECT", "TEXTAREA", "BUTTON"].includes(e.target.tagName) + ) + return; + if (handled.has(e.code)) { + e.preventDefault(); + keys.add(e.code); + } + }; + const up = (e: KeyboardEvent) => keys.delete(e.code); + const blur = () => { + keys.clear(); + setPaused(true); + }; + window.addEventListener("keydown", down); + window.addEventListener("keyup", up); + window.addEventListener("blur", blur); + document.addEventListener("visibilitychange", blur); + return () => { + window.removeEventListener("keydown", down); + window.removeEventListener("keyup", up); + window.removeEventListener("blur", blur); + document.removeEventListener("visibilitychange", blur); + keys.clear(); + }; + }, [keys]); + return ( +
+
+ + ignition / RACING + + + +
+
+ +
+ + + + +
+ {t.track} + {t.layout} +
+
+ + {t.mode} +
+ {started && paused && ( +
+

{t.paused}

+

{t.pausedHint}

+ +
+ )} +
{t.hint}
+
+ {t.speed} + {Math.round(speed).toString().padStart(3, "0")} + KM/H +
+ +
+
+
+
+
+ ); +} diff --git a/packages/demo-car-circuit/src/racing/RacingScene.tsx b/packages/demo-car-circuit/src/racing/RacingScene.tsx new file mode 100644 index 0000000..d868611 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/RacingScene.tsx @@ -0,0 +1,220 @@ +import { Suspense, useMemo, useRef } from "react"; +import { Canvas, useFrame } from "@react-three/fiber"; +import { useGLTF, useProgress } from "@react-three/drei"; +import * as THREE from "three"; +import { DrivingWorld, DRIVING_CONTRACT, RacingTrack } from "./driving"; + +export function Vehicle({ + world, + model, +}: { + world: DrivingWorld; + model: string; +}) { + const { scene } = useGLTF(`${import.meta.env.BASE_URL}models/${model}.glb`); + const group = useRef(null); + const object = useMemo(() => { + const copy = scene.clone(true), + box = new THREE.Box3().setFromObject(copy), + size = box.getSize(new THREE.Vector3()); + const scale = 4.4 / Math.max(size.x, size.z); + copy.scale.setScalar(scale); + const center = box.getCenter(new THREE.Vector3()); + copy.position.set(-center.x * scale, -box.min.y * scale, -center.z * scale); + copy.traverse((o) => { + if (o instanceof THREE.Mesh) { + o.castShadow = true; + o.receiveShadow = true; + } + }); + return copy; + }, [scene]); + useFrame(() => { + if (group.current) { + const c = world.car; + group.current.position.set(c.x, 0.04, c.z); + group.current.rotation.y = -c.angle + Math.PI / 2; + } + }); + return ( + + + + ); +} +function Road({ track }: { track: RacingTrack }) { + const geometry = useMemo(() => { + const positions: number[] = [], + colors: number[] = []; + for (let i = 0; i < 400; i++) { + const a = track.sample(i / 400), + b = track.sample((i + 1) / 400); + for (const [l, r, color] of [ + [-5.3, -4.5, i % 8 < 4 ? "#f4efdf" : "#d74332"], + [-4.5, 4.5, "#30383b"], + [4.5, 5.3, i % 8 < 4 ? "#f4efdf" : "#d74332"], + ] as [number, number, string][]) { + const v = (p: typeof a, s: number) => [ + p.x - Math.sin(p.angle) * s, + 0.02, + p.z + Math.cos(p.angle) * s, + ]; + const verts = [v(a, l), v(a, r), v(b, r), v(a, l), v(b, r), v(b, l)], + c = new THREE.Color(color); + verts.forEach((p) => { + positions.push(...p); + colors.push(c.r, c.g, c.b); + }); + } + } + const g = new THREE.BufferGeometry(); + g.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); + g.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); + g.computeVertexNormals(); + return g; + }, [track]); + return ( + <> + + + + {Array.from({ length: 80 }, (_, i) => { + const p = track.sample(i / 80), + side = i % 2 ? 1 : -1; + return ( + + + + + ); + })} + {Array.from({ length: 10 }, (_, i) => { + const p = track.sample(0); + return ( + + + + + ); + })} + + ); +} +function Driver({ + world, + keys, + paused, + onStats, +}: { + world: DrivingWorld; + keys: Set; + paused: boolean; + onStats: (speed: number) => void; +}) { + const { active: loading } = useProgress(); + const accumulator = useRef(0), + frames = useRef(0), + cameraReady = useRef(false); + useFrame(({ camera }, delta) => { + if (!paused && !loading) { + accumulator.current += Math.min(delta, 0.1); + while (accumulator.current >= DRIVING_CONTRACT.dt) { + const steer = + (keys.has("ArrowRight") || keys.has("KeyD") ? 1 : 0) - + (keys.has("ArrowLeft") || keys.has("KeyA") || keys.has("KeyQ") + ? 1 + : 0); + const throttle = + keys.has("ArrowDown") || keys.has("KeyS") + ? -1 + : keys.has("ArrowUp") || keys.has("KeyW") || keys.has("KeyZ") + ? 1 + : 0; + world.step((throttle + 1) * 3 + steer + 1); + accumulator.current -= DRIVING_CONTRACT.dt; + } + } else accumulator.current = 0; + const c = world.car, + target = new THREE.Vector3( + c.x - Math.cos(c.angle) * 12, + 7.5, + c.z - Math.sin(c.angle) * 12, + ); + if (!cameraReady.current) { + camera.position.copy(target); + cameraReady.current = true; + } else camera.position.lerp(target, 1 - Math.exp(-delta * 5)); + camera.lookAt( + c.x + Math.cos(c.angle) * 8, + 0.8, + c.z + Math.sin(c.angle) * 8, + ); + if (++frames.current % 6 === 0) onStats(c.speed * 3.6); + }); + return null; +} +export function RacingScene(props: { + world: DrivingWorld; + keys: Set; + paused: boolean; + model: string; + onStats: (speed: number) => void; +}) { + return ( + + + + + + + + + + {Array.from({ length: 24 }, (_, i) => { + const a = (i * Math.PI * 2) / 24; + return ( + + + + + ); + })} + + + + + + + ); +} diff --git a/packages/demo-car-circuit/src/racing/driving.ts b/packages/demo-car-circuit/src/racing/driving.ts new file mode 100644 index 0000000..5b17933 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/driving.ts @@ -0,0 +1,145 @@ +import { CatmullRomCurve3, Vector3 } from "three"; + +export const DRIVING_CONTRACT = Object.freeze({ + id: "circuit-racing-v1", + dt: 1 / 60, + actionCount: 9, + maxSpeed: 32, +}); +export interface RoadSample { + x: number; + z: number; + angle: number; + distance: number; + progress: number; +} +export class RacingTrack { + readonly width = 9; + readonly points: Vector3[]; + readonly length: number; + readonly id: string; + constructor(test = false) { + this.id = test ? "harbour-test-v1" : "alpine-park-v1"; + const coords = test + ? [ + [-60, -35], + [0, -40], + [55, -25], + [60, 20], + [20, 35], + [0, 10], + [-40, 40], + [-65, 10], + ] + : [ + [-30, -32], + [20, -32], + [55, -20], + [60, 10], + [30, 20], + [12, 40], + [-25, 40], + [-55, 20], + [-62, -10], + [-55, -30], + ]; + const curve = new CatmullRomCurve3( + coords.map(([x, z]) => new Vector3(x, 0, z)), + true, + "catmullrom", + 0.35, + ); + this.points = curve.getSpacedPoints(400); + this.length = curve.getLength(); + } + sample(progress: number): RoadSample { + const p = (((progress % 1) + 1) % 1) * 400, + i = Math.floor(p), + a = this.points[i], + b = this.points[i + 1], + t = p - i; + return { + x: a.x + (b.x - a.x) * t, + z: a.z + (b.z - a.z) * t, + angle: Math.atan2(b.z - a.z, b.x - a.x), + distance: 0, + progress: p / 400, + }; + } + nearest(x: number, z: number): RoadSample { + let best = Infinity, + result = this.sample(0); + for (let i = 0; i < 400; i++) { + const a = this.points[i], + b = this.points[i + 1], + dx = b.x - a.x, + dz = b.z - a.z; + const t = Math.max( + 0, + Math.min(1, ((x - a.x) * dx + (z - a.z) * dz) / (dx * dx + dz * dz)), + ); + const px = a.x + t * dx, + pz = a.z + t * dz, + d = Math.hypot(x - px, z - pz); + if (d < best) { + best = d; + result = { + x: px, + z: pz, + angle: Math.atan2(dz, dx), + distance: d, + progress: (i + t) / 400, + }; + } + } + return result; + } +} +export interface CarState { + x: number; + z: number; + angle: number; + speed: number; + steering: number; +} +export class DrivingWorld { + readonly track: RacingTrack; + car: CarState; + ticks = 0; + constructor(track = new RacingTrack()) { + this.track = track; + this.car = this.start(); + } + private start(): CarState { + const p = this.track.sample(0); + return { x: p.x, z: p.z, angle: p.angle, speed: 0, steering: 0 }; + } + reset() { + this.car = this.start(); + this.ticks = 0; + } + step(action: number) { + if (!Number.isInteger(action) || action < 0 || action >= 9) + throw new Error("Driving action must be an integer in [0,8]"); + const steer = (action % 3) - 1, + throttle = Math.floor(action / 3) - 1, + c = this.car, + dt = DRIVING_CONTRACT.dt; + const offRoad = + this.track.nearest(c.x, c.z).distance > this.track.width / 2; + c.steering += (steer - c.steering) * Math.min(1, dt * 9); + const acceleration = throttle === 1 ? 9 : throttle === -1 ? -20 : 0; + c.speed = Math.max( + 0, + Math.min( + DRIVING_CONTRACT.maxSpeed, + c.speed + + (acceleration - 0.7 - c.speed * 0.045 - (offRoad ? c.speed * 0.8 : 0)) * dt, + ), + ); + c.angle += c.steering * c.speed * 0.035 * dt; + c.x += Math.cos(c.angle) * c.speed * dt; + c.z += Math.sin(c.angle) * c.speed * dt; + this.ticks++; + } +} diff --git a/packages/demo-car-circuit/src/racing/racing.css b/packages/demo-car-circuit/src/racing/racing.css new file mode 100644 index 0000000..aafa3b8 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/racing.css @@ -0,0 +1,399 @@ +:root { + --race-bg: #121619; + --race-panel: #1b2024; + --race-line: #323a3f; + --race-text: #f3f1e8; + --race-muted: #9ca6aa; + --race-accent: #e6ee58; + --race-space: 4px; +} +* { + box-sizing: border-box; +} +.racing-app { + background: var(--race-bg); + color: var(--race-text); + min-height: 100vh; + font-family: Arial, Helvetica, sans-serif; +} +.racing-app button, +.racing-app a { + font: inherit; +} +.racing-app button { + cursor: pointer; +} +.racing-app button:focus-visible, +.racing-app a:focus-visible { + outline: 3px solid var(--race-accent); + outline-offset: 4px; +} +.race-header { + height: 76px; + display: flex; + align-items: center; + justify-content: space-between; + border-bottom: 1px solid var(--race-line); + padding: 0 32px; + gap: 24px; +} +.race-brand { + color: var(--race-text); + font-size: 24px; + font-weight: 800; + text-decoration: none; + letter-spacing: -1px; +} +.race-brand span { + font-size: 12px; + color: var(--race-accent); + letter-spacing: 2px; + margin-left: 16px; +} +.race-header nav { + display: flex; + gap: 32px; + align-items: center; + height: 100%; +} +.race-header nav .active { + border-bottom: 3px solid var(--race-accent); + height: 100%; + display: flex; + align-items: center; + font-size: 14px; +} +.race-header nav button { + background: none; + border: 0; + color: var(--race-muted); + font-size: 14px; + opacity: 0.55; + cursor: not-allowed; +} +.language { + color: var(--race-text); + background: none; + border: 1px solid var(--race-line); + padding: 8px 12px; + font-size: 12px !important; +} +.race-layout { + display: grid; + grid-template-columns: 320px 1fr; + min-height: calc(100vh - 76px); +} +.race-sidebar { + padding: 40px 28px 24px; + display: flex; + flex-direction: column; + border-right: 1px solid var(--race-line); +} +.eyebrow { + font-size: 9px; + font-weight: 700; + letter-spacing: 2px; + color: var(--race-accent); +} +.race-sidebar h1 { + font-size: 32px; + letter-spacing: -1.5px; + line-height: 1.05; + margin: 20px 0; +} +.race-intro { + font-size: 13px; + line-height: 1.6; + color: var(--race-muted); + margin: 0 0 32px; +} +.vehicle-label { + font-size: 10px; + letter-spacing: 2px; + color: var(--race-muted); + margin-bottom: 12px; +} +.vehicle-options { + display: grid; + gap: 8px; +} +.vehicle-options button { + display: flex; + gap: 16px; + align-items: center; + padding: 16px 12px; + border: 1px solid var(--race-line); + background: var(--race-panel); + color: var(--race-text); + font-size: 13px; +} +.vehicle-options button.selected { + border-color: var(--race-accent); +} +.vehicle-options button span { + font-size: 10px; + color: var(--race-muted); +} +.vehicle-options b { + margin-left: auto; + color: var(--race-accent); +} +.muted { + font-size: 10px; + color: var(--race-muted); + margin: 12px 0 24px; +} +.drive-button { + display: flex; + justify-content: space-between; + background: var(--race-accent); + color: var(--race-bg); + border: 0; + padding: 18px 16px; + font-weight: 700 !important; + font-size: 13px !important; +} +.reset-button { + border: 0; + background: none; + color: var(--race-muted); + padding: 16px; + font-size: 11px !important; +} +.race-controls { + margin-top: 24px; + border-top: 1px solid var(--race-line); + padding-top: 24px; +} +.race-controls p { + display: flex; + align-items: center; + justify-content: space-between; + font-size: 11px; + color: var(--race-muted); + margin: 8px 0; +} +.race-controls kbd { + background: var(--race-panel); + border: 1px solid var(--race-line); + padding: 5px 8px; + color: var(--race-text); + min-width: 76px; + text-align: center; + font-size: 10px; +} +.local-badge { + margin-top: auto; + padding-top: 32px; + color: var(--race-muted); + font-size: 9px; + letter-spacing: 1px; +} +.local-badge i, +.session-pill i { + display: inline-block; + width: 6px; + height: 6px; + background: var(--race-accent); + border-radius: 50%; + margin-right: 8px; +} +.race-viewport { + position: relative; + min-width: 0; + height: calc(100vh - 76px); + min-height: 560px; + overflow: hidden; +} +.track-label { + position: absolute; + top: 28px; + left: 28px; + background: #172127e6; + padding: 14px 20px; + border-left: 3px solid var(--race-accent); +} +.track-label span { + display: block; + font-size: 14px; + font-weight: 800; + letter-spacing: 2px; +} +.track-label small { + display: block; + color: #bac4c6; + font-size: 10px; + margin-top: 6px; +} +.session-pill { + position: absolute; + top: 28px; + right: 28px; + font-size: 10px; + letter-spacing: 1px; + background: #172127e6; + padding: 14px; +} +.driving-hint { + position: absolute; + bottom: 32px; + left: 28px; + background: #172127e6; + padding: 14px 20px; + font-size: 11px; + color: #cdd6d7; + max-width: 55%; +} +.speedometer { + position: absolute; + bottom: 28px; + right: 28px; + background: #172127f2; + padding: 20px 24px; + width: 164px; +} +.speedometer > span { + font-size: 9px; + letter-spacing: 2px; + color: var(--race-muted); +} +.speedometer strong { + display: block; + font-size: 56px; + line-height: 1.1; + letter-spacing: -4px; + font-variant-numeric: tabular-nums; + font-style: italic; +} +.speedometer small { + font-size: 10px; + color: var(--race-muted); +} +.speed-track { + height: 3px; + background: var(--race-line); + margin-top: 16px; + overflow: hidden; +} +.speed-track i { + height: 100%; + display: block; + background: var(--race-accent); +} +.pause-overlay, +.race-error { + position: absolute; + inset: 0; + background: #111820a8; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 4; +} +.pause-overlay h2 { + font-size: 36px; + margin: 0 0 8px; + letter-spacing: -1px; +} +.pause-overlay p { + color: #cad1d1; + font-size: 13px; +} +.pause-overlay button, +.race-error button { + background: var(--race-accent); + border: 0; + padding: 16px 28px; + margin-top: 16px; + color: #121619; +} +.race-loading { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + background: #1b2024; + z-index: 5; + font-size: 13px; +} +.race-loading progress { + accent-color: var(--race-accent); + width: 220px; +} +@media (max-width: 900px) { + .race-layout { + grid-template-columns: 250px 1fr; + } + .race-sidebar { + padding: 28px 20px; + } + .race-header { + padding: 0 20px; + } + .race-header nav { + gap: 16px; + } + .session-pill { + display: none; + } + .race-sidebar h1 { + font-size: 28px; + } + .driving-hint { + display: none; + } +} +@media (max-width: 600px) { + .race-layout { + display: flex; + flex-direction: column-reverse; + } + .race-viewport { + height: 55vh; + min-height: 360px; + } + .race-header { + height: 60px; + padding: 0 16px; + } + .race-brand { + font-size: 20px; + } + .race-brand span { + font-size: 9px; + margin-left: 8px; + } + .race-header nav { + display: none; + } + .race-sidebar { + border: 0; + padding: 24px; + } + .race-sidebar h1 { + font-size: 28px; + } + .track-label { + top: 16px; + left: 16px; + } + .speedometer { + bottom: 16px; + right: 16px; + width: 128px; + padding: 12px 16px; + } + .speedometer strong { + font-size: 44px; + } + .race-controls { + margin-top: 8px; + } + .local-badge { + padding-top: 16px; + } +} diff --git a/packages/demo-car-circuit/src/vite-env.d.ts b/packages/demo-car-circuit/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/packages/demo-car-circuit/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/demo-car-circuit/test/racing-driving.test.ts b/packages/demo-car-circuit/test/racing-driving.test.ts new file mode 100644 index 0000000..2efc509 --- /dev/null +++ b/packages/demo-car-circuit/test/racing-driving.test.ts @@ -0,0 +1,33 @@ +import { it, expect } from "vitest"; +import { DrivingWorld, DRIVING_CONTRACT } from "../src/racing/driving"; + +it("accelerates, brakes and advances on the public fixed-step contract", () => { + const world = new DrivingWorld(); + const start = { ...world.car }; + for (let i = 0; i < 60; i++) world.step(7); + expect(world.car.speed).toBeGreaterThan(5); + expect( + Math.hypot(world.car.x - start.x, world.car.z - start.z), + ).toBeGreaterThan(2); + const speed = world.car.speed; + for (let i = 0; i < 20; i++) world.step(1); + expect(world.car.speed).toBeLessThan(speed); + expect(DRIVING_CONTRACT.actionCount).toBe(9); +}); +it("repeats the same trajectory with the same commands", () => { + const a = new DrivingWorld(), + b = new DrivingWorld(); + for (let i = 0; i < 100; i++) { + a.step(i < 50 ? 7 : 8); + b.step(i < 50 ? 7 : 8); + } + expect(a.car).toEqual(b.car); + expect(() => a.step(42)).toThrow(); +}); + +it('can recover from a stationary off-road position', () => { + const world = new DrivingWorld(); + world.car.z -= 12; + for (let i=0;i<60;i++) world.step(7); + expect(world.car.speed).toBeGreaterThan(1); +}); From 24a2a8c7a7d4caab659df61608561518d226a3bb Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Sun, 6 Sep 2026 18:03:28 +0200 Subject: [PATCH 04/18] feat: add shared racing rules and verified four-car races (#14) --- packages/demo-car-circuit/README.md | 7 +- .../demo-car-circuit/src/racing/RacingApp.tsx | 99 +++++++++- .../src/racing/RacingScene.tsx | 33 +++- .../demo-car-circuit/src/racing/driving.ts | 8 +- packages/demo-car-circuit/src/racing/race.ts | 172 ++++++++++++++++++ .../demo-car-circuit/src/racing/racing.css | 51 ++++++ .../demo-car-circuit/src/racing/reference.ts | 17 ++ .../demo-car-circuit/test/racing-race.test.ts | 80 ++++++++ 8 files changed, 451 insertions(+), 16 deletions(-) create mode 100644 packages/demo-car-circuit/src/racing/race.ts create mode 100644 packages/demo-car-circuit/src/racing/reference.ts create mode 100644 packages/demo-car-circuit/test/racing-race.test.ts diff --git a/packages/demo-car-circuit/README.md b/packages/demo-car-circuit/README.md index 513648e..8c9519d 100644 --- a/packages/demo-car-circuit/README.md +++ b/packages/demo-car-circuit/README.md @@ -6,4 +6,9 @@ The first racing slice provides two imported Kenney Car Kit vehicles (CC0), Alpi Asset license and provenance are in `src/public/models/`. The versioned nine-action driving contract is in `src/racing/driving.ts`. The legacy constant-speed environment and its evaluation tests remain available; its checkpoints are not compatible with the racing contract. -Training, race rules, saved drivers and player-versus-trained-driver inference are subsequent slices, not yet exposed by this first slice. +Course mode adds a three-second countdown, three laps through 20 ordered checkpoints, standings and results. Up to four cars use a shared decision batch and physics clock. Ordered gates reject skipped checkpoints; brief cuts between gates are penalized rather than physically forbidden. Off-road excursions cost two seconds; a five-second off-road timeout rescues the vehicle to its last validated checkpoint and adds five seconds. Car contacts separate their collision bodies and reduce speed. A race stops at five simulated minutes or when all drivers finish. Equal times are ordered by stable driver ID. + +The observation mode explicitly uses four **rule-based reference controllers**, not learned policies. Training, saved learned drivers and player-versus-trained-driver inference remain subsequent slices. + +The frozen racing evaluation protocol names Alpine Park for training, Harbour for held-out testing, and seeds 101/307/509 before learned checkpoint selection. The original constant-speed evaluation has its separate version. + diff --git a/packages/demo-car-circuit/src/racing/RacingApp.tsx b/packages/demo-car-circuit/src/racing/RacingApp.tsx index 53bdad3..9e34e50 100644 --- a/packages/demo-car-circuit/src/racing/RacingApp.tsx +++ b/packages/demo-car-circuit/src/racing/RacingApp.tsx @@ -1,5 +1,6 @@ import { Component, useEffect, useRef, useState, type ReactNode } from "react"; import { useProgress } from "@react-three/drei"; +import { RaceWorld } from "./race"; import { DrivingWorld } from "./driving"; import { RacingScene } from "./RacingScene"; import "./racing.css"; @@ -105,7 +106,22 @@ function Loading({ label }: { label: string }) { export default function RacingApp() { const [lang, setLang] = useState<"fr" | "en">("fr"), t = copy[lang]; - const [world] = useState(() => new DrivingWorld()); + const [world, setWorld] = useState(() => new DrivingWorld()); + const [race, setRace] = useState(); + const [reference, setReference] = useState(false); + const [, updateHUD] = useState(0); + const resetSession = (competitive: boolean, references = false) => { + const next = competitive + ? new RaceWorld({ count: references ? 4 : 1 }) + : undefined; + setReference(references); + setRace(next); + setWorld(next ? next.drivers[0].world : new DrivingWorld()); + keys.clear(); + setSpeed(0); + setPaused(true); + setStarted(false); + }; const keys = useRef(new Set()).current; const [started, setStarted] = useState(false), [paused, setPaused] = useState(true), @@ -164,11 +180,19 @@ export default function RacingApp() { ignition / RACING @@ -234,14 +258,25 @@ export default function RacingApp() { className="reset-button" onClick={() => { keys.clear(); - world.reset(); - setSpeed(0); - setPaused(true); - setStarted(false); + resetSession(!!race, reference); }} > {t.reset} + {race && ( + + )}
{t.controls}

@@ -273,7 +308,12 @@ export default function RacingApp() { keys={keys} paused={paused} model={model} - onStats={setSpeed} + race={race} + reference={reference} + onStats={(value) => { + setSpeed(value); + updateHUD((n) => n + 1); + }} /> @@ -283,9 +323,48 @@ export default function RacingApp() {

- {t.mode} + {race + ? `${reference ? (lang === "fr" ? "RÉFÉRENCES À RÈGLES" : "RULE-BASED REFERENCES") : t.race} · ${Math.min(3, race.drivers[0].laps + 1)}/3` + : t.mode}
- {started && paused && ( + {race && started && !paused && race.countdown > 0 && ( +
+ {Math.ceil(race.countdown)} +
+ )} + {race && ( +
+ {(race.elapsedTicks / 60).toFixed(1)} s · + + {race.drivers[0].penaltySeconds} s +
+ )} + {race?.finished && ( +
+

{lang === "fr" ? "Résultat" : "Result"}

+

+ {race.drivers[0].finishSeconds === null + ? lang === "fr" + ? "Temps limite atteint" + : "Time limit reached" + : `${race.drivers[0].finishSeconds.toFixed(2)} s · 3/3`} +

+

+ {lang === "fr" ? "Remises en piste" : "Rescues"}:{" "} + {race.drivers[0].rescues} (+{race.drivers[0].penaltySeconds} s) +

+
    + {race.standings.map((d) => ( +
  1. + #{d.id + 1} · {d.finishSeconds?.toFixed(2) ?? "DNF"} s +
  2. + ))} +
+ +
+ )} + {started && paused && !race?.finished && (

{t.paused}

{t.pausedHint}

diff --git a/packages/demo-car-circuit/src/racing/RacingScene.tsx b/packages/demo-car-circuit/src/racing/RacingScene.tsx index d868611..78f62b9 100644 --- a/packages/demo-car-circuit/src/racing/RacingScene.tsx +++ b/packages/demo-car-circuit/src/racing/RacingScene.tsx @@ -1,7 +1,9 @@ -import { Suspense, useMemo, useRef } from "react"; +import { Suspense, useEffect, useMemo, useRef } from "react"; import { Canvas, useFrame } from "@react-three/fiber"; import { useGLTF, useProgress } from "@react-three/drei"; import * as THREE from "three"; +import { referenceAction } from "./reference"; +import { RaceWorld } from "./race"; import { DrivingWorld, DRIVING_CONTRACT, RacingTrack } from "./driving"; export function Vehicle({ @@ -73,6 +75,7 @@ function Road({ track }: { track: RacingTrack }) { g.computeVertexNormals(); return g; }, [track]); + useEffect(() => () => geometry.dispose(), [geometry]); return ( <> @@ -126,11 +129,15 @@ function Driver({ keys, paused, onStats, + race, + reference = false, }: { world: DrivingWorld; keys: Set; paused: boolean; onStats: (speed: number) => void; + race?: RaceWorld; + reference?: boolean; }) { const { active: loading } = useProgress(); const accumulator = useRef(0), @@ -151,7 +158,14 @@ function Driver({ : keys.has("ArrowUp") || keys.has("KeyW") || keys.has("KeyZ") ? 1 : 0; - world.step((throttle + 1) * 3 + steer + 1); + const action = (throttle + 1) * 3 + steer + 1; + if (race) + race.step( + race.drivers.map((d, i) => + reference || i > 0 ? referenceAction(d.world) : action, + ), + ); + else world.step(action); accumulator.current -= DRIVING_CONTRACT.dt; } } else accumulator.current = 0; @@ -180,6 +194,8 @@ export function RacingScene(props: { paused: boolean; model: string; onStats: (speed: number) => void; + race?: RaceWorld; + reference?: boolean; }) { return ( @@ -204,7 +220,11 @@ export function RacingScene(props: { {Array.from({ length: 24 }, (_, i) => { const a = (i * Math.PI * 2) / 24; return ( - + @@ -213,6 +233,13 @@ export function RacingScene(props: { + {props.race?.drivers.slice(1).map((d) => ( + + ))} diff --git a/packages/demo-car-circuit/src/racing/driving.ts b/packages/demo-car-circuit/src/racing/driving.ts index 5b17933..de86e6e 100644 --- a/packages/demo-car-circuit/src/racing/driving.ts +++ b/packages/demo-car-circuit/src/racing/driving.ts @@ -134,10 +134,14 @@ export class DrivingWorld { Math.min( DRIVING_CONTRACT.maxSpeed, c.speed + - (acceleration - 0.7 - c.speed * 0.045 - (offRoad ? c.speed * 0.8 : 0)) * dt, + (acceleration - + 0.7 - + c.speed * 0.045 - + (offRoad ? c.speed * 0.8 : 0)) * + dt, ), ); - c.angle += c.steering * c.speed * 0.035 * dt; + c.angle += c.steering * c.speed * 0.12 * dt; c.x += Math.cos(c.angle) * c.speed * dt; c.z += Math.sin(c.angle) * c.speed * dt; this.ticks++; diff --git a/packages/demo-car-circuit/src/racing/race.ts b/packages/demo-car-circuit/src/racing/race.ts new file mode 100644 index 0000000..9b94f7a --- /dev/null +++ b/packages/demo-car-circuit/src/racing/race.ts @@ -0,0 +1,172 @@ +import { DrivingWorld, RacingTrack, DRIVING_CONTRACT } from "./driving"; + +export const RACE_PROTOCOL = Object.freeze({ + id: "circuit-race-v1", + laps: 3, + checkpoints: 20, + rescueTicks: 300, + rescuePenalty: 5, + offroadPenalty: 2, + maxTicks: 60 * 300, + trainingTrack: "alpine-park-v1", + heldOutTrack: "harbour-test-v1", + evaluationSeeds: [101, 307, 509], +}); +export interface RaceDriver { + id: number; + world: DrivingWorld; + laps: number; + gates: number; + lastProgress: number; + safeProgress: number; + offroadTicks: number; + penaltySeconds: number; + rescues: number; + collisions: number; + finishSeconds: number | null; +} +/** All decisions are supplied as one batch, before any physics is advanced. */ +export class RaceWorld { + readonly track: RacingTrack; + readonly drivers: RaceDriver[]; + readonly countdownTicks: number; + ticks = 0; + elapsedTicks = 0; + constructor({ + count = 1, + test = false, + countdown = 3, + }: { count?: number; test?: boolean; countdown?: number } = {}) { + if (!Number.isInteger(count) || count < 1 || count > 4) + throw new Error("Race needs 1–4 drivers"); + this.track = new RacingTrack(test); + this.countdownTicks = Math.round(countdown / DRIVING_CONTRACT.dt); + this.drivers = Array.from({ length: count }, (_, id) => { + const world = new DrivingWorld(this.track); + const p = this.track.sample(0); + const side = count === 1 ? 0 : id % 2 ? 1.6 : -1.6; + const back = Math.floor(id / 2) * 5; + world.car.x += -Math.sin(p.angle) * side - Math.cos(p.angle) * back; + world.car.z += Math.cos(p.angle) * side - Math.sin(p.angle) * back; + return { + id, + world, + laps: 0, + gates: 0, + lastProgress: this.track.nearest(world.car.x, world.car.z).progress, + safeProgress: 0, + offroadTicks: 0, + penaltySeconds: 0, + rescues: 0, + collisions: 0, + finishSeconds: null, + }; + }); + } + get finished() { + return ( + this.drivers.every((d) => d.finishSeconds !== null) || + this.elapsedTicks >= RACE_PROTOCOL.maxTicks + ); + } + get countdown() { + return Math.max( + 0, + (this.countdownTicks - this.ticks) * DRIVING_CONTRACT.dt, + ); + } + get standings() { + return [...this.drivers].sort((a, b) => { + if (a.finishSeconds !== null || b.finishSeconds !== null) + return ( + (a.finishSeconds ?? Infinity) - (b.finishSeconds ?? Infinity) || + a.id - b.id + ); + const remaining = (d: RaceDriver) => + (((d.gates + 1) % RACE_PROTOCOL.checkpoints) / + RACE_PROTOCOL.checkpoints - + d.lastProgress + + 1) % + 1; + return b.gates - a.gates || remaining(a) - remaining(b) || a.id - b.id; + }); + } + step(actions: readonly number[]) { + if ( + actions.length !== this.drivers.length || + actions.some((a) => !Number.isInteger(a) || a < 0 || a > 8) + ) + throw new Error("Supply one valid action per driver"); + if (this.finished) return; + this.ticks++; + if (this.ticks <= this.countdownTicks) return; + this.elapsedTicks++; + for (const d of this.drivers) + if (d.finishSeconds === null) d.world.step(actions[d.id]); + for (let i = 0; i < this.drivers.length; i++) + for (let j = i + 1; j < this.drivers.length; j++) { + const da = this.drivers[i], + db = this.drivers[j]; + if (da.finishSeconds !== null || db.finishSeconds !== null) continue; + const a = da.world.car, + b = db.world.car, + dx = b.x - a.x, + dz = b.z - a.z, + dist = Math.hypot(dx, dz); + if (dist >= 2.4) continue; + const nx = dist > 1e-6 ? dx / dist : 1, + nz = dist > 1e-6 ? dz / dist : 0, + push = (2.4 - dist) / 2; + a.x -= nx * push; + a.z -= nz * push; + b.x += nx * push; + b.z += nz * push; + a.speed *= 0.7; + b.speed *= 0.7; + da.collisions++; + db.collisions++; + } + for (const d of this.drivers) { + if (d.finishSeconds !== null) continue; + const c = d.world.car, + p = this.track.nearest(c.x, c.z); + let delta = p.progress - d.lastProgress; + if (delta > 0.5) delta -= 1; + if (delta < -0.5) delta += 1; + const forward = delta > 0 && delta * this.track.length < 2; + const next = + ((d.gates + 1) % RACE_PROTOCOL.checkpoints) / RACE_PROTOCOL.checkpoints; + const distanceToGate = (next - d.lastProgress + 1) % 1; + if ( + p.distance <= this.track.width / 2 && + forward && + distanceToGate > 0 && + distanceToGate <= delta + 1e-8 + ) { + d.gates++; + d.safeProgress = next; + d.laps = Math.floor(d.gates / RACE_PROTOCOL.checkpoints); + if (d.laps >= RACE_PROTOCOL.laps) + d.finishSeconds = + this.elapsedTicks * DRIVING_CONTRACT.dt + d.penaltySeconds; + } + d.lastProgress = p.progress; + if (p.distance > this.track.width / 2 && d.offroadTicks === 0) + d.penaltySeconds += RACE_PROTOCOL.offroadPenalty; + d.offroadTicks = + p.distance > this.track.width / 2 ? d.offroadTicks + 1 : 0; + if (d.offroadTicks >= RACE_PROTOCOL.rescueTicks) { + const safe = this.track.sample(d.safeProgress); + c.x = safe.x; + c.z = safe.z; + c.angle = safe.angle; + c.speed = 0; + c.steering = 0; + d.lastProgress = safe.progress; + d.offroadTicks = 0; + d.rescues++; + d.penaltySeconds += RACE_PROTOCOL.rescuePenalty; + } + } + } +} diff --git a/packages/demo-car-circuit/src/racing/racing.css b/packages/demo-car-circuit/src/racing/racing.css index aafa3b8..ba102aa 100644 --- a/packages/demo-car-circuit/src/racing/racing.css +++ b/packages/demo-car-circuit/src/racing/racing.css @@ -397,3 +397,54 @@ padding-top: 16px; } } + +.race-countdown { + position: absolute; + inset: 35% 0 auto; + text-align: center; + color: #edff7a; + font-size: 100px; + font-weight: 800; + pointer-events: none; +} +.race-timing { + position: absolute; + top: 85px; + right: 24px; + color: white; + font-variant-numeric: tabular-nums; + background: #17211ddd; + padding: 12px 16px; + border-radius: 8px; +} + +.race-header nav button:not(:disabled) { + opacity: 1; + cursor: pointer; + color: var(--race-text); +} +@media (min-width: 601px) { + .race-sidebar { + height: calc(100vh - 76px); + overflow-y: auto; + } +} +@media (max-width: 600px) { + .race-header { + height: auto; + min-height: 100px; + flex-wrap: wrap; + gap: 12px; + padding: 12px 16px; + } + .race-header nav { + display: flex; + order: 3; + width: 100%; + height: 30px; + } +} + +.pause-overlay ol { list-style:none; width:min(360px,80%); padding:0; margin:20px 0; counter-reset:position; } +.pause-overlay li { counter-increment:position; display:flex; align-items:center; gap:18px; background:#182329; padding:12px 18px; margin:4px 0; font-variant-numeric:tabular-nums; border:1px solid #394449; } +.pause-overlay li::before { content:counter(position); color:var(--race-accent); font-weight:bold; } diff --git a/packages/demo-car-circuit/src/racing/reference.ts b/packages/demo-car-circuit/src/racing/reference.ts new file mode 100644 index 0000000..30a8f7f --- /dev/null +++ b/packages/demo-car-circuit/src/racing/reference.ts @@ -0,0 +1,17 @@ +import { DrivingWorld } from "./driving"; +/** Explicitly labelled rule-based reference; never presented as a learned driver. */ +export function referenceAction(world: DrivingWorld): number { + const c = world.car, + near = world.track.nearest(c.x, c.z); + const target = world.track.sample( + near.progress + (3 + c.speed * 0.15) / world.track.length, + ); + const error = Math.atan2( + Math.sin(Math.atan2(target.z - c.z, target.x - c.x) - c.angle), + Math.cos(Math.atan2(target.z - c.z, target.x - c.x) - c.angle), + ); + const steer = Math.abs(error) < 0.06 ? 0 : Math.sign(error); + const desired = Math.abs(error) > 0.5 ? 7 : Math.abs(error) > 0.2 ? 10 : 16; + const throttle = c.speed > desired + 1 ? -1 : c.speed < desired ? 1 : 0; + return (throttle + 1) * 3 + steer + 1; +} diff --git a/packages/demo-car-circuit/test/racing-race.test.ts b/packages/demo-car-circuit/test/racing-race.test.ts new file mode 100644 index 0000000..e6e8190 --- /dev/null +++ b/packages/demo-car-circuit/test/racing-race.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { RaceWorld } from "../src/racing/race"; + +describe("shared racing rules", () => { + it("holds every car for the countdown then advances a common tick", () => { + const race = new RaceWorld({ count: 4 }); + const initial = race.drivers.map((d) => ({ ...d.world.car })); + for (let i = 0; i < 180; i++) race.step([7, 7, 7, 7]); + expect(race.drivers.map((d) => d.world.car)).toEqual(initial); + race.step([7, 7, 7, 7]); + expect(race.drivers.every((d) => d.world.ticks === 1)).toBe(true); + }); + it("rejects an invalid batch before any car moves", () => { + const race = new RaceWorld({ count: 2, countdown: 0 }); + expect(() => race.step([7, 9])).toThrow(); + expect(race.ticks).toBe(0); + expect(race.drivers[0].world.ticks).toBe(0); + }); + it("cannot count teleports or backwards start-line crossings as laps", () => { + const race = new RaceWorld({ count: 1, countdown: 0 }); + const d = race.drivers[0]; + for (const progress of [0.3, 0.6, 0.9, 0.01, 0.99, 0.01]) { + Object.assign(d.world.car, race.track.sample(progress)); + race.step([4]); + } + expect(d.laps).toBe(0); + }); + it("separates colliding cars and rescues stranded cars with an explicit penalty", () => { + const race = new RaceWorld({ count: 2, countdown: 0 }); + Object.assign(race.drivers[1].world.car, race.drivers[0].world.car); + race.step([4, 4]); + const [a, b] = race.drivers.map((d) => d.world.car); + expect(Math.hypot(a.x - b.x, a.z - b.z)).toBeGreaterThanOrEqual(2.39); + a.x = 1000; + for (let i = 0; i < 300; i++) race.step([4, 4]); + expect(race.drivers[0].rescues).toBe(1); + expect(race.drivers[0].penaltySeconds).toBe(7); + expect(race.track.nearest(a.x, a.z).distance).toBeLessThan(1); + }); +}); + +import { referenceAction } from "../src/racing/reference"; +it.each([false, true])( + "completes three ordered laps on track test=%s with a labelled rule-based reference", + (test) => { + const race = new RaceWorld({ count: 1, countdown: 0, test }); + while (!race.finished) + race.step(race.drivers.map((d) => referenceAction(d.world))); + expect(race.drivers[0].laps).toBe(3); + expect(race.drivers[0].finishSeconds).not.toBeNull(); + expect(race.standings[0].id).toBe(0); + }, +); + +it("ranks simultaneous identical finishes deterministically and freezes completed results", () => { + const race = new RaceWorld({ count: 2, countdown: 0 }); + for (const d of race.drivers) d.finishSeconds = 100; + expect(race.standings.map((d) => d.id)).toEqual([0, 1]); + race.step([7, 7]); + expect(race.ticks).toBe(0); +}); +it("runs four reference drivers on one common clock to completion", () => { + const race = new RaceWorld({ count: 4, countdown: 0 }); + while (!race.finished) + race.step(race.drivers.map((d) => referenceAction(d.world))); + expect(race.drivers.every((d) => d.laps === 3)).toBe(true); + expect(race.standings.map((d) => d.finishSeconds)).toEqual( + race.standings.map((d) => d.finishSeconds).sort((a, b) => a! - b!), + ); +}); + +it("ranks the front grid ahead of the rear row across the start-line wrap", () => { + const race = new RaceWorld({ count: 4 }); + expect( + race.standings + .slice(0, 2) + .map((d) => d.id) + .sort(), + ).toEqual([0, 1]); +}); From 03cb49d81066df135d2209936c54512efc1914b7 Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Sun, 6 Sep 2026 18:20:56 +0200 Subject: [PATCH 05/18] feat: train and persist real racing drivers with frozen evaluation (#15) --- packages/demo-car-circuit/README.md | 10 +- packages/demo-car-circuit/package.json | 9 +- .../src/public/drivers/README.md | 7 + .../src/public/drivers/driver-11.json | 1 + .../src/public/drivers/driver-29.json | 1 + .../src/public/drivers/driver-47.json | 1 + .../src/public/drivers/report-11.json | 186 +++++++++++ .../src/public/drivers/report-29.json | 186 +++++++++++ .../src/public/drivers/report-47.json | 186 +++++++++++ .../demo-car-circuit/src/racing/RacingApp.tsx | 215 +++++++----- .../src/racing/TrainingPanel.tsx | 313 ++++++++++++++++++ .../src/racing/learned-driver.ts | 164 +++++++++ .../src/racing/learning-protocol.ts | 22 ++ .../src/racing/observations.ts | 52 +++ .../demo-car-circuit/src/racing/racing.css | 79 ++++- .../demo-car-circuit/src/racing/training.ts | 175 ++++++++++ .../test/racing-learning.test.ts | 134 ++++++++ pnpm-lock.yaml | 3 + scripts/train-racing.mts | 63 ++++ 19 files changed, 1712 insertions(+), 95 deletions(-) create mode 100644 packages/demo-car-circuit/src/public/drivers/README.md create mode 100644 packages/demo-car-circuit/src/public/drivers/driver-11.json create mode 100644 packages/demo-car-circuit/src/public/drivers/driver-29.json create mode 100644 packages/demo-car-circuit/src/public/drivers/driver-47.json create mode 100644 packages/demo-car-circuit/src/public/drivers/report-11.json create mode 100644 packages/demo-car-circuit/src/public/drivers/report-29.json create mode 100644 packages/demo-car-circuit/src/public/drivers/report-47.json create mode 100644 packages/demo-car-circuit/src/racing/TrainingPanel.tsx create mode 100644 packages/demo-car-circuit/src/racing/learned-driver.ts create mode 100644 packages/demo-car-circuit/src/racing/learning-protocol.ts create mode 100644 packages/demo-car-circuit/src/racing/observations.ts create mode 100644 packages/demo-car-circuit/src/racing/training.ts create mode 100644 packages/demo-car-circuit/test/racing-learning.test.ts create mode 100644 scripts/train-racing.mts diff --git a/packages/demo-car-circuit/README.md b/packages/demo-car-circuit/README.md index 8c9519d..461bca3 100644 --- a/packages/demo-car-circuit/README.md +++ b/packages/demo-car-circuit/README.md @@ -8,7 +8,15 @@ Asset license and provenance are in `src/public/models/`. The versioned nine-act Course mode adds a three-second countdown, three laps through 20 ordered checkpoints, standings and results. Up to four cars use a shared decision batch and physics clock. Ordered gates reject skipped checkpoints; brief cuts between gates are penalized rather than physically forbidden. Off-road excursions cost two seconds; a five-second off-road timeout rescues the vehicle to its last validated checkpoint and adds five seconds. Car contacts separate their collision bodies and reduce speed. A race stops at five simulated minutes or when all drivers finish. Equal times are ordered by stable driver ID. -The observation mode explicitly uses four **rule-based reference controllers**, not learned policies. Training, saved learned drivers and player-versus-trained-driver inference remain subsequent slices. +The observation mode explicitly uses four **rule-based reference controllers**, not learned policies. The Train mode now creates a real imitation MLP, aggregates trajectories on Alpine Park, and switches from solo to frozen reference traffic after eight rounds. A new run uses 16 rounds of 4,096 examples and three Adam epochs per round (learning rate 0.003, batch size 128, two 32-unit tanh layers, nine-way softmax). The seed fixes initialization and sampling; the 20-element observation contains driving state, track anticipation and up to three opponents. + +Stop completes the current gradient update then stops collection. Resume uses the loaded driver's seed and weights; loading recreates the optimizer, so this is a weights resume rather than an exact optimizer-state continuation. New driver discards the current unsaved network. Save stores real weights, contracts and reports in this browser's local garage. Reload refuses incompatible contracts or malformed weights. + +Evaluation reconstructs a separate frozen network and checks both circuits, solo and with three rule-based traffic vehicles, using scenario seeds 101/307/509. Success means three laps, no rescue and no more than ten penalty seconds. Results remain attached to the evaluated weights; resumed training clears old reports. + +`src/public/drivers/` contains all three scheduled training seeds and their complete reports, including failures. Aggregate measured success is 29/36; no checkpoint was selected by test-track performance. These imitation drivers are imperfect and often make contact in traffic. Run `pnpm dlx tsx scripts/train-racing.mts` at the root to reproduce native training/evaluation (requires the installed TFJS Node addon). + +Player-versus-trained-driver inference and races between saved learned drivers are the next slices. The frozen racing evaluation protocol names Alpine Park for training, Harbour for held-out testing, and seeds 101/307/509 before learned checkpoint selection. The original constant-speed evaluation has its separate version. diff --git a/packages/demo-car-circuit/package.json b/packages/demo-car-circuit/package.json index a196086..c670b37 100644 --- a/packages/demo-car-circuit/package.json +++ b/packages/demo-car-circuit/package.json @@ -3,16 +3,21 @@ "private": true, "version": "0.0.0", "type": "module", - "scripts": { "dev": "vite", "build": "vite build", "test": "vitest run" }, + "scripts": { + "dev": "vite", + "build": "vite build", + "test": "vitest run" + }, "dependencies": { "@ignitionai/backend-tfjs": "workspace:*", "@ignitionai/core": "workspace:*", "@react-three/drei": "^10.0.6", "@react-three/fiber": "^9.1.2", - "three": "^0.162.0", + "@tensorflow/tfjs": "4.22.0", "react": "^19.0.0", "react-dom": "^19.0.0", "recharts": "^2.15.3", + "three": "^0.162.0", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/packages/demo-car-circuit/src/public/drivers/README.md b/packages/demo-car-circuit/src/public/drivers/README.md new file mode 100644 index 0000000..3aef63a --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/README.md @@ -0,0 +1,7 @@ +# Learned demonstration drivers + +These are actual network weights trained by `scripts/train-racing.mts`, not rule controllers. Method: imitation with dataset aggregation, 16 fixed rounds, seeds 11/29/47, 65,536 examples per seed, three gradient epochs per round. Same 20-observation/nine-action contract and race physics as the player. Solo collection followed by frozen rule-based traffic. + +Checkpoint selection was the final scheduled update on the training track. All three scheduled seeds are included, without held-out selection. Reports include the untrained baseline and every evaluation (three scenario seeds, two circuits, solo/traffic). Aggregate success: 29/36. Each untrained baseline completed zero laps. Seed 11: 9/12, seed 29: 10/12, seed 47: 10/12. Failure reports remain included. Seed 11 fails all three solo Harbour runs; no universal driving competence is claimed. Traffic contains many repeated contact ticks; these drivers are not collision-free racers. + +Saved JSON files contain trained weights and contract metadata. Inference reconstructs the network and uses its output directly. No reference controller fallback is present. The reports were measured using TensorFlow.js 4.22 native on macOS arm64; browser backend variation requires a separate browser check. diff --git a/packages/demo-car-circuit/src/public/drivers/driver-11.json b/packages/demo-car-circuit/src/public/drivers/driver-11.json new file mode 100644 index 0000000..3843347 --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/driver-11.json @@ -0,0 +1 @@ +{"format":"ignition-driver-v1","algorithm":"imitation-mlp","driving":"circuit-racing-v1","observation":"racing-observation-v1","protocol":"racing-learning-v1","seed":11,"updates":48,"samples":65536,"createdAt":"2026-09-06T16:08:08.382Z","weights":[{"shape":[20,32],"values":[0.7662398815155029,0.15150514245033264,0.6886430382728577,-0.3688841760158539,-0.7444329261779785,0.08348602056503296,0.14541901648044586,-0.5694031715393066,0.722967267036438,1.0724462270736694,-0.8567120432853699,-0.3598369061946869,-0.37135934829711914,-0.4677908420562744,0.5280458331108093,-0.681696891784668,-0.10545718669891357,0.792594313621521,0.2182445079088211,0.0002866219147108495,0.5119280815124512,0.34146296977996826,-0.07468391954898834,-1.020184874534607,-0.640769898891449,0.5356532335281372,-0.3242756426334381,0.14866209030151367,0.9725106954574585,-0.3950018584728241,0.07908976078033447,-0.1350409984588623,0.5740941166877747,0.14499442279338837,-0.10016711056232452,0.10603732615709305,0.10509097576141357,0.03250978887081146,0.09968509525060654,0.2408009022474289,-0.18626129627227783,0.0585113987326622,0.019503870978951454,-0.04489220678806305,-0.019641706719994545,0.17260520160198212,0.41982501745224,0.1547541618347168,-0.08508484065532684,0.24239854514598846,0.23104451596736908,0.09771068394184113,0.10837209969758987,0.07555531710386276,0.10281349718570709,-0.07188168168067932,0.3387993574142456,0.22121040523052216,-0.057484280318021774,0.5211179256439209,-0.37865012884140015,0.19823390245437622,0.049254897981882095,0.14595836400985718,0.3790963888168335,-0.24804693460464478,-0.08119001239538193,-0.39856472611427307,-0.1774311661720276,0.6011093854904175,-0.014564097858965397,-0.4267628490924835,-0.14869874715805054,0.3296651840209961,0.3433752655982971,-0.36976662278175354,-0.25666582584381104,0.3145981729030609,-0.5147938132286072,0.10389754176139832,0.6087018847465515,-0.38758939504623413,0.09679936617612839,0.4695352613925934,-0.35486072301864624,-0.20285262167453766,0.18694493174552917,-0.3005217909812927,-0.6147792935371399,0.48264971375465393,0.007014530710875988,0.03496081382036209,-0.4996394217014313,-0.05071737617254257,0.28237438201904297,0.2463892102241516,-0.20890749990940094,0.43472808599472046,-0.6870946884155273,-0.44906362891197205,0.4679509997367859,-0.3570500612258911,0.34459683299064636,0.4653838574886322,0.32798004150390625,0.4033393859863281,-0.001199517399072647,-0.6161071062088013,0.1234421506524086,0.6090041399002075,0.10472927242517471,-0.1273256242275238,-0.14896070957183838,0.3436756432056427,0.5313869118690491,-0.17502479255199432,-0.2634965777397156,0.10304400324821472,0.09328073263168335,-0.12526999413967133,-0.4037785530090332,-0.14370611310005188,0.5494517087936401,0.26928088068962097,0.07554392516613007,-0.32392969727516174,0.2433033585548401,0.32506614923477173,-0.9360371828079224,0.5693376660346985,-1.1854400634765625,-0.7631576657295227,-0.40326589345932007,-2.056920289993286,1.9691495895385742,1.8554507493972778,0.7427190542221069,0.2484302967786789,0.3307555615901947,-1.7026039361953735,1.286824107170105,1.8271243572235107,-0.23104113340377808,-1.5419261455535889,-1.4858347177505493,0.7685897350311279,1.5013011693954468,1.3617116212844849,-1.5069470405578613,1.53016996383667,-2.063436269760132,0.5769596099853516,-0.023740828037261963,1.196594476699829,0.8264307975769043,-1.5632109642028809,0.08674957603216171,1.9560433626174927,-1.953688621520996,-1.1356375217437744,-0.18338631093502045,-0.10945255309343338,0.16532407701015472,0.28292492032051086,-0.060587771236896515,-0.05012902244925499,-0.2863399088382721,0.553411602973938,0.2818179130554199,0.006550931371748447,0.18424031138420105,-0.22697922587394714,0.043340153992176056,-0.2514674961566925,0.27228987216949463,-0.09330818057060242,0.09781666845083237,0.2762434482574463,0.3794291913509369,-0.23861941695213318,-0.09187182784080505,-0.18992893397808075,-0.3031792640686035,0.417838454246521,-0.014218446798622608,0.22796078026294708,-0.4730689525604248,-0.02371823973953724,0.20006519556045532,-0.13870543241500854,-0.38608860969543457,0.012804967351257801,0.1475759595632553,0.3968034088611603,0.0487917959690094,0.45772650837898254,-0.16548623144626617,-0.04588351026177406,-0.36357811093330383,0.2482028752565384,-0.034338951110839844,-0.16970990598201752,0.0068528540432453156,-0.19817546010017395,0.17805328965187073,-0.12817735970020294,-0.19423151016235352,0.09856361895799637,-0.08447956293821335,-0.08997918665409088,0.09418881684541702,0.22252780199050903,0.21090571582317352,0.11235278099775314,0.05655583366751671,-0.186097651720047,0.12745662033557892,-0.14713960886001587,-0.06349152326583862,0.3061567544937134,0.12558500468730927,0.2859901785850525,-0.08234860748052597,-0.2534751892089844,-0.01644262485206127,-0.201091006398201,-0.08814039081335068,-0.2415294349193573,0.048777371644973755,0.041364360600709915,0.08626311272382736,-0.07757439464330673,-0.19642576575279236,0.17213772237300873,0.07516833394765854,0.11525272578001022,-0.24593575298786163,0.2292710840702057,-0.18726779520511627,-0.04683918505907059,-0.2363470047712326,0.006174509413540363,-0.23664765059947968,0.2190781533718109,-0.030367745086550713,-0.1367909014225006,0.15190011262893677,0.05587070807814598,-0.3389817774295807,0.1371951848268509,0.070962093770504,-0.25866618752479553,-0.13446064293384552,-0.1762092560529709,0.1663927286863327,-0.20750494301319122,-0.010001113638281822,-0.14365817606449127,-0.06793595105409622,-0.03236323967576027,0.1856984794139862,-0.028552180156111717,-0.12287241220474243,-0.28438934683799744,0.306514710187912,0.16697371006011963,0.12774302065372467,0.30230388045310974,0.10328009724617004,0.1309596747159958,-0.06311525404453278,0.024697769433259964,-0.010938556864857674,0.2249782532453537,0.09261245280504227,-0.24657662212848663,-0.2930588126182556,0.15673814713954926,0.024359097704291344,0.019869696348905563,-0.26812151074409485,-0.17124857008457184,0.05544034764170647,-0.15408693253993988,0.1634589582681656,0.2436036467552185,0.2661658227443695,-0.18229623138904572,0.04022446647286415,-0.01260251272469759,-0.017097823321819305,-0.1962285190820694,-0.0855162963271141,-0.15663954615592957,0.19083720445632935,-0.2812567949295044,0.0206363033503294,0.009133436717092991,0.36147794127464294,-0.17738133668899536,0.12714163959026337,-0.3252379298210144,0.19927051663398743,-0.12389571964740753,0.08741650730371475,0.08747011423110962,-0.1000419408082962,-0.1577548086643219,-0.20109719038009644,-0.2119651585817337,0.28171396255493164,-0.03306359425187111,-0.06191755458712578,-0.17375986278057098,0.0803912803530693,-0.1522514671087265,0.04534834250807762,0.07785248756408691,-0.024130869656801224,-0.16752998530864716,0.08107103407382965,0.06732799857854843,0.04055379703640938,-0.0026342268101871014,0.46927395462989807,-0.14070506393909454,0.1592714935541153,0.18354320526123047,-0.25838547945022583,-0.3089292049407959,0.36180025339126587,-0.05058429390192032,0.2886297404766083,-0.025116154924035072,0.009973053820431232,-0.15301433205604553,0.12213336676359177,0.32428038120269775,-0.06030265986919403,-0.15919046103954315,0.26513344049453735,-0.012227332219481468,-0.09452086687088013,0.17862260341644287,-0.20293943583965302,-0.24789801239967346,-0.14951737225055695,0.38425078988075256,0.3220232427120209,-0.010699013248085976,-0.05411580950021744,-0.26308268308639526,0.11617344617843628,0.01431114599108696,-0.00479393033310771,-0.3104272484779358,0.12818607687950134,-0.08777958899736404,-0.10750934481620789,0.2984274625778198,0.017315873876214027,0.017968710511922836,-0.1000680923461914,0.03992830589413643,0.10077450424432755,0.06913133710622787,0.002258791122585535,0.24736249446868896,-0.20135267078876495,-0.3181566298007965,-0.11425916850566864,0.1999291628599167,0.11267739534378052,0.2735925018787384,-0.33544617891311646,0.21324652433395386,-0.2100009322166443,0.21707352995872498,0.06912786513566971,0.3547714054584503,-0.1436898559331894,0.05804004147648811,-0.09934248775243759,-0.18459255993366241,0.18492092192173004,-0.3534501791000366,0.14058707654476166,-0.21406061947345734,-0.0847024992108345,-0.23406730592250824,0.24244211614131927,0.11949014663696289,-0.23664721846580505,0.19663570821285248,-0.07626251876354218,-0.08391132950782776,-0.19681595265865326,-0.11278718709945679,-0.3867065906524658,-0.25012078881263733,-0.1392783522605896,-0.4197915494441986,0.07169323414564133,-0.03765660151839256,0.13840320706367493,0.1746167540550232,0.29530614614486694,0.019722387194633484,0.3939727246761322,-0.024118827655911446,0.3637142479419708,0.20644311606884003,-0.32381176948547363,0.12205980718135834,0.025434888899326324,0.028507690876722336,0.03207039833068848,0.33592185378074646,0.22907690703868866,-0.3447839319705963,0.1834263652563095,0.10088324546813965,-0.15716852247714996,-0.13537640869617462,-0.10649021714925766,-0.19988086819648743,-0.2492152750492096,0.23508095741271973,0.34080472588539124,0.15400777757167816,0.24525810778141022,0.003048056038096547,-0.08043766766786575,0.22834627330303192,-0.15101122856140137,0.0422808937728405,-0.06435716897249222,0.01680930331349373,-0.11294282972812653,-0.08576143532991409,-0.12677539885044098,-0.22926290333271027,0.10036292672157288,-0.06524641811847687,0.2147405445575714,0.2972783148288727,0.07399140298366547,-0.09109368920326233,-0.25966325402259827,-0.2841886281967163,-0.07094576209783554,0.2375188022851944,-0.03971739858388901,-0.24825310707092285,-0.18302157521247864,0.013076618313789368,0.20283257961273193,-0.20987874269485474,0.020113106817007065,0.04261794686317444,-0.1625104546546936,0.26118823885917664,0.15802940726280212,0.29818296432495117,-0.15811727941036224,-0.06026405841112137,0.24287563562393188,0.13199442625045776,-0.22624796628952026,-0.13880130648612976,-0.11291492730379105,0.03594827651977539,0.13991081714630127,0.031351447105407715,-0.070345938205719,0.1918712705373764,-0.054471250623464584,0.010924668982625008,-0.035032499581575394,-0.22568707168102264,-0.3502241373062134,-0.09117848426103592,0.1660546064376831,-0.13922879099845886,-0.032665736973285675,0.0034553473815321922,0.2006913125514984,-0.123428113758564,0.005858920980244875,-0.06005571037530899,-0.21152254939079285,0.19135837256908417,-0.05063429847359657,0.05003601685166359,0.23165014386177063,0.22547656297683716,-0.18005339801311493,0.12065019458532333,-0.1916852593421936,-0.22564952075481415,0.052271224558353424,0.1253547966480255,0.009826736524701118,0.29071924090385437,0.057016827166080475,0.0766150951385498,0.38571697473526,-0.023874379694461823,-0.15872813761234283,0.1217670813202858,0.31136196851730347,0.3220314681529999,0.0641581192612648,0.17662043869495392,0.2710748314857483,-0.12741360068321228,-0.06281191110610962,0.24915429949760437,-0.19562624394893646,0.3100396394729614,-0.30551677942276,-0.18422284722328186,0.15612083673477173,0.03566619008779526,0.1541333943605423,0.24033139646053314,0.3053247630596161,0.21910524368286133,-0.10231008380651474,0.12267948687076569,0.064407117664814,0.14895789325237274,-0.0950280949473381,-0.16915298998355865,-0.1843140572309494,-0.19666732847690582,0.1554231494665146,0.15080827474594116,-0.2203589230775833,-0.11074567586183548,0.188805490732193,-0.20849843323230743,-0.3103110194206238,0.2782864570617676,0.21026068925857544,-0.16547521948814392,-0.16864077746868134,-0.04303489252924919,0.14914493262767792,0.24319441616535187,0.2217445820569992,0.17022085189819336,0.11661923676729202,-0.07117940485477448,-0.12867386639118195,-0.2163117527961731,0.07116291671991348,-0.07474753260612488,0.06954366713762283,0.10459742695093155,0.18556804955005646,-0.2039104700088501,0.18395192921161652,0.015944888815283775,-0.057332251220941544,-0.13367199897766113,-0.1503017544746399,0.09296829253435135,0.2857970893383026,-0.09012506902217865,0.037157755345106125,-0.24626241624355316,0.25180724263191223,-0.38109877705574036,-0.19586727023124695,0.04571736603975296,-0.31100156903266907,-0.09528344869613647,0.041031867265701294,-0.0021741343662142754,0.17021720111370087,-0.23128816485404968,0.005233600735664368,-0.16161835193634033,-0.08587989956140518,-0.19588923454284668,0.08350507915019989,-0.2809545695781708,0.02970990724861622,-0.12691721320152283,0.08863863348960876,0.11228733509778976,-0.14501336216926575,0.1902434229850769,-0.16355521976947784,0.11276838183403015,0.01805555820465088,-0.09352964907884598,0.11778149008750916,0.08931165188550949,-0.1813991814851761,0.03644615411758423,-0.3554743528366089,-0.3405444920063019,-0.01275517139583826,0.02793573960661888,-0.1582053154706955,0.24403588473796844,0.04942364990711212,0.16278643906116486,0.13235078752040863,0.0968100056052208,0.3325921297073364,-0.21176566183567047,0.052417706698179245,0.07056941837072372,-0.006327794399112463,-0.07705601304769516,-0.0019905478693544865,-0.21311591565608978,0.03029690496623516,0.0662325844168663,0.15440261363983154,-0.09441915899515152,-0.07236724346876144,-0.15116333961486816,-0.3294859230518341,0.13613007962703705,-0.13667652010917664,-0.28672298789024353,0.27337121963500977,0.08844312280416489,0.06847292929887772,0.014813432469964027,0.16270731389522552,0.14538009464740753,0.123130664229393,0.17539113759994507,0.307348370552063,-0.02310982719063759]},{"shape":[32],"values":[-0.21909277141094208,-0.43290722370147705,-0.10342632979154587,-0.023305967450141907,0.3785715699195862,-0.37025216221809387,-0.24556156992912292,0.32936394214630127,-0.2978765070438385,-0.32978108525276184,0.09474308043718338,-0.24806912243366241,-0.06463310122489929,0.22441498935222626,-0.02250981144607067,0.3117109537124634,0.10117409378290176,-0.20618100464344025,0.22191061079502106,-0.34859535098075867,-0.059675365686416626,-0.27792641520500183,0.10399790108203888,0.39733439683914185,-0.12282254546880722,-0.4119341969490051,0.013559936545789242,0.14202703535556793,-0.21005156636238098,-0.1552008092403412,0.07249321043491364,0.2388950139284134]},{"shape":[32,32],"values":[0.5042830109596252,-0.09014109522104263,-0.197612002491951,-0.006745198275893927,-0.023619605228304863,-0.1866925209760666,0.15413352847099304,0.5368921160697937,0.4386928379535675,0.39101478457450867,-0.3633923828601837,0.6322101950645447,-0.3832452595233917,-0.37681496143341064,0.014400854706764221,-0.6855100393295288,-0.6144633889198303,0.6328778862953186,0.7158591747283936,-0.13136141002178192,-0.2237684279680252,-0.2659306228160858,-0.5506540536880493,-0.5145538449287415,0.4300624132156372,-0.11723138391971588,-0.22616976499557495,-0.25056445598602295,0.16730189323425293,0.5329301357269287,0.2945577800273895,0.33261048793792725,0.20087644457817078,0.1164974495768547,0.13875240087509155,-0.40579932928085327,-0.13018479943275452,0.1479879766702652,0.12188596278429031,-0.4009605646133423,-0.3973252773284912,0.3835182189941406,-0.06798747926950455,0.01905968226492405,0.015622137114405632,0.0012477352283895016,0.0778699591755867,0.1305462121963501,-0.21393316984176636,0.20081602036952972,0.16389118134975433,-0.2696390151977539,-0.025113152340054512,0.30371302366256714,-0.10330120474100113,-0.2436201125383377,0.05303953215479851,0.039479054510593414,0.06216791272163391,-0.23804348707199097,0.4277642071247101,-0.03176765888929367,-0.3926270306110382,-0.07238079607486725,0.31944990158081055,-0.2302771657705307,-0.8897037506103516,-0.45249003171920776,-0.41490811109542847,-0.5202892422676086,-0.2600279748439789,0.8404431939125061,0.9023479223251343,-0.042112238705158234,-0.5636172294616699,0.2719811201095581,-0.6942974328994751,-0.554820716381073,-0.30821502208709717,-0.6527342796325684,-0.06771958619356155,0.48115748167037964,0.5295531749725342,-0.1861618459224701,-0.2710456848144531,-0.6590291857719421,-0.6559422016143799,-0.3561141788959503,0.19151858985424042,-0.11425794661045074,-0.488293319940567,-0.11017731577157974,-0.6938963532447815,0.7940636873245239,0.5666778087615967,0.6420795917510986,-0.12233927845954895,-0.330810546875,-0.03095807321369648,-0.2449830174446106,-0.39490509033203125,-0.3679894506931305,0.08269070088863373,-0.0790402889251709,0.025685511529445648,0.1859428435564041,0.09025996923446655,-0.4035310745239258,0.11693336814641953,-0.026763824746012688,-0.4243173599243164,-0.3170483410358429,0.29330381751060486,-0.3260183036327362,0.04875830188393593,0.07081069052219391,-0.5711960792541504,-0.19018881022930145,-0.102317214012146,0.06279203295707703,0.3996068239212036,-0.17644880712032318,-0.6255443096160889,0.45995765924453735,-0.04384010657668114,0.09075383841991425,0.23662324249744415,0.3142419755458832,-0.10653514415025711,0.5745928287506104,-0.2829630374908447,0.1500341147184372,0.20788395404815674,-0.28259533643722534,-0.2873566448688507,0.15569670498371124,0.6154280304908752,-0.3255501389503479,0.5157648324966431,-0.49994492530822754,-0.3492438495159149,1.2657952308654785,0.09270798414945602,0.09450598061084747,0.8031587600708008,-0.5166824460029602,-0.4910791218280792,0.471979022026062,-0.12642276287078857,-0.39179563522338867,0.6775293350219727,0.6116546988487244,-0.2719912528991699,0.624998927116394,0.4545328617095947,0.7338414788246155,-0.6045213341712952,-0.35674893856048584,0.4545147716999054,0.3250548243522644,0.2755894958972931,-1.1113823652267456,-0.24501372873783112,-1.0017144680023193,-1.3203414678573608,-0.6294044256210327,-0.09573870897293091,0.18394629657268524,0.7805435657501221,0.46173617243766785,-0.2774942219257355,0.8142560720443726,-0.2951997220516205,-0.4951237738132477,-1.427536129951477,-0.2622995674610138,0.2173887938261032,0.12144454568624496,0.47954800724983215,-0.08270277827978134,-1.4915480613708496,-0.23590798676013947,0.04566363990306854,-0.060768093913793564,1.4010848999023438,-0.8195803761482239,-1.0635066032409668,0.033480167388916016,-0.04356174170970917,0.46120843291282654,0.6417524218559265,0.3398267328739166,0.016669243574142456,0.6860440373420715,1.0929343700408936,0.7123554944992065,0.5496697425842285,0.8513287901878357,0.3103597164154053,-0.6393807530403137,-1.0333890914916992,-0.36111512780189514,-0.05079708993434906,0.35612455010414124,1.104900598526001,-0.3429713249206543,0.5713453888893127,0.8786389827728271,-0.05889773741364479,0.36364877223968506,-0.35520365834236145,-0.20425494015216827,0.5498439073562622,0.7777095437049866,-0.4429646134376526,0.10844399034976959,-0.4015246331691742,0.42978230118751526,0.7126315832138062,-0.39354458451271057,0.9254270792007446,-0.669012725353241,-0.9300620555877686,-0.5911031365394592,-0.3075050115585327,0.8015957474708557,0.308906614780426,0.6518149375915527,0.6503925919532776,0.713019073009491,0.030300239101052284,-0.4156772196292877,-0.3861044943332672,0.2670711278915405,0.08910857886075974,-0.20038242638111115,0.22255904972553253,0.3140505254268646,0.838375985622406,0.3548072874546051,0.31132933497428894,-0.4924042224884033,-0.38639360666275024,0.0002550794160924852,0.5127627849578857,0.38742008805274963,0.5434756875038147,0.4249389171600342,-0.7112624645233154,0.622215211391449,0.6504901647567749,-0.0030065812170505524,0.21572773158550262,-0.3622916042804718,-0.5065879821777344,-0.27570316195487976,0.3357582092285156,0.3168136775493622,0.025192324072122574,0.23962804675102234,0.42608723044395447,0.21278338134288788,-0.06592723727226257,-0.20070777833461761,-0.5675106644630432,0.20957013964653015,-0.027664078399538994,0.1654728502035141,-0.09325221180915833,-0.7819374203681946,0.2047334760427475,0.49049824476242065,-0.3844340443611145,0.34663787484169006,0.3017837107181549,-0.4561316668987274,-0.011773250997066498,-0.030545152723789215,-0.566208004951477,-0.6806126832962036,-0.07534797489643097,0.0234752856194973,0.26398617029190063,-0.9632359743118286,0.2521553337574005,0.08377684652805328,-0.20814253389835358,-0.17013691365718842,0.6429846882820129,0.1725015938282013,0.034425631165504456,0.29960840940475464,0.09683369845151901,-0.08541196584701538,0.30450639128685,0.023329120129346848,0.12259367108345032,0.5909400582313538,-0.7870517373085022,0.9609614610671997,0.21734710037708282,-1.1929776668548584,0.3304801285266876,-0.14166004955768585,-0.929496705532074,1.0738238096237183,0.37665581703186035,-0.37448880076408386,0.5024996995925903,-0.07441572844982147,-1.125558614730835,-1.1957664489746094,-0.04375581443309784,0.07198744267225266,0.15257292985916138,-0.501750111579895,0.1546918898820877,0.43263310194015503,-0.08977105468511581,-0.0017173618543893099,-0.38530418276786804,-0.21474356949329376,0.658280074596405,-0.25761309266090393,-0.0993884727358818,0.5052453875541687,-0.21779733896255493,-0.3356318771839142,-0.06824158877134323,0.08764797449111938,0.09319471567869186,-0.51179039478302,0.5641534328460693,0.41476017236709595,-0.359327495098114,0.22269561886787415,0.6978732347488403,-0.9909806251525879,-0.5864783525466919,-0.15761016309261322,-0.16205313801765442,0.38844266533851624,0.6707112789154053,0.8068820834159851,0.32027584314346313,-0.5086190700531006,-0.41656073927879333,0.3874492645263672,0.5294313430786133,-0.4046246409416199,-0.2836383283138275,-0.02692222408950329,0.24669001996517181,-0.23730513453483582,-0.20223358273506165,-0.7090931534767151,-0.6969600915908813,0.057600680738687515,0.19950881600379944,-0.4887915551662445,-0.09920721501111984,-0.22466564178466797,0.09819846600294113,0.19255565106868744,-0.30337533354759216,-0.17050494253635406,-0.38115665316581726,-0.1677805483341217,0.28014886379241943,-0.21637441217899323,0.01924014464020729,0.21194981038570404,-1.048879861831665,-0.29032430052757263,0.041805803775787354,0.12517890334129333,0.5120618343353271,-0.2891231179237366,-0.7025619149208069,-0.017719099298119545,-0.23409846425056458,0.19845224916934967,0.24303601682186127,0.27214929461479187,-0.3011503517627716,0.23944780230522156,0.3918052613735199,0.0279400497674942,0.24416619539260864,0.7056629657745361,0.0710659846663475,-0.3812544047832489,-0.7231264114379883,-0.47194236516952515,0.14206939935684204,-0.20268777012825012,0.6765146851539612,0.17570023238658905,0.40617725253105164,0.4807601571083069,0.29009291529655457,0.08138475567102432,-0.3764652907848358,0.0760822743177414,0.3795483708381653,0.27309519052505493,0.33727744221687317,-0.07505428045988083,-0.3538050055503845,0.17404213547706604,0.0296771340072155,-0.18057206273078918,0.483379065990448,-0.1573207974433899,-0.3466559946537018,-0.26356109976768494,-0.38703176379203796,0.804512083530426,0.599362313747406,0.684076726436615,0.5364117622375488,0.738310694694519,-0.056229185312986374,0.011343469843268394,-0.5398069024085999,-0.059615571051836014,0.12863008677959442,-0.2898412048816681,0.6971870064735413,0.2047952115535736,0.7035121917724609,0.5477936267852783,0.173933744430542,-0.2546635866165161,-0.5848396420478821,0.37514999508857727,0.5487802624702454,0.23704789578914642,0.03589319437742233,0.12894856929779053,-0.8848134875297546,0.35073527693748474,0.7208733558654785,-0.09456393122673035,0.5687708854675293,-0.46118858456611633,-0.31474536657333374,-0.47489720582962036,-0.07746005058288574,0.07900653034448624,-0.10896541178226471,0.013162276707589626,0.18131272494792938,-0.18389683961868286,-0.08793830871582031,0.2978318929672241,0.010394372045993805,-0.0777430310845375,-0.20014403760433197,-0.39435067772865295,-0.24884730577468872,-0.1263866424560547,0.0065904888324439526,-0.24604038894176483,-0.1559734344482422,0.4571278393268585,0.18365731835365295,-0.23369979858398438,0.46302008628845215,0.037025537341833115,-0.4292008876800537,-0.11637220531702042,-0.4181778132915497,0.3186059296131134,0.05972832441329956,-0.2203240692615509,-0.19841493666172028,0.3256753385066986,0.23452560603618622,0.1948660910129547,-0.2839166522026062,-0.6533963084220886,-1.3536955118179321,-0.5588324666023254,-0.38531211018562317,-1.2293684482574463,-0.12886612117290497,0.5318069458007812,0.879662036895752,0.4223081171512604,1.0223219394683838,-0.41623160243034363,-1.0740567445755005,0.7877470254898071,-0.7199878096580505,-0.7842461466789246,1.2429683208465576,-0.6412895917892456,0.48790988326072693,0.9492450952529907,-0.48289555311203003,-1.152915596961975,0.9617682099342346,0.9253010153770447,0.6017854809761047,-0.2293572723865509,-0.8755605816841125,1.4876246452331543,-0.8169042468070984,0.20910616219043732,0.8616068363189697,1.0144096612930298,0.3552325665950775,-0.34220004081726074,-0.17893952131271362,0.0004989787703379989,-0.44388067722320557,0.10509645193815231,-0.254639595746994,0.17649394273757935,0.13101935386657715,0.05467661842703819,0.3061797618865967,0.7105732560157776,-0.4397147297859192,0.05738726258277893,-0.38203760981559753,-0.4647546112537384,-0.07294833660125732,-0.0785866230726242,0.244032084941864,-0.06067280098795891,-0.44771283864974976,-0.5575366616249084,0.01571737974882126,0.08133865892887115,0.21027743816375732,-0.4716240465641022,-0.6672155261039734,0.16093845665454865,0.0019390633096918464,0.18531689047813416,0.5532884001731873,0.023934198543429375,0.394404798746109,0.27865487337112427,0.17396409809589386,0.2978459298610687,0.5153871774673462,-0.1743166595697403,-0.005218400154262781,-0.019260695204138756,-0.5596446394920349,0.2029990255832672,-0.5874453186988831,-0.06786665320396423,-0.06243979558348656,-0.23808173835277557,0.4979392886161804,0.38790076971054077,-0.7174531817436218,0.10973245650529861,0.07094307988882065,-0.1350754350423813,0.3175918459892273,0.4578460454940796,-0.1998520791530609,-0.5522421002388,-0.3592621982097626,-0.12411818653345108,0.3673451840877533,-0.55389004945755,-0.13318343460559845,0.24686813354492188,-0.3028358817100525,-0.29131096601486206,-0.2541092038154602,0.3443637788295746,0.33995741605758667,0.3859453499317169,0.31008389592170715,0.11750023066997528,-0.0354604572057724,-0.3063880503177643,-0.34390950202941895,0.2841837704181671,-0.24552196264266968,0.04198459908366203,0.2877717614173889,0.03978658840060234,0.6074636578559875,0.2788175046443939,-0.29337814450263977,0.06568939983844757,-0.22684505581855774,0.057936519384384155,0.7260938286781311,0.3835218548774719,0.23447057604789734,-0.11491912603378296,-0.7893378734588623,0.3512539565563202,0.7158410549163818,-0.34192562103271484,0.33172813057899475,0.09893607348203659,-0.3816787004470825,-0.0179226603358984,0.40584346652030945,0.288143128156662,0.8575264811515808,-0.027321087196469307,0.22956733405590057,1.000674843788147,-0.09573347121477127,-0.02017902210354805,-0.6929712295532227,0.017528628930449486,-0.07370679825544357,0.4731760323047638,1.0626558065414429,-0.01779986545443535,-0.03437500074505806,0.5067021250724792,-0.026856431737542152,-0.030683008953928947,0.2879415452480316,-0.2759188711643219,0.2303093820810318,0.8034955263137817,-0.21033981442451477,-0.2116415649652481,-0.24245896935462952,-0.2210855931043625,-0.0068240114487707615,0.05796666070818901,0.5900996327400208,-0.4984560012817383,-0.7585126757621765,-0.7230998277664185,0.328933984041214,-0.20945028960704803,-0.8570535182952881,-0.11991775780916214,-0.5107137560844421,-0.6642124652862549,-0.05509369075298309,0.6840416193008423,0.46963468194007874,0.17527227103710175,-0.03468616306781769,0.3026088774204254,-0.5338450074195862,-0.2132112830877304,-0.2762891352176666,-0.5752382278442383,-0.2063402235507965,0.23550952970981598,0.734490692615509,0.12975893914699554,-0.293016254901886,-0.4930122196674347,-0.41142821311950684,-0.3535844087600708,0.2703143358230591,-0.22109881043434143,-0.547294020652771,-0.12200449407100677,-0.32616302371025085,0.6480715870857239,0.647098183631897,0.7163130044937134,0.16672874987125397,0.0855419710278511,0.8376772999763489,0.31041115522384644,0.4945189356803894,0.8581045269966125,0.28141769766807556,-1.0885651111602783,-1.1177488565444946,-0.3590444028377533,-0.23301580548286438,-0.16284239292144775,0.9179301857948303,-0.26300132274627686,0.1870644986629486,1.0521003007888794,-0.1386975646018982,0.17711298167705536,-0.3593408167362213,-0.37035202980041504,0.3146136403083801,0.9444905519485474,-0.35971978306770325,-0.5936189889907837,-0.5503393411636353,0.16586750745773315,0.5359424948692322,-0.3610694408416748,0.5755863785743713,-0.06084766611456871,-1.073948860168457,-1.0653631687164307,0.04775387793779373,-0.6539106369018555,-1.133691668510437,-0.6073721647262573,-0.6533507704734802,-0.7882328629493713,-0.28765222430229187,0.292375385761261,0.8136492967605591,0.26449108123779297,0.07398196309804916,0.09956521540880203,-1.2613162994384766,0.18865537643432617,-0.5700405240058899,-0.8704350590705872,0.31802240014076233,0.1276175081729889,0.4140457510948181,0.5851498246192932,-0.8154004812240601,-0.5509259700775146,0.2732740044593811,0.20102360844612122,0.36228111386299133,-0.1223989799618721,-0.5266410112380981,0.5709245800971985,-0.6782822608947754,0.7063713669776917,1.3621472120285034,0.9902127981185913,-0.5110110640525818,1.0708800554275513,0.3397563099861145,0.554537296295166,0.8628024458885193,0.03336883336305618,-0.19797758758068085,-0.05074867233633995,0.01959528960287571,-0.7355239987373352,1.073441743850708,-1.406149983406067,0.5575284957885742,0.7865368723869324,0.7678597569465637,0.3092546761035919,0.8899766206741333,-1.0707111358642578,-0.6023889780044556,0.3645736277103424,0.487825870513916,0.216250479221344,1.2571873664855957,1.4119521379470825,-0.9093960523605347,0.8019565939903259,0.5613827705383301,1.3131994009017944,0.15041762590408325,-0.8566830158233643,0.10377007722854614,-0.5232651829719543,0.10681505501270294,-0.06508520990610123,0.3537832796573639,-0.24163350462913513,-0.20202648639678955,0.13745538890361786,0.3777524530887604,-0.637313187122345,0.06411056220531464,-0.08994829654693604,-0.04893568158149719,-0.4078773558139801,0.14010682702064514,-0.003726342925801873,-0.3551073372364044,0.36655229330062866,0.3238549828529358,-0.3214753270149231,-0.057367198169231415,0.04220091179013252,-0.5868780016899109,-0.12698501348495483,0.1584942489862442,0.019078250974416733,0.6332802772521973,0.011724069714546204,-0.5021881461143494,0.34758254885673523,0.1985035389661789,-0.053365327417850494,-0.06063606217503548,-0.2993711829185486,0.349178671836853,0.10848790407180786,0.5901610851287842,0.37078678607940674,0.1910364180803299,1.0259878635406494,0.19173669815063477,-0.21164743602275848,-1.0571463108062744,0.22699478268623352,-0.6653257012367249,0.27838870882987976,0.8109248876571655,-0.7576424479484558,-0.05289557948708534,0.6476414799690247,-0.9914576411247253,0.21395035088062286,0.5094535946846008,-0.6747276186943054,0.2760467231273651,0.7515944838523865,-0.5542449951171875,-0.4972325265407562,-0.3647538125514984,-0.38251039385795593,0.21896538138389587,-0.6067361831665039,0.9108262658119202,0.28556522727012634,-0.6849166750907898,-0.5599246025085449,-0.3728238642215729,-0.1989629864692688,0.4406382441520691,0.3896898031234741,0.007306629326194525,0.025655873119831085,-0.011042699217796326,-0.40721383690834045,-0.4537029564380646,0.004705946892499924,0.36099401116371155,0.2044263333082199,0.21024194359779358,0.504967212677002,0.1048024445772171,0.2098740190267563,-0.07460743933916092,0.3071839511394501,-0.3370799422264099,0.10414741933345795,0.3612477481365204,0.23626753687858582,0.49424245953559875,0.06129652261734009,0.06861139088869095,0.11185441166162491,0.3111826479434967,-0.27512815594673157,0.35182246565818787,-0.09061360359191895,-0.15906022489070892,-0.025290898978710175,-0.006581960711628199,-0.14122049510478973,-0.6650552153587341,-0.28634876012802124,-0.07614808529615402,-0.27115917205810547,-0.4407639503479004,0.5512175559997559,0.5790424346923828,0.7677775025367737,-0.13552351295948029,-0.11245501041412354,-0.5199999809265137,-0.05999058485031128,-0.39507821202278137,-0.5634640455245972,-0.3642382025718689,0.2618614435195923,0.4041352868080139,0.05731990188360214,-0.35189613699913025,-0.35735273361206055,0.04958096146583557,0.2765657901763916,0.4040682911872864,-0.15566205978393555,0.005184059962630272,0.24114055931568146,-0.41926488280296326,0.6075991988182068,0.6727449893951416,0.6022346615791321,0.10452087223529816,0.2162885069847107,-0.24729083478450775,0.2282274067401886,0.2910875380039215,-0.1000923216342926,0.2662041485309601,0.2414063662290573,0.07393822073936462,-0.2699053883552551,-0.4004659950733185,0.0663192942738533,-0.14174850285053253,-0.3245888352394104,0.390325665473938,-0.18077118694782257,-0.5493263006210327,0.7602974772453308,0.6282860636711121,-0.46670520305633545,0.14499209821224213,0.2057880461215973,-0.9138738512992859,-0.4246053993701935,-0.5398601293563843,0.4293157160282135,0.1913827806711197,-0.7037061452865601,-0.0005706084775738418,0.4041852056980133,0.04189348220825195,-0.0941973403096199,-0.4544059932231903,0.6037085652351379,0.8252005577087402,0.4090360403060913,0.5696160197257996,0.7036542892456055,-0.015647493302822113,-0.17049501836299896,-0.7272423505783081,-0.0016218111850321293,-0.005907585844397545,-0.44722244143486023,0.8250250220298767,-0.015814922749996185,0.3888840079307556,0.8056722283363342,-0.12451071292161942,0.12377072125673294,-0.6183146834373474,-0.16918645799160004,0.32396501302719116,0.40037086606025696,0.14347362518310547,0.025403695181012154,-0.43892914056777954,0.07421699911355972,0.255157470703125,0.4317130744457245,0.6721332669258118,-0.6994998455047607,-0.7766883373260498,-0.7066370844841003,0.3754011392593384,-0.6514704823493958,-0.6807132363319397,-0.12067990005016327,-0.3501433730125427,-1.1368392705917358,-0.053637951612472534,0.5164682865142822,0.9805508852005005,0.021590743213891983,0.03644447773694992,-0.2953343987464905,-0.8654231429100037,0.31230154633522034,-0.5421581864356995,-1.19864821434021,-0.030999742448329926,0.30503952503204346,0.5012243986129761,0.6206091046333313,-0.7761892676353455,-0.8631339073181152,0.29467645287513733,0.019205085933208466,0.12718553841114044,-0.15832188725471497,-0.4990095794200897,0.319996178150177,-0.7381312847137451,0.7815610766410828,1.2316933870315552,0.8851038813591003,0.10540971159934998,-0.27703237533569336,-0.4862528145313263,-0.013623127713799477,-0.6121994853019714,-0.5326507091522217,-0.10897829383611679,-0.23200489580631256,0.24708974361419678,-0.1427648663520813,0.27137333154678345,0.38610556721687317,-0.4693775177001953,-0.2773684859275818,-0.1181216835975647,-0.5052650570869446,0.008926993235945702,-0.1503182202577591,-0.17190028727054596,0.3729648292064667,-0.15878216922283173,-0.1033557653427124,0.21648836135864258,0.23492418229579926,0.422097384929657,-0.26175975799560547,-0.011192241683602333,0.25472572445869446,-0.3172494173049927,0.178367480635643,0.6218383312225342,0.2320953756570816]},{"shape":[32],"values":[-0.24416105449199677,0.4067629277706146,-0.10210400074720383,0.26675713062286377,0.35779833793640137,-0.20237478613853455,-0.11746443063020706,0.2474897801876068,0.25886520743370056,-0.0018455712124705315,0.14022710919380188,-0.3138156831264496,-0.13974188268184662,0.2735600471496582,0.3652844727039337,-0.167120099067688,0.14985588192939758,-0.24730688333511353,-0.17004390060901642,0.20726726949214935,0.39085957407951355,-0.242408886551857,0.21943117678165436,0.3029194176197052,-0.37072062492370605,0.3915705680847168,0.2954377233982086,0.2807527482509613,-0.1557735949754715,-0.03514060750603676,0.2244340479373932,0.15948189795017242]},{"shape":[32,9],"values":[0.9365372657775879,0.7408824563026428,-0.12412580102682114,0.2220870703458786,0.1684785932302475,0.05575098469853401,-0.9125363230705261,0.06966818869113922,-0.3603575825691223,-1.1030455827713013,-0.17844393849372864,-0.29508528113365173,-1.1635186672210693,-0.05939526483416557,0.11951663345098495,-0.9407258629798889,0.3411313593387604,0.2877116799354553,-0.037543416023254395,-0.48897606134414673,0.907411515712738,-0.5497209429740906,-0.5168946385383606,0.7417369484901428,-0.6514323949813843,-0.1463823765516281,1.0616956949234009,-0.6529030799865723,-0.041521355509757996,0.06695036590099335,-0.9439311027526855,0.4765336513519287,0.5131426453590393,-0.8321066498756409,0.27967435121536255,0.5529355406761169,-0.9902400374412537,0.04747698828577995,-0.33820638060569763,-0.7858584523200989,0.008528204634785652,0.5758702158927917,-0.9514048099517822,0.4960858225822449,0.13725459575653076,-0.2178356796503067,-0.7078641057014465,0.6415917277336121,-0.31638532876968384,-0.9319307208061218,0.9838762283325195,-0.20184947550296783,-0.5760562419891357,0.8753237724304199,-0.5839117765426636,0.3250143527984619,0.4072510600090027,0.1022210419178009,-0.3856514096260071,0.1918342411518097,0.17373734712600708,-0.2883199453353882,-0.17692174017429352,-0.23334382474422455,-0.18440556526184082,-1.2956676483154297,0.19377927482128143,0.5427948236465454,0.264201283454895,0.09976151585578918,0.21604785323143005,-0.3006311058998108,0.15601320564746857,0.3294188976287842,-0.8431345820426941,-0.2583518624305725,0.3620813488960266,-0.5330756902694702,0.3055133819580078,0.5539800524711609,-0.7790137529373169,0.9599138498306274,0.4220524728298187,-0.4748949706554413,-0.4382460117340088,0.303606778383255,-0.23406484723091125,-0.5653020739555359,0.07697824388742447,0.21458283066749573,-0.09878384321928024,-0.7237530946731567,-0.6170429587364197,0.28077325224876404,-0.06432539224624634,-0.4044071137905121,0.8852314352989197,0.7317485809326172,0.72550368309021,1.5498958826065063,0.7351185083389282,0.5219256281852722,0.23203010857105255,-0.1675463169813156,0.023174691945314407,-1.7050182819366455,-0.5965197086334229,-0.014587294310331345,0.03698914125561714,-0.6283851861953735,0.5147217512130737,-0.13006030023097992,-0.5442968606948853,1.0065970420837402,-0.38228070735931396,-0.3123592138290405,1.2303599119186401,-0.4429296851158142,-1.1424777507781982,-1.1256959438323975,0.27235761284828186,0.26439422369003296,0.1278611719608307,0.21702483296394348,0.21084368228912354,0.3176123797893524,-0.5593456625938416,-0.20393522083759308,-0.138249933719635,-0.4918285608291626,0.3562760353088379,0.34305480122566223,-1.094228982925415,0.5386423468589783,0.4551967978477478,-0.024738820269703865,-0.7635584473609924,0.5215868353843689,-0.4491021931171417,-0.2982410788536072,0.29340097308158875,-0.49948203563690186,-0.4615572988986969,1.1509160995483398,0.18771275877952576,-0.5937767028808594,-0.19293880462646484,0.11695801466703415,-0.6338202357292175,-0.3739261329174042,0.8279045820236206,0.6558563113212585,0.48859113454818726,0.7662027478218079,1.097427248954773,1.1537729501724243,0.25686532258987427,0.657698392868042,-0.06005548685789108,-1.3919062614440918,-0.5539987087249756,-0.5525941848754883,0.8760174512863159,1.1984059810638428,-0.521491289138794,0.5131549835205078,0.3213440477848053,-0.03722134605050087,0.4190879464149475,-0.4189060926437378,-0.9286630749702454,0.46299177408218384,-0.9032803177833557,-0.9694614410400391,0.7293389439582825,0.3809429109096527,-0.8373230695724487,-0.050700172781944275,0.46467217803001404,0.6429226398468018,-0.9613456726074219,0.4670150578022003,-0.09490317106246948,-0.820492148399353,0.31536316871643066,0.2638995349407196,-0.911113977432251,0.34581974148750305,0.3495161235332489,-0.04514143615961075,-0.7606232762336731,0.5486270189285278,-0.3147706091403961,-0.15464189648628235,1.0165350437164307,-0.14504030346870422,-0.5246485471725464,0.8196916580200195,-1.015346646308899,-1.4973841905593872,-1.4490898847579956,-0.33984699845314026,-0.7833383083343506,0.11816612631082535,0.6868644952774048,1.4820836782455444,1.1441396474838257,-0.06051967665553093,-1.7623746395111084,-1.4345877170562744,-0.05487491562962532,-0.15659183263778687,-0.4054277837276459,0.6065940260887146,1.0059102773666382,0.678764820098877,1.365841031074524,-0.3271710276603699,-0.0881950631737709,0.8072776794433594,-0.49492529034614563,-0.04393703490495682,0.6916779279708862,-0.46893444657325745,-0.46997249126434326,-1.9117568731307983,0.2035650759935379,0.30046430230140686,-0.9547749161720276,0.009881140664219856,0.27189353108406067,0.009656652808189392,0.2799697816371918,-0.18711557984352112,-1.0204685926437378,-0.26368239521980286,-0.09714113175868988,-0.4852856993675232,0.4351709485054016,0.1497284471988678,-1.2069324254989624,0.45438000559806824,0.2175752818584442,0.4746845066547394,-0.7765138745307922,-0.8849416971206665,0.6664975881576538,-0.29773837327957153,-0.6248120665550232,0.9298093914985657,1.061646819114685,0.3943411707878113,0.38100460171699524,-0.0694061741232872,0.5194379091262817,-0.051235463470220566,-0.665269136428833,0.433537095785141,-0.3515172302722931,-0.46524736285209656,0.403496116399765,0.3044404685497284,0.3622268736362457,-0.6675723791122437,0.3579648733139038,0.6488921642303467,-0.40011221170425415,0.004706306848675013,-0.4291295111179352,-1.1036537885665894,0.23670774698257446,0.22530536353588104,-0.4887958765029907,0.26716163754463196,0.3508029580116272,-0.9819226264953613,0.1953248828649521,0.6761777997016907,-1.1125115156173706,0.21868745982646942,0.6549010872840881,-1.016467571258545,0.2105463594198227,0.4243132770061493,-0.6772024631500244,0.005859077908098698,0.27489545941352844,-0.7727219462394714]},{"shape":[9],"values":[-0.1336117684841156,-0.19803184270858765,-0.25048550963401794,-0.04175126180052757,0.10430386662483215,0.0773598849773407,0.03341536223888397,0.1351887434720993,0.08531235158443451]}],"configuration":{"hiddenLayers":[32,32],"learningRate":0.003,"batchSize":128,"epochsPerRound":3,"samplesPerRound":4096}} \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/drivers/driver-29.json b/packages/demo-car-circuit/src/public/drivers/driver-29.json new file mode 100644 index 0000000..0d98815 --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/driver-29.json @@ -0,0 +1 @@ +{"format":"ignition-driver-v1","algorithm":"imitation-mlp","driving":"circuit-racing-v1","observation":"racing-observation-v1","protocol":"racing-learning-v1","seed":29,"updates":48,"samples":65536,"createdAt":"2026-09-06T16:09:55.446Z","weights":[{"shape":[20,32],"values":[-0.7812262773513794,-0.5602768659591675,0.7913224697113037,0.3713487982749939,-0.5555347204208374,-0.259115993976593,-0.5675394535064697,0.8782848715782166,-0.8173646926879883,0.7604488134384155,0.2990923821926117,-0.29491594433784485,0.08978579193353653,0.9882049560546875,0.2977052330970764,-0.694548487663269,-0.7045120596885681,0.24104228615760803,-0.8352554440498352,-0.3745681345462799,0.24383309483528137,0.042887091636657715,-0.16252797842025757,-0.5277711153030396,0.7709505558013916,0.4435100853443146,0.32870808243751526,0.09128115326166153,0.4413225054740906,0.5039908289909363,0.5952858924865723,-0.7002553343772888,-0.2451329231262207,-0.21111492812633514,0.08310727030038834,-0.2567444145679474,-0.16389456391334534,0.4580329358577728,0.1884472668170929,0.21574082970619202,0.03352437913417816,0.18680152297019958,0.13375253975391388,-0.029622599482536316,0.1852881759405136,0.016360117122530937,0.18122759461402893,0.2947292625904083,0.2773993909358978,0.29337194561958313,-0.010827205143868923,-0.009077955037355423,-0.08895904570817947,-0.0639805719256401,0.15516936779022217,0.20264661312103271,0.3617059290409088,-0.06032402813434601,-0.030351124703884125,0.2797867953777313,-0.06628123670816422,0.09132246673107147,-0.26702967286109924,0.19050489366054535,-0.222618967294693,0.8100611567497253,-0.07267029583454132,-0.0022105365060269833,0.5488627552986145,-0.6803628206253052,0.0901704654097557,-0.15899984538555145,0.11232052743434906,-0.09793099015951157,0.15308885276317596,-0.22527076303958893,-0.1443660408258438,0.4670597314834595,0.17935629189014435,0.6566370725631714,-0.10461577773094177,-0.9325817823410034,-0.3381543457508087,0.09539281576871872,-0.4359312057495117,0.15623806416988373,-0.1504162698984146,-0.011589037254452705,-0.05136329308152199,0.20203475654125214,-0.4722755253314972,0.02382062003016472,-0.29577213525772095,0.34108150005340576,-0.3362981081008911,0.02524256892502308,-0.07477021217346191,-0.01691330224275589,0.45668601989746094,-0.05173414200544357,-0.5905953049659729,-1.3538761138916016,-0.39420026540756226,0.004922610241919756,-0.3715260922908783,0.16185574233531952,0.20383445918560028,-0.17767177522182465,-0.04324755817651749,-0.008196179755032063,0.18798287212848663,-0.15242062509059906,0.4290187954902649,-0.34575867652893066,0.261588454246521,-0.66036057472229,-0.01060807891190052,0.5404634475708008,-0.15645907819271088,-0.1131460964679718,0.10521556437015533,-0.5742849707603455,0.44171059131622314,0.28013816475868225,-0.06622034311294556,-0.4313105642795563,0.07645142823457718,0.2757766842842102,1.2969913482666016,0.9526742100715637,1.6071064472198486,-1.39457106590271,1.4551705121994019,0.18111619353294373,-0.9228824377059937,0.08710876852273941,0.5832308530807495,0.8983867764472961,1.4435209035873413,-1.3947055339813232,1.931810736656189,0.5684331655502319,-1.936052680015564,-1.6668745279312134,1.7406895160675049,-1.144309639930725,0.45761266350746155,-1.852567434310913,1.6270900964736938,1.4635940790176392,1.7548295259475708,-0.8784270286560059,-0.03390636667609215,-0.7733869552612305,1.9930577278137207,1.5493348836898804,-1.399267315864563,-1.2455384731292725,-0.6793205142021179,1.5332425832748413,-0.10702177882194519,0.3155101239681244,0.12919405102729797,0.26072898507118225,0.12165013700723648,-0.22461971640586853,-0.29246339201927185,-0.2334100902080536,-0.22837024927139282,0.384333997964859,-0.16745693981647491,-0.153361514210701,-0.5538187026977539,-0.24283654987812042,0.17723314464092255,-0.2027517408132553,0.11482609063386917,-0.027676697820425034,0.24020881950855255,-0.06392434984445572,0.08555492758750916,0.4899842143058777,0.05532388389110565,-0.17598390579223633,0.10805448889732361,-0.2391464114189148,-0.002468939870595932,0.2831767499446869,-0.3594035506248474,0.020072780549526215,-0.25229862332344055,0.2651166319847107,-0.11847811192274094,0.16640567779541016,-0.016530774533748627,-0.02173386886715889,0.26050132513046265,-0.23594743013381958,0.2564651668071747,-0.021962888538837433,-0.08896606415510178,0.19596336781978607,-0.014752073213458061,-0.2035893201828003,-0.31917476654052734,0.20010638236999512,-0.31170737743377686,4.354555858299136e-05,-0.014122143387794495,-0.22220467031002045,0.08353092521429062,0.04189712554216385,0.14531037211418152,0.35930922627449036,-0.2904878258705139,0.4459243416786194,-0.026339184492826462,0.2921150028705597,0.020086558535695076,-0.11136262118816376,-0.14830368757247925,-0.01047505997121334,0.05775384604930878,-0.29375237226486206,0.16288317739963531,0.16786335408687592,-0.22513961791992188,0.07846525311470032,-0.05281110107898712,-0.3807392120361328,-0.09849583357572556,-0.11161307990550995,-0.04977061599493027,-0.15544745326042175,0.20353300869464874,0.14936485886573792,0.12085267156362534,0.06888781487941742,0.04684794694185257,0.1394582986831665,0.09132624417543411,-0.2062046229839325,-0.26723185181617737,0.07246167957782745,-0.0016718273982405663,0.000339481484843418,0.2679312825202942,-0.05440336465835571,-0.012117539532482624,0.15272147953510284,-0.01691334694623947,0.27181780338287354,0.14367324113845825,0.19249792397022247,0.24992865324020386,0.14999955892562866,-0.21888864040374756,0.08994044363498688,0.015303967520594597,-0.3214448392391205,-0.14452683925628662,0.3218010365962982,0.09222419559955597,0.25074896216392517,-0.27105221152305603,-0.228887677192688,-0.289486289024353,-0.06157572194933891,0.07906362414360046,-0.2172461599111557,0.04504138231277466,0.10777714848518372,0.08680730313062668,-0.12852738797664642,-0.2985581159591675,-0.3081569969654083,0.054982393980026245,0.1354409158229828,0.17947372794151306,-0.14999975264072418,0.1137518510222435,-0.09327903389930725,-0.1944749355316162,0.34126678109169006,-0.18313084542751312,0.025434639304876328,0.06805305927991867,0.055031243711709976,-0.22965635359287262,-0.16465938091278076,0.031591035425662994,-0.12976957857608795,0.1259087175130844,0.2135431319475174,-0.060114786028862,-0.2713052034378052,0.013381600379943848,0.024797366932034492,-0.21018202602863312,-0.06328069418668747,-0.2520691454410553,-0.22713971138000488,0.2635674476623535,-0.15785612165927887,-0.17276521027088165,-0.12470505386590958,0.058700378984212875,-0.013267315924167633,-0.15169461071491241,-0.10132993757724762,0.1369570642709732,-0.21274462342262268,0.10247775912284851,-0.29426735639572144,-0.13992305099964142,-0.2080194056034088,-0.20145495235919952,0.18705585598945618,-0.32012948393821716,0.3075442314147949,-0.11203921586275101,-0.14103056490421295,0.11743821203708649,0.09678921848535538,-0.40137049555778503,0.23212918639183044,-0.08545303344726562,-0.4009428918361664,-0.1600007265806198,-0.2926003038883209,0.14506149291992188,-0.20846103131771088,-0.00020561600103974342,-0.18627779185771942,0.0377236008644104,-0.2899378538131714,0.1641903668642044,0.011854847893118858,0.29520079493522644,0.3853478729724884,0.24713757634162903,-0.016911910846829414,-0.3628389537334442,-0.04127059131860733,0.12452393025159836,-0.035087671130895615,-0.08223295956850052,0.30049535632133484,0.11587151139974594,-0.23456822335720062,-0.4470214545726776,0.25970765948295593,0.19170522689819336,-0.24674953520298004,-0.10413740575313568,-0.332980215549469,-0.20280495285987854,-0.2495821714401245,0.1438116431236267,-0.30656322836875916,0.3396279215812683,0.039826370775699615,-0.19677734375,-0.21102003753185272,0.2965305745601654,0.15274402499198914,-0.20625223219394684,0.0324527844786644,0.028922848403453827,-0.25981712341308594,0.2629712224006653,-0.020794592797756195,-0.36724528670310974,0.1187116876244545,-0.24488456547260284,-0.20988482236862183,0.11214832961559296,-0.3181012272834778,-0.1926967054605484,-0.19342513382434845,0.14345403015613556,-0.062302324920892715,-0.21597862243652344,0.3077905774116516,0.23893998563289642,-0.08166347444057465,0.03340578079223633,0.018889380618929863,0.1964087188243866,-0.1299189329147339,-0.21822527050971985,-0.13231664896011353,-0.1984526515007019,-0.18854458630084991,0.07716930657625198,-0.02549578621983528,-0.15016931295394897,-0.20323523879051208,0.08342169970273972,0.009309310466051102,-0.09418875724077225,-0.06044953688979149,0.20410281419754028,-0.004086857195943594,0.07724862545728683,-0.2936144769191742,-0.31447163224220276,-0.24582384526729584,0.0432920977473259,-0.218478262424469,0.2292633056640625,-0.3026933968067169,0.31524842977523804,-0.18153980374336243,0.0030812020413577557,0.07731188088655472,0.08331844955682755,-0.056961581110954285,-0.05206203833222389,-0.02418626844882965,0.18519732356071472,-0.1764908730983734,-0.26826897263526917,0.2913739085197449,0.23860996961593628,0.1825995147228241,0.24285948276519775,-0.07494228333234787,0.2427482157945633,-0.27144497632980347,0.13299371302127838,-0.050744589418172836,-0.2464868426322937,-0.06360290944576263,-0.2726570665836334,0.31119099259376526,0.19483722746372223,0.25066494941711426,-0.005016566254198551,0.061664171516895294,0.1758013665676117,-0.3436416685581207,-0.24943381547927856,-0.03228021040558815,0.018300721421837807,-0.2057548612356186,0.06150146573781967,-0.04294198378920555,-0.0264259222894907,-0.3303729295730591,-0.2692030072212219,-0.22814875841140747,0.33140090107917786,-0.112357959151268,0.1993466168642044,0.2470066249370575,0.3142997622489929,-0.016268793493509293,-0.25860732793807983,-0.33694595098495483,0.17081625759601593,0.19072780013084412,0.037448372691869736,-0.06184304505586624,-0.1020459532737732,0.28566741943359375,0.32366570830345154,-0.023366441950201988,-0.3758666515350342,0.1643650382757187,-0.03758605569601059,0.17140865325927734,-0.0513337142765522,-0.16386277973651886,0.0741131454706192,-0.16646669805049896,-0.274320125579834,-0.2655836343765259,-0.26898810267448425,-0.11554992944002151,-0.25926658511161804,-0.25876763463020325,-0.11144942045211792,-0.19529542326927185,0.08846376091241837,0.13244682550430298,0.20487961173057556,-0.23380988836288452,-0.24128468334674835,-0.21668457984924316,0.21033832430839539,-0.2568014860153198,0.11042507737874985,-0.2083297073841095,0.008328879252076149,-0.025215350091457367,-0.023964470252394676,0.06403842568397522,0.18096701800823212,0.0010026597883552313,0.01427439134567976,-0.2189875841140747,0.11669693142175674,0.010730734094977379,-0.15517660975456238,-0.041497230529785156,-0.03624912351369858,0.20074661076068878,0.24683284759521484,-0.17957879602909088,0.133704274892807,-0.2560625672340393,-0.20283368229866028,0.3231142461299896,-0.20686745643615723,-0.02918795496225357,-0.04721374437212944,0.08886683732271194,0.25189077854156494,-0.12986111640930176,0.21523603796958923,0.2424830198287964,0.23665663599967957,0.14677073061466217,-0.1805950403213501,0.31459659337997437,-0.2622368037700653,0.1536836326122284,-0.24385187029838562,-0.21771350502967834,-0.11470148712396622,0.14685028791427612,0.2108452171087265,0.07429499924182892,0.06512873619794846,0.16923631727695465,-0.04705612361431122,0.11930381506681442,-0.028645504266023636,0.22076112031936646,-0.06734645366668701,0.06968695670366287,-0.13017919659614563,-0.1511068195104599,0.009980103000998497,0.1954379379749298,0.15225769579410553,0.2077215611934662,-0.10000360012054443,-0.20234520733356476,0.30792948603630066,0.19312959909439087,-0.190922349691391,-0.10631544142961502,-0.0616106279194355,-0.06250187754631042,0.08949856460094452,0.2859296202659607,0.16627293825149536,0.08259572088718414,0.17760902643203735,0.29162800312042236,-0.07316127419471741,-0.1504751741886139,-0.07925203442573547,0.028151625767350197,0.29825249314308167,-0.0012781367404386401,-0.24412411451339722,-0.11306166648864746,0.15892145037651062,-0.17046505212783813,-0.14414122700691223,0.11669822782278061,-0.24864241480827332,-0.020423617213964462,0.0825691893696785,0.24608153104782104,0.04269499331712723,0.2331354022026062,-0.2835075855255127,-0.19812804460525513,-0.20316682755947113,0.1414734423160553,0.05111328512430191,-0.10481369495391846,0.34801438450813293,-0.13709913194179535,-0.23373207449913025,-0.1866595596075058,0.21698488295078278,-0.2590939998626709,-0.16889454424381256,-0.028769152238965034,0.29148879647254944,0.23669679462909698,0.10726755857467651,-0.04018246382474899,0.31838950514793396,0.0909787118434906,-0.31394147872924805,-0.3015976846218109,-0.27266329526901245,-0.10412454605102539,-0.15496334433555603,-0.06468474119901657,-0.13855214416980743,-0.05809742957353592,-0.09896644949913025,-0.15993693470954895,-0.14081907272338867,0.31115859746932983,-0.2224349081516266,0.038493476808071136,-0.18141910433769226,-0.24779687821865082,-0.20104600489139557,-0.21253305673599243,-0.000540928274858743,-0.14824256300926208,-0.02751029282808304,-0.051558151841163635,0.06069452315568924,-0.2302059531211853,0.1489153951406479,-0.05393669381737709,0.2784190773963928,0.1793469786643982,0.21956190466880798,0.2546943724155426,-0.330780029296875,-0.07097731530666351,-0.2339654564857483,-0.03680894523859024,-0.004618960898369551,0.2625179588794708,-0.216008260846138,0.08033356070518494,-0.14548254013061523,-0.24793145060539246]},{"shape":[32],"values":[0.24634675681591034,-0.12798520922660828,-0.2391349971294403,0.15213005244731903,-0.09833977371454239,-0.2793714106082916,0.3702792823314667,-0.10343068838119507,0.17288613319396973,-0.35298117995262146,-0.15665455162525177,0.37406638264656067,0.06796953827142715,-0.3649328947067261,0.08485689759254456,0.16561244428157806,0.07217051833868027,0.09297734498977661,0.39286425709724426,-0.0389639288187027,-0.1799056977033615,0.33998608589172363,-0.11751320958137512,0.38445693254470825,-0.17479144036769867,-0.3915707767009735,-0.23004625737667084,0.08715096861124039,-0.3014598488807678,-0.38768133521080017,0.07995599508285522,0.28490322828292847]},{"shape":[32,32],"values":[0.28973498940467834,-0.5410449504852295,-0.8264572024345398,0.06520415842533112,-0.4102485179901123,-0.955267071723938,-0.6461390256881714,0.03660126030445099,-1.0422857999801636,0.5916104316711426,0.37464025616645813,-0.7798923254013062,-0.734919548034668,0.3034369945526123,0.7456902265548706,0.24282512068748474,0.5401924252510071,1.1991081237792969,-0.5310462117195129,-0.9633222222328186,1.0046136379241943,-0.327852725982666,0.5434975028038025,0.36027219891548157,-0.6568329930305481,0.7549927830696106,0.6052625179290771,-0.4311177730560303,0.13539160788059235,1.1055424213409424,-0.4979882836341858,0.7167884707450867,-0.4637007415294647,0.2281436175107956,-0.5959152579307556,0.09141281992197037,-0.0037950549740344286,-0.26702138781547546,-0.2895486354827881,-0.42103147506713867,0.05143335834145546,-0.08068933337926865,-0.21329592168331146,-0.31826451420783997,-1.4000397641211748e-05,-0.1976223886013031,0.4028500020503998,0.11202697455883026,0.4586315155029297,0.06915900111198425,-0.2844584584236145,-0.15138258039951324,0.5109984278678894,0.02208125963807106,0.23150674998760223,-0.19980405271053314,-0.34374532103538513,0.12109727412462234,0.06972377747297287,-0.3094088137149811,0.3107718229293823,0.04376469552516937,-0.2790173292160034,0.8485510945320129,0.25107529759407043,-0.21497425436973572,-0.3409743905067444,1.0296472311019897,0.484866738319397,-0.21210521459579468,-0.6776278018951416,0.7063863277435303,1.067529320716858,0.5524924397468567,0.05975227802991867,-0.6340082883834839,0.5840277671813965,0.5538191795349121,1.0519428253173828,-0.6076704263687134,-0.4992177486419678,0.07549851387739182,0.5416562557220459,0.9072591066360474,0.7989566326141357,-0.22803053259849548,0.7790233492851257,0.7321038842201233,-0.5398131012916565,-0.05373099446296692,-0.8879536986351013,-0.5734808444976807,0.6568708419799805,-0.17199304699897766,-0.6259459257125854,0.22132693231105804,0.08034881949424744,0.6069542765617371,0.15073294937610626,-0.5178927183151245,0.1329227089881897,0.4076182544231415,0.46420711278915405,0.04264403507113457,0.11855927109718323,0.17949075996875763,0.07252241671085358,0.5535178184509277,0.045309897512197495,-0.3547971248626709,-0.6016296744346619,0.014379606582224369,0.2044605314731598,-0.6603805422782898,0.304015576839447,0.2683418393135071,-0.4152921736240387,-0.05886036530137062,-0.6052635312080383,0.06176399067044258,0.027743756771087646,-0.06693191826343536,-0.209868922829628,0.537281334400177,-0.057225301861763,-0.02736399881541729,0.5139009952545166,-0.09768586605787277,-0.07926667481660843,-0.44017544388771057,-0.7713490128517151,0.5505515933036804,-0.0464734248816967,-0.17102688550949097,-0.5645861029624939,-0.1364136040210724,-0.3749242424964905,-0.40783026814460754,-0.026731764897704124,-0.5935409069061279,-0.1845640242099762,-0.3598054051399231,0.5172508358955383,0.02749498561024666,0.24018433690071106,0.005121058784425259,-0.3100666403770447,-0.12930038571357727,0.3085586726665497,-0.1404188722372055,0.5950319766998291,-0.1080712303519249,-0.2683872878551483,0.49366387724876404,0.1204049289226532,-0.7551159858703613,0.04955446347594261,0.3193962275981903,-0.3496864140033722,0.7215724587440491,-0.19083531200885773,-0.007301434874534607,-0.3584243357181549,-0.362310528755188,0.113491490483284,-0.017404787242412567,-0.16213802993297577,-0.33270716667175293,0.15051791071891785,-0.15328596532344818,0.01392557192593813,-0.35660654306411743,0.006807831581681967,-0.2931954264640808,0.299259752035141,0.25371602177619934,-0.04384706914424896,-0.037238769233226776,-0.4528809189796448,-0.21791723370552063,0.05318811908364296,-0.11285888403654099,-0.11894652247428894,-0.3618643879890442,-0.24680352210998535,-0.18819570541381836,0.057931169867515564,-0.43841102719306946,0.1210857704281807,0.01750471256673336,-0.13098593056201935,-0.02397930435836315,-0.36471888422966003,0.4857352375984192,1.274552822113037,-0.7642486691474915,-0.47315284609794617,0.022970667108893394,0.5989998579025269,-0.20970171689987183,-0.6781348586082458,-0.3303883969783783,0.28502246737480164,0.05202944949269295,-0.3806994557380676,-0.155201256275177,-0.4973970949649811,0.9739876985549927,0.6849797964096069,0.008873692713677883,-0.7537391185760498,-0.9972413182258606,-0.40301546454429626,0.1894792765378952,-0.7125951051712036,-0.24939553439617157,0.35789528489112854,0.598572850227356,0.6372287273406982,0.7527804970741272,-0.6921091079711914,0.4910908043384552,0.8268334269523621,-0.27644380927085876,-0.34092023968696594,-0.55544513463974,-0.1715870201587677,0.36784377694129944,0.037845052778720856,-0.0831194818019867,0.04221135377883911,0.39126768708229065,0.3475315272808075,-0.03728589415550232,-0.0886756107211113,0.19057674705982208,0.6788679361343384,-0.0911911278963089,0.21603690087795258,-0.47822198271751404,-0.34487977623939514,-0.1985587477684021,0.3321402370929718,0.4096674919128418,-0.010310174897313118,0.23715530335903168,-0.05231330543756485,-0.07793932408094406,0.18921399116516113,-0.10263430327177048,-0.7214526534080505,0.02496834471821785,0.01206211932003498,-0.3961942195892334,-0.5846900939941406,-0.598490834236145,-0.02014652080833912,0.13562573492527008,-0.47527554631233215,0.08919261395931244,-0.15310527384281158,-0.39238253235816956,-0.0705425962805748,-0.28093990683555603,-0.4449191391468048,0.20483560860157013,-0.049476560205221176,-0.13834646344184875,-0.2719151973724365,-0.04921182245016098,0.3243148624897003,0.09498663246631622,0.43811851739883423,0.2243017852306366,-0.4748874604701996,-0.36491212248802185,0.3222978115081787,0.10290370136499405,0.3922441899776459,-0.33894285559654236,-0.15040665864944458,0.42479604482650757,0.4891136586666107,-0.18832367658615112,0.1908181756734848,0.4131510555744171,0.14088967442512512,0.3599676489830017,0.31998056173324585,0.00472420547157526,0.14624467492103577,0.8952357172966003,0.5469678640365601,0.3102000951766968,-0.5365906357765198,0.13686257600784302,1.1500412225723267,-0.2571139633655548,0.28021571040153503,-0.09804340451955795,0.6146405935287476,0.28477147221565247,0.8677011132240295,-0.7059022188186646,-0.7741734981536865,-0.3549143671989441,0.3086856007575989,0.668011486530304,0.4169404208660126,0.19476903975009918,0.7421868443489075,0.1242673471570015,-0.518695592880249,-0.28802287578582764,-0.343879759311676,-0.762458324432373,0.021958865225315094,-0.6340534090995789,-0.4543546438217163,0.4824621379375458,0.5301802158355713,-0.21621285378932953,-0.2056668996810913,0.08177340775728226,-0.31509578227996826,0.018097665160894394,-0.46225422620773315,0.41742897033691406,-0.17154011130332947,0.27352720499038696,0.42139288783073425,-0.38412848114967346,0.3345496952533722,0.6512423753738403,0.3669359087944031,-0.05681375041604042,-0.3801385760307312,0.2539207935333252,-0.10071910172700882,0.3102000951766968,0.2682085335254669,-0.19114944338798523,0.32670652866363525,0.7272768020629883,-0.5671923756599426,0.3471482992172241,-0.36453965306282043,-0.38075289130210876,0.2902016043663025,-0.1738939732313156,-0.5896941423416138,0.48318588733673096,-0.19956816732883453,-0.04756203666329384,0.508127748966217,-0.6252098083496094,-0.10778464376926422,-0.1343374103307724,0.7682515978813171,-0.11852575838565826,-0.4852006733417511,-0.3259974420070648,-0.3558543622493744,0.6180370450019836,-0.30879658460617065,0.09648140519857407,-0.847740113735199,0.4485898017883301,0.17824940383434296,0.026977375149726868,-0.09503543376922607,-0.404437780380249,-0.46912068128585815,0.41753873229026794,-0.6739163994789124,-0.39129623770713806,0.4893474280834198,-0.20777583122253418,0.6071867942810059,0.682578444480896,-0.2954968512058258,0.25457072257995605,0.3368857800960541,-0.8449335694313049,0.7932724952697754,-0.23960421979427338,0.2723166346549988,-0.03552224487066269,-0.23692508041858673,-0.46431082487106323,-0.4225209951400757,0.6333377957344055,-0.14707385003566742,0.3803320527076721,0.15662337839603424,-0.13986383378505707,-0.35769689083099365,0.9011930823326111,0.49599605798721313,-0.38413649797439575,0.1898200362920761,0.2032538503408432,-0.26886987686157227,0.1716214269399643,0.23839092254638672,-0.0326688177883625,0.33033138513565063,1.0100913047790527,-0.4370062053203583,0.6447597146034241,-0.11991941928863525,-0.2428024262189865,0.4591441750526428,-0.0036542031448334455,-0.7008862495422363,0.38367462158203125,0.4975453019142151,-0.06656394898891449,-0.05727805942296982,0.9299880266189575,0.43491852283477783,0.4299081563949585,-0.35945019125938416,0.15910303592681885,1.3281023502349854,0.2323150634765625,-0.019437013193964958,-0.3230731785297394,0.7092258334159851,0.5342548489570618,0.7040529847145081,-0.7698097229003906,-0.823293149471283,-0.8670211434364319,0.7978492379188538,1.3088274002075195,0.15358352661132812,0.4110774099826813,0.35062724351882935,0.2402467131614685,-0.059531498700380325,-0.5319064259529114,-0.832220733165741,-0.10382422804832458,0.5199766159057617,-1.2246495485305786,-0.512577474117279,0.40678858757019043,-0.25757938623428345,0.24627116322517395,1.1378660202026367,-0.8137826919555664,-0.20871226489543915,0.5217025279998779,0.9874327182769775,0.10349493473768234,0.03514113649725914,-0.1917121410369873,0.03492238372564316,0.6506175994873047,0.4766818583011627,-0.36053571105003357,-0.7845381498336792,-0.003745644586160779,-0.09838967770338058,-0.3913559317588806,0.10970311611890793,0.3793821632862091,-0.8676871061325073,0.034958478063344955,-1.1136517524719238,-0.3683367371559143,0.6646263003349304,-0.14149129390716553,0.04686257615685463,1.146405816078186,-0.6964967846870422,-0.3332774043083191,0.6065131425857544,-0.9358606338500977,-0.8184587359428406,0.6808913946151733,0.29108065366744995,-0.772972822189331,-0.09046132862567902,-0.33815667033195496,0.19319365918636322,-0.953075110912323,-0.545831561088562,-0.8375999331474304,-0.2878376543521881,-0.0332970954477787,-0.1003679409623146,-0.7831978797912598,-0.6855728626251221,0.6398205757141113,0.2483072429895401,0.2639893889427185,-0.4067416191101074,-0.16044451296329498,-0.5428904891014099,0.37182411551475525,-0.4130125939846039,-1.0600627660751343,-0.00537259504199028,0.19392308592796326,0.8062600493431091,0.5595628619194031,-0.36700108647346497,-0.03070291317999363,0.8113101124763489,-0.21050870418548584,0.36061161756515503,0.1720695048570633,-0.4087980389595032,0.3896872401237488,-0.3523116111755371,-0.5209660530090332,-0.9139407277107239,-0.20965991914272308,-0.5921805500984192,0.13092952966690063,0.30242252349853516,-0.7540211081504822,-0.27005535364151,0.5128797888755798,0.8080195784568787,0.309684693813324,0.3259839415550232,0.2735708951950073,-0.19698941707611084,0.05561663582921028,0.4202830195426941,-0.035978253930807114,0.5271927714347839,0.3207690119743347,-0.46831104159355164,0.6854113936424255,0.5812349915504456,-0.6624550819396973,0.3661787509918213,0.19396410882472992,-0.3373624086380005,0.48451849818229675,-0.23615604639053345,-0.036797091364860535,0.12211918830871582,-0.4562998414039612,-0.09896357357501984,0.2872767150402069,0.3189954161643982,0.01778334006667137,0.3217528760433197,-0.15546099841594696,-0.54010009765625,0.1359788477420807,-0.3577057123184204,-0.32855474948883057,-0.3858676254749298,0.22843143343925476,-0.002846956253051758,-0.0702151283621788,-0.1691153198480606,-0.09126486629247665,-0.39432618021965027,0.6933901309967041,-0.1745297759771347,-0.31163904070854187,0.12245514988899231,-0.04508258402347565,-0.3481594920158386,0.003542222548276186,0.048530213534832,-0.22558479011058807,0.3729837238788605,-0.3362500071525574,0.42817187309265137,0.3021634519100189,-0.1351853907108307,-0.3822517693042755,-0.8401013016700745,-0.03805891424417496,-0.1279073804616928,0.08176835626363754,-1.5038193464279175,0.4255935251712799,0.558860182762146,-0.05710865184664726,-1.3261759281158447,0.41733917593955994,-0.13939142227172852,0.51421719789505,0.44918784499168396,0.848525881767273,-0.2996837794780731,-0.7853986620903015,0.46525296568870544,-0.10906517505645752,0.14036443829536438,0.27064695954322815,-0.2561000883579254,0.9245616793632507,0.6700090765953064,-0.2609019875526428,-0.3949807584285736,1.3964823484420776,0.03586200252175331,0.28496095538139343,-1.2196961641311646,0.2661580443382263,-0.19105125963687897,-0.5681206583976746,0.07493648678064346,0.15835246443748474,0.4105754792690277,-0.6097162961959839,-0.3517274558544159,-0.4130825698375702,-0.5590596795082092,0.44368740916252136,-0.11628777533769608,-1.0499578714370728,-0.5689568519592285,0.4890921711921692,-0.06294434517621994,-0.5628564953804016,-0.27876245975494385,-0.2217947095632553,-0.3393351137638092,0.35364609956741333,-0.3046586811542511,-1.3043787479400635,0.15685172379016876,-0.24159936606884003,0.37476876378059387,0.463898628950119,-0.09133343398571014,-0.0028233106713742018,0.36240798234939575,-0.48838895559310913,0.3828364312648773,-0.11103462427854538,-0.29667332768440247,0.6313633322715759,-0.11866343766450882,-0.07539507746696472,-0.2547187805175781,0.7329267263412476,0.36063438653945923,0.03869938850402832,0.41189903020858765,-0.5308207869529724,-0.05596153065562248,0.6299906969070435,0.40878742933273315,-0.6703009605407715,-0.15214301645755768,0.4826943278312683,0.2626848816871643,0.03272094577550888,0.5964562892913818,-0.24623534083366394,0.6001583337783813,0.5722787976264954,-0.6904587745666504,0.272805392742157,-0.40913206338882446,-0.12129579484462738,0.3521156907081604,-0.017714915797114372,-0.5910543203353882,0.5259605646133423,0.9402877688407898,0.2583773732185364,0.14434579014778137,0.20042753219604492,-0.02709583379328251,-0.12106799334287643,0.18429982662200928,0.2693314850330353,-0.03550572693347931,0.4746996760368347,0.556167483329773,-0.14708982408046722,-0.23022931814193726,0.7426558136940002,0.2543995678424835,0.08339189738035202,-0.2836083173751831,-0.061586104333400726,-0.2451310008764267,0.10513130575418472,0.17144261300563812,-0.6500352025032043,0.01591130904853344,0.7042129635810852,0.09083348512649536,0.28078070282936096,0.20478373765945435,-0.16502945125102997,-0.22222790122032166,0.3985705077648163,-0.24857060611248016,-0.02523684874176979,0.28045347332954407,-0.25999534130096436,-0.10868982970714569,0.5765261650085449,0.054310161620378494,-0.4814302623271942,-1.0282461643218994,-0.016785889863967896,0.1986611783504486,0.12091987580060959,0.40154045820236206,-0.781632661819458,-0.1756410151720047,0.13816359639167786,0.8195673823356628,-0.25478479266166687,-0.2933003306388855,0.7115838527679443,0.2451380342245102,-0.29230746626853943,1.0539799928665161,0.16781164705753326,0.9412104487419128,0.5608577132225037,-0.890094518661499,0.683028519153595,0.2301977574825287,-0.847094714641571,0.19128789007663727,0.3893412947654724,-0.5099725127220154,0.7173230051994324,-0.20149002969264984,1.100445032119751,0.8730995059013367,-0.7801007032394409,-0.7420291900634766,0.10628417134284973,0.30920326709747314,-0.7133331298828125,-0.7004656791687012,-0.20960089564323425,0.5445324778556824,0.09813888370990753,-0.5192908644676208,-0.21938060224056244,-0.901496946811676,0.891741931438446,0.36098068952560425,-0.09804495424032211,-0.6778091192245483,-0.9942260384559631,-0.2479168176651001,-0.07631266117095947,-0.8508890867233276,-0.32669004797935486,0.016472307965159416,-0.10460688173770905,0.8541299700737,0.511819064617157,-0.30748966336250305,0.34271302819252014,0.48729655146598816,-0.27808600664138794,0.12077837437391281,0.47200167179107666,0.4017218053340912,0.20428645610809326,0.08455117791891098,-0.053935904055833817,0.017427828162908554,0.4820438623428345,0.6325896382331848,-0.3315508961677551,0.28191104531288147,0.14647796750068665,0.4693807363510132,0.10906501114368439,-0.12356812506914139,-0.6130954623222351,-0.4715564548969269,-0.289959192276001,0.16138888895511627,0.6533408164978027,-0.20434734225273132,0.10467150807380676,0.23922905325889587,0.286886602640152,0.08436887711286545,-0.36179056763648987,-0.7841504812240601,-0.19265934824943542,0.28007373213768005,-0.7004937529563904,-0.1202538013458252,-0.4505728781223297,-0.41526395082473755,-0.05275923013687134,-0.011165049858391285,0.5167484879493713,0.5624412298202515,0.2662222981452942,0.13671204447746277,-0.3293517827987671,0.6632835268974304,-0.3678348660469055,-0.11666931211948395,0.34430158138275146,0.6626098155975342,-0.5697770714759827,-0.09223742038011551,-0.3060261011123657,-0.056341953575611115,-0.755912721157074,-0.0696839988231659,0.4521799683570862,-0.11637464910745621,0.2996639013290405,0.007394472602754831,-0.48693332076072693,-0.13806605339050293,-0.5553992390632629,-0.523682713508606,0.4569709897041321,-0.0884450152516365,-0.5642644762992859,-0.006836562417447567,-0.14470525085926056,0.6594476699829102,-1.015129566192627,-1.3485356569290161,0.8064321279525757,0.1812475323677063,-0.6413338780403137,-1.0398906469345093,0.5691708922386169,0.9555869698524475,0.0943351462483406,-0.33986201882362366,-1.0844275951385498,0.0018854651134461164,0.35266798734664917,1.2741096019744873,-0.6492457985877991,-0.15951086580753326,0.5626740455627441,0.35410836338996887,0.4776092767715454,0.5136843919754028,-0.07243897765874863,0.9803959131240845,0.672015368938446,-0.6550537943840027,0.2982715368270874,-0.8241580724716187,-0.7474340796470642,0.6611464023590088,-0.23182567954063416,-1.2052497863769531,0.9307647347450256,0.8623417615890503,0.3652331531047821,0.010253174230456352,0.25888746976852417,-0.5516970157623291,0.014352729544043541,-0.5334020853042603,0.4170890152454376,-0.20025233924388885,-0.037974584847688675,0.6851063370704651,-0.007901554927229881,0.20806027948856354,0.7112900614738464,0.08389827609062195,-0.2506144642829895,-0.007202534470707178,0.37202489376068115,-0.1202557161450386,-0.1616758108139038,0.11860792338848114,-0.2598493695259094,0.24476243555545807,0.7588306069374084,-0.5267176032066345,-0.1318809539079666,-0.0321972519159317,-0.35570821166038513,0.11484435200691223,0.40840256214141846,-0.15339244902133942,0.1776481717824936,-0.5267637968063354,-0.2124343067407608,0.051935333758592606,-0.1581462174654007,0.7220465540885925,0.0093666547909379,0.41131460666656494,-0.24103304743766785,0.7682883739471436,0.23709702491760254,-0.7463274002075195,0.6468712687492371,0.5498613715171814,-0.7195895910263062,-0.4395795166492462,-0.17138926684856415,0.11449487507343292,-0.06701650470495224,0.36364322900772095,0.09572909027338028,-0.26366737484931946,0.6130459308624268,-0.12847848236560822,-0.7385125756263733,0.7184975743293762,-0.4706989526748657,-0.2635701298713684,0.26798999309539795,-0.07902538776397705,-0.9011198878288269,0.16634345054626465,-0.7299131155014038,-1.069161057472229,0.7822937965393066,0.5952023267745972,-0.033664293587207794,1.1153464317321777,0.6379733085632324,0.23621870577335358,-0.3024752736091614,0.6759923696517944,-0.7603055238723755,-0.39207470417022705,0.27635467052459717,0.8238145709037781,-0.883685827255249,-0.6721670031547546,0.12315457314252853,-0.23993834853172302,-0.3843708038330078,0.3588481843471527,0.1998826265335083,-0.5122799873352051,0.4874553680419922,-0.5221794843673706,-1.3320386409759521,0.2954331338405609,-0.8235512375831604,-0.42738887667655945,0.3911912739276886,0.42471328377723694,-0.8874000310897827,0.16962814331054688,-0.4252476394176483,0.2239254266023636,0.3448978066444397,0.13006921112537384,-0.20264776051044464,-0.005330030340701342,0.3708030879497528,0.4243864417076111,0.22747962176799774,0.41639214754104614,0.11848529428243637,-0.03160189837217331,0.02876836434006691,0.44946789741516113,0.0810856819152832,-0.16864930093288422,-0.04597419500350952,0.10901686549186707,-0.13542722165584564,-0.0686318576335907,0.1608073115348816,-0.500139057636261,-0.20351643860340118,-0.3517710566520691,0.11349942535161972,0.34518611431121826,0.039544109255075455,-0.340971976518631,0.02452239952981472,-0.1416822075843811,-0.22655464708805084,0.09155061841011047,-0.07285654544830322,0.8156485557556152,0.11189281940460205,0.0017009591683745384,0.04280130937695503,-0.5711523294448853,-0.6362268328666687,-0.5560252666473389,0.4365249276161194,-0.9930769205093384,0.38956132531166077,0.4349776804447174,-0.8628082275390625,-0.9573285579681396,0.8471400737762451,0.37748366594314575,0.3869539797306061,0.4578506052494049,0.9010730981826782,-0.06049223989248276,-0.42984485626220703,0.5407437086105347,-0.5860691666603088,0.6376529932022095,0.6460440158843994,-0.7591471672058105,0.6456218957901001,0.20135483145713806,-0.2580212354660034,-0.32178521156311035,0.9361374974250793,-0.5668684840202332,0.377401739358902]},{"shape":[32],"values":[0.27484822273254395,0.07017829269170761,0.31222298741340637,-0.26092275977134705,-0.15073168277740479,0.004270494449883699,0.09315135329961777,0.0789744183421135,-0.34845560789108276,0.1151144802570343,0.28451868891716003,0.08370808511972427,-0.2124018520116806,0.2536422312259674,-0.20926815271377563,0.16257302463054657,0.06246935948729515,0.22304721176624298,-0.12075994163751602,-0.23499582707881927,-0.02669578418135643,-0.21122883260250092,-0.19161422550678253,0.3544677495956421,0.01574050821363926,0.2338884323835373,0.23306609690189362,0.1974865049123764,-0.2242376059293747,0.19196905195713043,0.28060412406921387,-0.09735903888940811]},{"shape":[32,9],"values":[-1.1989352703094482,0.4639102816581726,-0.09549835324287415,-1.0614827871322632,0.45392724871635437,0.4187159240245819,-0.7498741745948792,0.3389471471309662,0.4555675983428955,0.8225334882736206,0.2088208943605423,-1.0268864631652832,0.5053839087486267,0.03519011288881302,-0.06163545325398445,0.7745183110237122,0.4196527600288391,-0.0466555655002594,0.12399584800004959,-0.15092188119888306,-1.3188810348510742,0.30616259574890137,0.4311060309410095,-0.10134226083755493,-0.1524725705385208,0.22605833411216736,0.37626197934150696,-0.02695501036942005,0.060248345136642456,0.7692108750343323,-0.4738582670688629,-0.2354086935520172,0.9360381364822388,-0.23487141728401184,-0.7162982821464539,0.3836323320865631,0.9920768737792969,0.41326776146888733,0.18589121103286743,0.7185810208320618,-0.37620195746421814,-0.11501746624708176,-0.3902682960033417,-0.32619425654411316,-0.23866814374923706,0.40168261528015137,0.5629134178161621,-0.8152179718017578,0.4005160927772522,0.6371474862098694,-0.11043927818536758,0.40563884377479553,-0.3035918176174164,-0.5097577571868896,-0.01896040327847004,0.3852965235710144,-0.23279635608196259,0.12074054032564163,0.2607788145542145,-0.6473749279975891,0.21546311676502228,0.3981231451034546,-0.7486724853515625,-0.9036577939987183,-0.07188572734594345,-0.17500925064086914,-0.12828198075294495,0.54082190990448,0.1338871866464615,-1.1942641735076904,-0.1890755146741867,0.04491666704416275,1.3094971179962158,2.1397318840026855,0.8680766224861145,0.3172242045402527,0.08545190840959549,-0.26084816455841064,-0.3471822738647461,-0.5184475183486938,-0.3615112006664276,-1.3405882120132446,-0.3503320813179016,0.10489724576473236,-0.40770116448402405,0.3014720380306244,0.4629644751548767,-0.5096436738967896,-0.02840421162545681,0.3307543098926544,-0.6321372985839844,0.2126784324645996,-0.2461584061384201,-0.8141162395477295,-0.007499991916120052,-0.11425994336605072,-0.06885351985692978,0.42656150460243225,0.2042231559753418,0.27089250087738037,0.4609428644180298,-0.5869835615158081,0.45528584718704224,0.6533735394477844,-0.3081764578819275,-0.029085300862789154,0.35983648896217346,-1.0825302600860596,1.840780258178711,1.3460924625396729,-0.05206548050045967,0.058769989758729935,0.13989844918251038,0.1790313869714737,-1.3154758214950562,-1.0779362916946411,-0.7234088182449341,-0.7587615251541138,0.23976561427116394,-0.28063642978668213,-0.9544067978858948,0.3669275939464569,0.3355006277561188,-1.3683897256851196,0.6036087274551392,0.2854732275009155,-0.040892213582992554,-0.017766311764717102,0.664310097694397,0.034367628395557404,-0.3912004828453064,0.8852044939994812,-0.4016716778278351,-0.8002798557281494,0.8837572932243347,0.5779502391815186,-1.172762155532837,-0.6932163834571838,0.69698566198349,-0.4715871214866638,-0.826049268245697,0.6380746960639954,0.5892857313156128,-0.0283112283796072,-0.3601137697696686,-0.8095453381538391,-0.5677653551101685,0.39043793082237244,-0.5441881418228149,-0.6161037087440491,0.6968299150466919,-0.007436718791723251,1.2082284688949585,-0.9138746857643127,-1.1967097520828247,0.11937402188777924,-0.7782554626464844,-0.20588481426239014,-0.17902769148349762,-0.6517143249511719,0.6096556782722473,0.6457455158233643,-0.09724759310483932,0.6590502262115479,1.107347011566162,0.01780693605542183,0.4613862931728363,-0.4488401710987091,-0.9697385430335999,-0.2091834545135498,-0.6430327296257019,0.5233576893806458,0.542340099811554,1.5748398303985596,-0.07745029032230377,0.6393835544586182,-0.031093118712306023,-0.8442904949188232,-0.5828650593757629,-0.6673150062561035,-0.5463959574699402,-0.6497985124588013,0.8877924680709839,-0.3771786689758301,-0.7189632058143616,0.5951188206672668,-0.4205544888973236,-0.3205733597278595,1.1391934156417847,1.4878228902816772,0.09169577062129974,-0.08462534844875336,0.4517227113246918,-0.0677584707736969,-0.5606839060783386,-0.09860044717788696,-0.38418251276016235,0.1694307029247284,0.04625944048166275,-0.15482406318187714,0.9093537926673889,0.15518783032894135,-0.6497834920883179,0.9425863027572632,-0.18020352721214294,-0.707401692867279,0.8024618029594421,-1.2725878953933716,0.5821700692176819,-0.011521488428115845,-0.8319674730300903,0.3291285037994385,-0.08420336246490479,-1.4965994358062744,0.5199054479598999,0.3584181070327759,0.3111768662929535,0.34140244126319885,-0.14930962026119232,0.6970545649528503,0.3993353545665741,-0.572228193283081,0.5523932576179504,0.1807057410478592,-0.7877286076545715,-0.9324752688407898,-1.063322901725769,-0.21959830820560455,-1.0427623987197876,-0.33658987283706665,0.20687271654605865,-0.5611068606376648,0.47485998272895813,0.2047981172800064,-0.07872994989156723,-1.4974465370178223,-1.4006084203720093,0.002572618890553713,-0.4718643128871918,-0.5213684439659119,0.8278821110725403,1.0325000286102295,1.0976159572601318,-0.20906506478786469,0.37453925609588623,-0.9520119428634644,0.4233752489089966,0.5539579391479492,-0.10041161626577377,0.1491871178150177,0.6548774838447571,-0.7788050174713135,0.09723006188869476,0.026184607297182083,1.0046941041946411,0.0972708985209465,-0.5049022436141968,0.5964870452880859,-0.30477651953697205,-0.24866484105587006,-0.31968948245048523,-1.2099682092666626,-0.6794850826263428,-0.38200652599334717,-1.0299255847930908,0.14934559166431427,0.3854273855686188,0.28860604763031006,0.39897727966308594,0.5748359560966492,0.3556179702281952,-0.024709319695830345,-0.8182770609855652,0.42708539962768555,0.23391655087471008,-0.7204897403717041,0.5330204963684082,0.7760778069496155,-0.7849507331848145,-0.005234014242887497,-0.7128791213035583,0.7473661303520203,-0.14271381497383118,-0.7289249897003174,0.7691843509674072,0.1335296928882599,-0.2273411899805069,0.6997437477111816]},{"shape":[9],"values":[-0.11376062035560608,-0.14930874109268188,-0.1807243525981903,-0.042470742017030716,-0.004386167041957378,0.011529074050486088,-0.004605733789503574,0.19349737465381622,0.17707209289073944]}],"configuration":{"hiddenLayers":[32,32],"learningRate":0.003,"batchSize":128,"epochsPerRound":3,"samplesPerRound":4096}} \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/drivers/driver-47.json b/packages/demo-car-circuit/src/public/drivers/driver-47.json new file mode 100644 index 0000000..f657465 --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/driver-47.json @@ -0,0 +1 @@ +{"format":"ignition-driver-v1","algorithm":"imitation-mlp","driving":"circuit-racing-v1","observation":"racing-observation-v1","protocol":"racing-learning-v1","seed":47,"updates":48,"samples":65536,"createdAt":"2026-09-06T16:10:27.396Z","weights":[{"shape":[20,32],"values":[0.7642660737037659,-0.8859182000160217,-0.5176710486412048,0.3376449942588806,-0.10664648562669754,0.676186740398407,-0.5060363411903381,-0.2038159966468811,-0.6507564783096313,-0.5048707723617554,0.17842808365821838,-0.7504705190658569,0.29925915598869324,-0.02318069525063038,-0.2735099196434021,-0.16358107328414917,-0.3111725151538849,0.26450178027153015,0.7350045442581177,0.638462483882904,-0.8949164152145386,-1.0099148750305176,-0.2804567217826843,-0.26642102003097534,0.5651455521583557,0.8924463391304016,-0.5133675336837769,0.8997725248336792,0.46127063035964966,-0.19602170586585999,-0.2782912850379944,0.041944995522499084,-0.007601778022944927,0.08722333610057831,0.28328436613082886,-0.05754195153713226,0.04414936527609825,0.4414471685886383,0.1857951283454895,0.12396416813135147,0.18964123725891113,0.05862794443964958,0.009896034374833107,0.05080259218811989,0.14835768938064575,0.2706579566001892,0.19805659353733063,-0.3148568868637085,-0.2682976722717285,0.17287760972976685,-0.07331636548042297,0.485231876373291,0.23381772637367249,-0.07099061459302902,-0.14993859827518463,0.20926466584205627,0.1016392856836319,-0.011439988389611244,0.2197723388671875,0.1347050964832306,0.1216159388422966,0.07544031739234924,0.17560110986232758,-0.19242390990257263,0.050755783915519714,0.16603292524814606,0.11934289336204529,0.09101244062185287,0.5020192861557007,-0.6208327412605286,-0.23257885873317719,0.29104092717170715,-0.25879883766174316,-0.231947124004364,0.5565145611763,0.0509224459528923,-0.09063924849033356,-0.20653361082077026,-0.16676031053066254,-0.15417639911174774,0.2701468765735626,-0.30092477798461914,0.3325224220752716,0.5347676873207092,0.34248700737953186,-0.181660458445549,0.2524857223033905,-0.026931332424283028,0.7868600487709045,0.3922673463821411,-0.0938287228345871,-0.16177129745483398,-0.2094954252243042,-0.18202842772006989,0.24043726921081543,-0.8132220506668091,0.34130656719207764,0.4515869915485382,0.24021638929843903,-0.2176899015903473,-0.3150295317173004,0.12239711731672287,0.2307722121477127,-0.250873327255249,0.26066842675209045,0.21399566531181335,-0.035759784281253815,0.25998783111572266,0.3238540589809418,0.5754225850105286,-0.33731770515441895,0.49159693717956543,-0.0002798694185912609,-0.06319838017225266,0.1655692607164383,-0.23880605399608612,-0.3183359205722809,0.13245493173599243,0.013336270116269588,0.14063389599323273,0.2902415990829468,0.34963735938072205,-0.20436882972717285,0.4861222505569458,0.32077619433403015,0.5440286993980408,-0.16748107969760895,-0.23785129189491272,1.5046969652175903,0.335479736328125,1.5494805574417114,-1.9615793228149414,1.7880560159683228,-1.2943705320358276,1.6060060262680054,-2.075364112854004,0.9817459583282471,1.4549915790557861,-0.8598465323448181,1.16006600856781,1.4648237228393555,1.1368632316589355,1.294223666191101,1.9530129432678223,-1.176387071609497,1.9064488410949707,1.5138956308364868,-1.509608268737793,-0.9118660688400269,-0.14679782092571259,1.6928884983062744,1.0497459173202515,0.3287605047225952,0.028737647458910942,0.9630666971206665,0.9817900657653809,-0.6381598711013794,-1.0904531478881836,1.4460508823394775,1.8514190912246704,0.11378590762615204,-0.07049398869276047,-0.3348352909088135,-0.29896968603134155,0.11280446499586105,-0.045449770987033844,-0.04011223092675209,-0.02909564971923828,-0.3916924297809601,0.45235776901245117,0.0069480882957577705,-0.0901031345129013,0.3677041530609131,0.07143451273441315,0.09963955730199814,0.2476356029510498,-0.07341286540031433,0.44463011622428894,-0.2541571259498596,0.04663533717393875,-0.023980464786291122,0.25771263241767883,0.2194179892539978,0.5643407106399536,0.1161392405629158,0.07342984527349472,0.5094682574272156,-0.06146106868982315,-0.08237854391336441,-0.20370036363601685,-0.07098361104726791,-0.3143114149570465,-0.26052147150039673,0.24801546335220337,-0.0698634535074234,0.2699098587036133,-0.058588702231645584,-0.26963409781455994,0.28028079867362976,-0.04456978291273117,-0.08073339611291885,0.07685921341180801,-0.11570974439382553,-0.053830835968256,-0.10251431167125702,-0.16669681668281555,-0.1658896505832672,-0.030430104583501816,-0.1761142760515213,0.1656298041343689,-0.12203798443078995,0.23590826988220215,-0.07380900532007217,-0.14254625141620636,-0.2616182267665863,0.17938487231731415,0.17535898089408875,0.21154406666755676,0.39099428057670593,-0.030447717756032944,-0.3109065592288971,-0.27872130274772644,-0.20980431139469147,-0.057675257325172424,-0.07336287945508957,-0.3389981985092163,0.11691872030496597,0.06648015975952148,-0.13689830899238586,-0.15082231163978577,-0.03477846458554268,0.1013636365532875,0.02423730678856373,0.11385323852300644,-0.11205247789621353,-0.13102620840072632,0.03968213126063347,0.3577044606208801,-0.25642287731170654,-0.01844724640250206,-0.013985131867229939,0.0031106690876185894,-0.05905749648809433,-0.11810483038425446,0.06435225903987885,0.11573116481304169,0.2841416597366333,0.11435645818710327,-0.033357877284288406,0.1476903259754181,0.09530334174633026,-0.1387508511543274,0.010502287186682224,-0.1259043663740158,0.11711632460355759,-0.031210849061608315,0.17438045144081116,-0.013342701829969883,-0.15988875925540924,-0.1500706523656845,-0.20836955308914185,-0.025575943291187286,-0.1570126861333847,0.006576478015631437,-0.026678111404180527,0.11604397743940353,-0.1700764149427414,-0.12961134314537048,-0.09378653764724731,-0.23667646944522858,0.05039387196302414,0.11883346736431122,0.27126315236091614,0.17583882808685303,-0.054657287895679474,-0.1626112312078476,-0.13307243585586548,0.1021299958229065,0.298612117767334,-0.2038886547088623,-0.026257874444127083,-0.12271218001842499,0.0733465701341629,0.15375113487243652,0.318191260099411,0.2063329666852951,-0.006503554526716471,-0.28827646374702454,0.254949688911438,0.06256510317325592,-0.3328310549259186,-0.10593170672655106,-0.034074731171131134,-0.2087385654449463,0.015152321197092533,0.006368872243911028,-0.16991011798381805,0.17696721851825714,0.2351577877998352,0.007912367582321167,-0.15932199358940125,-0.2178586721420288,0.31003206968307495,-0.16231010854244232,0.1609722077846527,-0.018508747220039368,-0.1928318589925766,-0.20247367024421692,0.2849484384059906,-0.22304046154022217,0.17472542822360992,0.14631463587284088,0.15407320857048035,-0.09109719097614288,-0.07944086939096451,-0.219941645860672,0.048623379319906235,-0.12151635438203812,0.1067269816994667,-0.3039284646511078,0.12594744563102722,0.08970152586698532,0.23435737192630768,-0.11489341408014297,-0.2664717137813568,0.13801389932632446,-0.2904728651046753,0.1276857554912567,0.21013732254505157,0.21016329526901245,0.17830511927604675,-0.2731276750564575,-0.14870750904083252,-0.3262917101383209,-0.0662626326084137,0.24945279955863953,-0.28383907675743103,-0.25563788414001465,0.005725543014705181,-0.35231828689575195,-0.24541613459587097,-0.033316511660814285,0.11343008279800415,0.16577684879302979,-0.19352446496486664,-0.33450859785079956,-0.21633964776992798,-0.2929517328739166,0.2898786664009094,0.18850834667682648,0.007254873402416706,0.23238874971866608,-0.026006966829299927,0.20660139620304108,0.20457759499549866,-0.12258663773536682,-0.3056347668170929,0.167873352766037,0.051163170486688614,0.020939549431204796,0.1495826095342636,-0.32112184166908264,0.22353890538215637,-0.025601180270314217,-0.056460749357938766,0.24552035331726074,0.12329220771789551,-0.1887638121843338,-0.2659187912940979,0.18563351035118103,-0.07183552533388138,0.20028096437454224,0.12981432676315308,-0.21941077709197998,0.03404701128602028,-0.06913003325462341,-0.017423469573259354,0.01021801121532917,-0.07243424654006958,-0.19425690174102783,-0.22082865238189697,-0.13092629611492157,0.017697691917419434,0.16851924359798431,0.05910622328519821,0.053972404450178146,-0.2185429036617279,-0.2129264920949936,0.3544588088989258,-0.15352721512317657,-0.24385294318199158,0.17931203544139862,0.05836230888962746,-0.07639817893505096,-0.054537333548069,0.2996859848499298,-0.22676654160022736,-0.22585780918598175,0.31393030285835266,0.011610772460699081,-0.22795680165290833,0.1422731578350067,0.09344843775033951,0.151397243142128,-0.23306655883789062,-0.014511614106595516,0.02212386392056942,0.22960156202316284,0.007362072821706533,0.21678517758846283,-0.08941438049077988,-0.1757681965827942,0.11327728629112244,0.16780947148799896,-0.025875888764858246,-0.14572514593601227,-0.020711075514554977,-0.042427562177181244,-0.09940113127231598,-0.19062285125255585,-0.002447987673804164,0.02070520631968975,-0.2041614055633545,0.2862214148044586,-0.24729302525520325,0.18407073616981506,-0.06438480317592621,0.12988786399364471,0.20229648053646088,-0.05718747526407242,0.05636388808488846,0.21812017261981964,0.129803866147995,0.2407943159341812,-0.13947613537311554,-0.13336150348186493,-0.08550170809030533,0.05614316835999489,0.17776605486869812,0.1913401335477829,-0.07810501009225845,-0.06949042528867722,0.2647789716720581,0.020604345947504044,0.170859694480896,-0.21073678135871887,-0.13365507125854492,-0.019383737817406654,-0.24484066665172577,-0.1601480096578598,-0.06930209696292877,-0.17235884070396423,0.13314290344715118,-0.09454570710659027,-0.22560900449752808,-0.304936021566391,0.29843011498451233,-0.16374634206295013,0.2727777361869812,0.26254284381866455,0.09906600415706635,0.2242613434791565,-0.16797806322574615,-0.21822504699230194,0.12679344415664673,0.30927416682243347,-0.2862755358219147,0.15936893224716187,0.29118213057518005,-0.2655089795589447,-0.10460435599088669,0.12188900262117386,-0.20252688229084015,-0.3567519187927246,0.150675967335701,-0.12128141522407532,-0.2213735729455948,0.05866129696369171,0.10331568121910095,0.31844910979270935,-0.21906982362270355,0.21363689005374908,0.2509174346923828,-0.1660897433757782,0.3423292934894562,0.057207196950912476,-0.08805181086063385,-0.07468537241220474,0.19142086803913116,0.22528508305549622,0.12106640636920929,-0.1409514844417572,0.23556998372077942,0.3482441008090973,0.26556047797203064,-0.06275418400764465,-0.24470721185207367,-0.04660329595208168,0.25953108072280884,-0.2495579719543457,0.12101231515407562,-0.07839611917734146,0.037839967757463455,0.2205539494752884,0.27633464336395264,-0.1245683878660202,-0.2940889596939087,0.30288150906562805,0.061939891427755356,0.1909908503293991,0.30122390389442444,0.25386014580726624,-0.23543785512447357,-0.07083836197853088,-0.11942766606807709,0.27911606431007385,-0.03807346522808075,0.16373880207538605,0.22639286518096924,-0.20086582005023956,0.18645894527435303,-0.14422741532325745,-0.11010591685771942,-0.28200867772102356,-0.22135889530181885,-0.09748317301273346,-0.36989331245422363,-0.09570945054292679,-0.21300330758094788,-0.10670150816440582,0.19341398775577545,0.12171462923288345,-0.1570129543542862,0.1459074318408966,0.15779036283493042,-0.015396379865705967,0.046085234731435776,0.15077126026153564,0.23663978278636932,-0.16853074729442596,-0.018919210880994797,-0.3116133511066437,0.24642087519168854,0.08398187905550003,-0.1344626396894455,-0.06713007390499115,0.207911416888237,0.15616737306118011,-0.15581893920898438,0.006961256731301546,0.2106332927942276,-0.35548198223114014,-0.29628559947013855,0.10168889164924622,0.17510391771793365,0.21861691772937775,0.18708935379981995,0.23405523598194122,-0.17571894824504852,-0.009943168610334396,0.17038242518901825,-0.12683601677417755,-0.022037336602807045,0.029753945767879486,0.07607690244913101,-0.10470326989889145,-0.22139191627502441,0.29887646436691284,0.23647919297218323,-0.16792555153369904,0.18634232878684998,-0.16881179809570312,0.2631217837333679,0.05765256658196449,-0.2689540982246399,0.23627659678459167,0.07537844777107239,0.016077497974038124,0.031114649027585983,-0.2908685803413391,-0.16700828075408936,-0.2546476721763611,-0.2338189035654068,0.11538553237915039,-0.10041389614343643,0.0031951682176440954,0.2193378359079361,0.11665081977844238,0.03850824758410454,0.06648789346218109,0.19711504876613617,0.12385012209415436,0.15862929821014404,0.25559455156326294,-0.26926514506340027,-0.08998427540063858,-0.12931008636951447,-0.12222584336996078,-0.08710826933383942,0.3258736729621887,-0.15425914525985718,-0.27380824089050293,0.06384579092264175,-0.3129734396934509,0.05170625448226929,-0.04557335004210472,-0.08281521499156952,0.2061460316181183,0.14843516051769257,-0.0630892738699913,-0.3033891022205353,0.3346603512763977,-0.32323363423347473,-0.022978385910391808,0.07232430577278137,0.3219410479068756,0.1396610587835312,-0.14545029401779175,0.18062016367912292,0.20252522826194763,-0.1268305629491806,-0.008755484595894814,-0.2608242928981781,0.21385930478572845,0.23271970450878143,-0.20864370465278625,-0.2259093075990677,-0.021647095680236816,-0.06459854543209076,0.1072925552725792,-0.0940999835729599,0.10991126298904419,0.3176804780960083,-0.023783670738339424,-0.02707580476999283,-0.2791936993598938,-0.04681647941470146,0.35295477509498596,-0.30843982100486755,0.1938197761774063]},{"shape":[32],"values":[-0.32820335030555725,0.37408509850502014,-0.0685834065079689,-0.010780831798911095,-0.029296591877937317,0.1541990488767624,-0.08756042271852493,0.13906830549240112,0.4383932650089264,0.30744469165802,-0.19423717260360718,0.18297681212425232,-0.35753899812698364,0.26570671796798706,-0.08680853992700577,0.17385108768939972,0.2667115032672882,0.025370929390192032,-0.08941490948200226,-0.18718621134757996,0.15865930914878845,0.46011486649513245,-0.23619486391544342,0.3392120897769928,0.009128840640187263,-0.24833561480045319,-0.016718227416276932,-0.2802165746688843,-0.3557579517364502,0.39297017455101013,-0.010324415750801563,-0.1772882342338562]},{"shape":[32,32],"values":[0.33825764060020447,0.6276324987411499,1.1047974824905396,-0.4963424503803253,0.12023838609457016,-0.8267486095428467,0.12767043709754944,-1.212653398513794,0.9126662611961365,0.8415391445159912,1.0834338665008545,0.7058338522911072,-0.9893162846565247,0.4829046428203583,-1.0141960382461548,0.16788364946842194,0.6356146931648254,1.0408365726470947,0.23865114152431488,1.229690432548523,-0.9279496669769287,1.3250972032546997,0.747772753238678,-0.01093249674886465,1.1563490629196167,-1.0140433311462402,0.5579809546470642,1.1642168760299683,1.020638108253479,-0.6619285345077515,0.954376220703125,0.510357677936554,-0.6234790086746216,-0.3752020001411438,-1.0065795183181763,-0.10189000517129898,0.8261911869049072,0.8924025893211365,-1.0843595266342163,-0.034850191324949265,-0.8804568648338318,0.3590560555458069,-0.7364476323127747,0.06381892412900925,-0.5195269584655762,-0.3496303856372833,-0.09408829361200333,0.400779664516449,-0.460235059261322,0.35214924812316895,-0.6301119923591614,-0.19843657314777374,0.19970501959323883,-0.7670794725418091,-1.2616451978683472,-0.6576301455497742,0.07330313324928284,1.3239303827285767,0.6846973299980164,-0.13018988072872162,-0.2574775218963623,0.04697764292359352,-0.21727563440799713,-0.6874598264694214,-0.15809500217437744,0.16386961936950684,0.03394830599427223,-0.33973586559295654,0.6859847903251648,0.43992725014686584,-0.3769703805446625,-1.0226577520370483,0.009286032989621162,0.1339671015739441,0.1581556648015976,0.5853574275970459,-0.23428885638713837,0.6920596361160278,-0.26551422476768494,0.8015831708908081,-0.13587671518325806,-0.09059738367795944,-0.39273133873939514,1.0676015615463257,-0.32115888595581055,-0.3244577944278717,-0.27112463116645813,-0.23570901155471802,0.14691920578479767,0.2885919511318207,0.6671486496925354,0.7298027276992798,0.759126603603363,-0.5776955485343933,0.2550279200077057,-0.4045826494693756,0.27708420157432556,-0.36287030577659607,-0.005963652860373259,0.1363198459148407,-0.6022935509681702,-0.12029567360877991,0.6614147424697876,1.020593285560608,0.1709303855895996,-0.4829106330871582,-0.1767757087945938,-0.6185674667358398,0.2525758743286133,-0.876868486404419,0.47808191180229187,-0.599977433681488,0.049985237419605255,-0.14970919489860535,0.26285892724990845,-0.6659398078918457,0.8829501867294312,0.2136974185705185,0.4870053827762604,0.1922159492969513,-0.5188245177268982,0.2500329613685608,-0.9173609614372253,-0.6246325373649597,-0.07119028270244598,0.28811097145080566,-0.12345172464847565,0.07838699221611023,-0.10404328256845474,-0.052637629210948944,-0.1843923181295395,-0.40840935707092285,0.6198450326919556,-0.2018042504787445,-0.15547943115234375,-0.400675505399704,-0.01852801814675331,0.17123721539974213,0.3116300404071808,0.5656038522720337,-0.09123308211565018,0.32633692026138306,-0.4370247423648834,0.23043449223041534,0.018544664606451988,0.21173979341983795,0.12383551150560379,0.6645053625106812,-0.19556161761283875,-0.24775972962379456,-0.20069094002246857,-0.24524956941604614,0.25817179679870605,0.034904684871435165,0.648913562297821,0.21831481158733368,0.5107675790786743,-0.453000009059906,-0.19275452196598053,0.1811143159866333,-0.16354918479919434,0.16113369166851044,-0.029767299070954323,0.6322026252746582,-0.6394085884094238,-0.23110488057136536,0.28027287125587463,0.7242629528045654,0.0883384644985199,0.4498940408229828,0.13651920855045319,-0.9127207398414612,-0.2795158922672272,-0.6181976199150085,-0.2164575159549713,-0.49857646226882935,0.478290319442749,0.12740087509155273,0.08766115456819534,-0.3656131625175476,0.42030343413352966,0.11403094977140427,0.40881600975990295,-0.19045758247375488,-0.06137601658701897,-0.6242613792419434,-1.003369927406311,-0.22872944176197052,-0.16814731061458588,0.44524630904197693,0.13208527863025665,0.05758564919233322,-0.4369611144065857,0.4186098277568817,0.18881931900978088,-0.31141984462738037,0.8838779330253601,0.2986043691635132,-0.335144966840744,-0.9905152916908264,-0.40008431673049927,0.24080277979373932,-0.12643174827098846,0.9164106249809265,-0.5867212414741516,0.02643107995390892,-0.24918705224990845,0.7183854579925537,0.18601848185062408,0.24919074773788452,-0.5623005032539368,0.7791758179664612,-0.5403348803520203,0.012448693625628948,-0.4314817786216736,-0.3183552324771881,0.30043983459472656,-0.1627698391675949,0.701115608215332,0.7810440063476562,0.4977714419364929,-0.5729568004608154,0.15783563256263733,-0.2584192454814911,-0.21445617079734802,-0.30104002356529236,-0.7085037231445312,0.7111759185791016,-1.0034717321395874,0.31597548723220825,0.5114057660102844,1.1410144567489624,0.12403113394975662,-0.5817460417747498,-0.8991144895553589,-0.49533262848854065,0.388945609331131,-1.0793458223342896,0.4202127456665039,-0.5005974173545837,-0.1264878809452057,-0.6079897284507751,-0.0824420154094696,-1.2579764127731323,1.0097360610961914,-0.4971940219402313,-0.12561927735805511,0.1341545432806015,-0.5999370813369751,0.5185337662696838,-1.119789481163025,-0.9776846170425415,-0.5710399746894836,0.832218587398529,-0.24724765121936798,-0.1595885008573532,-0.6501790881156921,-0.2230602204799652,-0.09924494475126266,0.1528560072183609,0.6265171766281128,0.6341236233711243,-0.5220934748649597,-0.5390405058860779,-0.8822404146194458,0.6688037514686584,-0.21384422481060028,0.44509437680244446,-0.19326449930667877,0.3889389932155609,-0.5797405242919922,0.8635247945785522,-0.3775186836719513,0.5138518810272217,-0.9437658190727234,0.2880333662033081,-0.34641897678375244,-0.9567691087722778,-0.9206619262695312,-0.26943665742874146,0.13664983212947845,0.7979230880737305,0.5603443384170532,0.1653452068567276,0.25914865732192993,0.24193549156188965,0.5438007712364197,-1.043945074081421,-0.5859411954879761,0.002980564720928669,-0.1870919167995453,-0.3896917998790741,0.4374654293060303,0.3064301311969757,-0.41041722893714905,-0.3631606698036194,-0.20676611363887787,0.5734339952468872,0.2778361141681671,0.35257333517074585,-0.795945942401886,-0.2473505735397339,-0.7253900170326233,0.23857396841049194,-0.1606086939573288,0.7341535687446594,-0.5950437188148499,0.07291289418935776,-0.5251189470291138,-0.30530303716659546,-0.3569265305995941,-0.40277761220932007,0.823820948600769,0.04662686213850975,0.6841146349906921,0.2876800000667572,0.08789864182472229,0.08430398255586624,1.056201457977295,-0.74627286195755,0.20132915675640106,-0.08136242628097534,0.11847992241382599,0.058519236743450165,-0.3013833463191986,0.0591001957654953,0.12922462821006775,-0.004464633297175169,0.18494468927383423,-0.008110548369586468,-0.04207400232553482,-0.23395173251628876,0.2656692862510681,-0.15475231409072876,-0.057731274515390396,-0.25641223788261414,0.1605406105518341,-0.26275548338890076,-0.26623812317848206,-0.21638622879981995,0.20488499104976654,0.1213638037443161,0.1828187257051468,0.2505998909473419,-0.43010178208351135,0.23061439394950867,0.09198546409606934,0.04218219220638275,-0.09554935991764069,-0.041362181305885315,0.10718680173158646,-0.23909883201122284,-0.37363526225090027,-0.06280509382486343,-0.2547750174999237,0.13276778161525726,0.8393257856369019,0.5479201078414917,-0.7586346864700317,-0.45484811067581177,-0.6600043177604675,0.1550302356481552,0.09629262238740921,0.2669832110404968,-0.5217658281326294,0.1800466626882553,-0.29261964559555054,0.7310566306114197,-0.2399996966123581,0.18230776488780975,-0.7850818037986755,0.5839490294456482,-0.15854965150356293,-0.30203166604042053,-0.5864615440368652,-0.13610948622226715,-0.09369014948606491,0.4155825972557068,0.5867131948471069,0.4133780300617218,0.006394561380147934,-0.14707595109939575,0.24546360969543457,-0.7631385922431946,0.4829764664173126,0.372673362493515,0.40137583017349243,-0.19532284140586853,0.389812707901001,-0.4626968801021576,0.27915602922439575,-0.7576374411582947,0.3588373363018036,0.6480669379234314,0.7058066725730896,0.4582706391811371,-0.14614884555339813,0.5819727778434753,-0.5205362439155579,-0.057949502021074295,0.49602097272872925,0.09429042786359787,0.1471158117055893,0.48893651366233826,-0.6579940915107727,0.3227992355823517,0.16869696974754333,-0.0876842737197876,0.41084766387939453,-0.5988799929618835,0.53316330909729,0.5659322738647461,0.6113670468330383,-0.27904415130615234,0.22440937161445618,0.030134987086057663,0.043963875621557236,-0.2108389288187027,0.3723528981208801,0.14907261729240417,0.20868362486362457,0.16767917573451996,-0.2630040645599365,-0.3009225130081177,-0.17588500678539276,0.33409398794174194,-0.11437255144119263,0.25674358010292053,-0.3925824463367462,-0.2536507546901703,-0.7254823446273804,0.3229128420352936,-0.05993589013814926,0.7002349495887756,-0.2846335172653198,0.07677455246448517,-0.2532038986682892,-0.05330630764365196,0.14227814972400665,-0.21288245916366577,0.8603328466415405,-0.08836151659488678,0.057149823755025864,-0.0954490602016449,0.054395586252212524,-0.3004574179649353,0.7375797629356384,-0.2164338231086731,-0.5012778639793396,0.17679160833358765,0.17376987636089325,-0.3136827349662781,0.5212562680244446,-0.28228217363357544,0.022485489025712013,-0.5632725358009338,-0.24173621833324432,-0.013745573349297047,0.16508661210536957,0.223260298371315,-0.29836827516555786,0.2710486054420471,0.04809752479195595,0.4728100299835205,-0.2492092400789261,0.1339922994375229,-0.36211854219436646,0.23767726123332977,-0.009575363248586655,-0.11998867988586426,-0.20419995486736298,-0.2215450555086136,0.34409773349761963,-0.033626530319452286,0.40767866373062134,0.363372802734375,-0.29340416193008423,0.26187485456466675,0.3522171378135681,-0.46678128838539124,-0.16559922695159912,-0.08320165425539017,0.016409294679760933,-0.1513391137123108,0.5193842649459839,-0.16525088250637054,-0.2822954058647156,-0.3406064212322235,0.16335606575012207,0.7022055387496948,-0.046649087220430374,0.23689426481723785,-0.5904327630996704,0.3700309097766876,-0.5719208717346191,-0.06908966600894928,-0.3153610825538635,0.6287548542022705,-0.5792304873466492,0.6836286783218384,-0.39153701066970825,0.15794534981250763,-0.13000304996967316,-0.24040794372558594,1.1369701623916626,0.08919180184602737,0.5380519032478333,0.10275682806968689,0.2755782902240753,-0.1039588674902916,0.5257837176322937,-0.4593542218208313,0.14208735525608063,-0.3568814694881439,-0.1888452172279358,0.2982889711856842,-0.42512834072113037,0.2710545063018799,-0.19504275918006897,0.48027607798576355,-0.3337494730949402,-0.3538834750652313,-0.16215012967586517,-0.6364311575889587,0.49200716614723206,0.09429292380809784,0.3203446567058563,-0.3716316819190979,-0.47734534740448,-0.17566965520381927,0.20519861578941345,-0.7818877696990967,0.47651731967926025,-0.5275103449821472,0.36346206068992615,0.4574122726917267,-0.31876838207244873,0.38223716616630554,-0.6075435876846313,-0.6528142690658569,-0.1549251824617386,0.2539849281311035,-0.495713472366333,-0.07120169699192047,-0.03002138063311577,0.08753877878189087,0.06592868268489838,-0.39456912875175476,0.2380574494600296,0.024495067074894905,0.09801916033029556,-0.3835303783416748,0.08583065867424011,0.830520510673523,0.34467339515686035,0.10065275430679321,-0.9347924590110779,-0.026775971055030823,-0.45372310280799866,0.22117562592029572,0.20320217311382294,0.8726258277893066,0.08346109092235565,0.4666069746017456,-0.4415946304798126,0.16126057505607605,-0.2484593689441681,-0.07188572734594345,1.0850597620010376,0.04017192870378494,0.5159265398979187,0.49970293045043945,-0.24656444787979126,-0.4516504406929016,0.8444395065307617,-0.32612669467926025,0.3473828136920929,0.5741242170333862,0.3395111560821533,-0.7962902188301086,0.16605040431022644,-0.30930039286613464,-0.1453077346086502,-0.869706928730011,0.2808423638343811,0.740100622177124,0.5932435393333435,0.5064225792884827,-0.6810962557792664,0.2126004844903946,-0.5844125151634216,0.015037458389997482,0.6950482130050659,0.8145187497138977,-0.4138832092285156,0.6922405958175659,-0.5133193731307983,0.792561411857605,0.2889710068702698,-0.24590188264846802,0.9590895771980286,-0.8730087876319885,0.3829779326915741,0.8030955791473389,1.0455735921859741,-0.40956026315689087,0.9170582890510559,-0.12169060111045837,0.7522369027137756,0.12012815475463867,-0.4166835844516754,-0.1639910191297531,-0.6032678484916687,-0.3042012155056,0.7316679358482361,0.786579430103302,0.7311678528785706,-0.4426046311855316,0.08804482221603394,-0.6619581580162048,0.6227598190307617,-0.6867494583129883,0.7161803245544434,-0.4481147527694702,0.9319836497306824,-0.8229205012321472,1.1867730617523193,-0.3963584899902344,0.7411688566207886,0.37076014280319214,0.35972946882247925,-0.17005296051502228,-0.7737396955490112,-0.5257849097251892,-0.919917643070221,-0.4741712212562561,-0.07167403399944305,0.20167316496372223,-0.697475016117096,0.5748578906059265,-0.5001387596130371,-0.24858777225017548,-0.8936753273010254,-0.047175366431474686,0.31561464071273804,0.44596773386001587,-0.16100068390369415,0.6772894263267517,-0.3799710273742676,-0.2548303008079529,-0.5028011798858643,0.08257848769426346,0.4081827998161316,-0.36637648940086365,0.874403715133667,0.3864026367664337,-0.1638525277376175,-0.6015990376472473,0.2446262538433075,-0.3852441906929016,0.5086137652397156,-0.6129273176193237,-0.44634056091308594,-0.042533691972494125,-0.7132610082626343,1.002056360244751,-0.051477156579494476,-0.4587869942188263,-0.5429319143295288,0.33138221502304077,-0.18013888597488403,-0.20788231492042542,-1.2879669666290283,-1.389482855796814,-1.0701191425323486,0.3087430000305176,0.8656697869300842,1.5659911632537842,-1.2451313734054565,0.060956504195928574,-1.0850237607955933,0.028863675892353058,-1.2861874103546143,0.5076937675476074,-0.16357825696468353,-0.5559298396110535,0.6249522566795349,1.1712510585784912,-1.1715664863586426,-0.4969739317893982,-0.7534463405609131,-0.6485655903816223,0.35536816716194153,-1.6295472383499146,-1.21213960647583,-0.6422994136810303,-0.391597181558609,1.5899932384490967,1.1069698333740234,-0.6773769855499268,-0.7231543660163879,0.2769222855567932,0.12888169288635254,-1.602088212966919,0.04322454705834389,0.2879562973976135,0.044845689088106155,-0.41547444462776184,0.8631069660186768,0.27898985147476196,-0.40277233719825745,-1.0750471353530884,-0.07822275906801224,0.08153615891933441,0.4324880838394165,0.7015857100486755,-0.05852667614817619,0.5781586766242981,-0.5726019740104675,0.5729089379310608,0.048105474561452866,0.12317517399787903,0.027866993099451065,1.1692571640014648,-0.38726916909217834,-0.035453107208013535,-0.09122102707624435,-0.13405105471611023,0.049990877509117126,-0.29507890343666077,0.5697159171104431,0.8430906534194946,0.011281520128250122,-0.6813789010047913,0.5016515254974365,0.022522035986185074,-0.35551717877388,-0.14483028650283813,0.05131727457046509,0.13908977806568146,-0.05530989170074463,-0.06212807819247246,0.02899078279733658,-0.278805136680603,-0.3561747968196869,0.856881856918335,0.22725090384483337,0.19397705793380737,-0.8785957098007202,-0.3835388720035553,-0.9330050945281982,-0.11590990424156189,-0.3700248599052429,0.5467833876609802,-0.3805137574672699,0.29532358050346375,-0.2560008466243744,-0.23414522409439087,-0.5804659724235535,-0.4093030095100403,0.8812131285667419,0.4197862148284912,0.3785930573940277,0.007368258200585842,-0.2672499716281891,0.074113629758358,0.48567792773246765,-0.20182588696479797,0.07762086391448975,-0.1307828724384308,0.33135467767715454,-0.21008333563804626,-0.38042089343070984,0.18489329516887665,0.1658702939748764,0.3210618197917938,0.37549638748168945,0.5763756632804871,-0.12210021167993546,0.07149890065193176,-0.1979520320892334,-0.641939640045166,-0.5218228101730347,-0.20126865804195404,0.6308029294013977,0.34568676352500916,-0.0034792283549904823,0.0853939950466156,-0.043721944093704224,-0.0563993901014328,-0.09683097153902054,-0.3237922489643097,0.533332109451294,-0.010476367548108101,-0.13114091753959656,0.17186425626277924,-0.15013383328914642,-0.1661204993724823,0.034039679914712906,0.2575013041496277,0.3581390380859375,0.5830625891685486,0.3601848781108856,-0.3133842945098877,-0.24188126623630524,-0.308633953332901,0.5450484156608582,-0.0013878114987164736,0.722481369972229,0.2806627154350281,0.47368156909942627,0.09253699332475662,-0.26289209723472595,-0.6783854365348816,-0.35498297214508057,-0.4777277112007141,1.1469426155090332,0.045976657420396805,0.4761640429496765,-0.22453434765338898,0.13614314794540405,0.657192051410675,0.3708422780036926,-0.18595688045024872,0.12107579410076141,-0.269692063331604,-0.009576334618031979,0.13385449349880219,-0.14437957108020782,-0.37572234869003296,0.3572099208831787,0.4544193148612976,-0.3063005805015564,0.3305903971195221,-0.2657325267791748,-0.14381293952465057,0.43162801861763,0.16661415994167328,0.1444234549999237,-0.4236661493778229,-0.3547663688659668,0.23732545971870422,-0.029660381376743317,0.15032856166362762,-0.04179779440164566,-0.015664061531424522,0.148323655128479,0.34606096148490906,-0.07346180826425552,0.15479348599910736,0.3103862702846527,0.41201895475387573,-0.1622091382741928,0.11119461059570312,-0.3370784819126129,-0.027345139533281326,0.24403449892997742,0.20681877434253693,0.4296570122241974,0.1810920089483261,-0.333166241645813,-0.5543591976165771,0.16116908192634583,-0.18093934655189514,0.5815582871437073,0.2827908396720886,0.5919273495674133,-0.2292412966489792,-0.06133616343140602,-0.8284524083137512,0.3258502185344696,-0.7121326923370361,0.7027473449707031,0.29023948311805725,0.6143958568572998,-0.053488388657569885,-0.22234763205051422,0.15358370542526245,-0.27903470396995544,-0.07252313196659088,0.6946385502815247,0.67659592628479,0.2221524715423584,0.8220652341842651,-0.5335172414779663,0.46238982677459717,0.8947893381118774,-0.03435034304857254,0.2201271653175354,-0.8202390670776367,-0.005443465895950794,0.7128568291664124,0.5922785401344299,-0.4550448954105377,0.36550018191337585,0.5879114866256714,0.3729562759399414,-0.19490380585193634,0.39975523948669434,0.31437477469444275,-0.17073725163936615,-0.26100897789001465,0.4814019501209259,0.19654543697834015,0.46218523383140564,-0.6046864986419678,-0.06330151110887527,-0.08419102430343628,0.6847127676010132,0.17257679998874664,0.08549623191356659,0.01751169003546238,0.3529232144355774,-0.1353750079870224,0.12876303493976593,-0.08722886443138123,-0.24222412705421448,0.35132166743278503,0.47475287318229675,-0.004706291947513819,-0.7093441486358643,-0.2530042827129364,-0.48396220803260803,-0.24668769538402557,0.11958841979503632,0.005888515152037144,-0.36597099900245667,0.2588071823120117,-0.14185726642608643,-0.6360609531402588,-0.31174835562705994,0.5226247310638428,0.020858677104115486,0.4791502356529236,-0.2533731460571289,0.7321353554725647,-0.4797267019748688,0.2384154498577118,-0.5346530079841614,-0.8999509811401367,-0.13328027725219727,-0.06578175723552704,0.104076087474823,-0.2475643754005432,-0.16654494404792786,0.012932782992720604,-0.42696496844291687,-0.8409150242805481,0.21422936022281647,-0.474193811416626,-0.17697305977344513,-0.0622556135058403,0.00563416862860322,0.09912273287773132,-0.7829045653343201,-0.5115686655044556,-0.46987390518188477,0.8369836807250977,0.20894721150398254,0.04365206137299538,-0.18617106974124908,-0.10326758772134781,0.1846165657043457,-0.07515078783035278,0.6260952353477478,0.17584653198719025,-0.18352670967578888,-0.45656174421310425,0.04265017807483673,0.5624117851257324,0.17661277949810028,0.6920033097267151,-0.032603684812784195,0.734475314617157,-0.5061591863632202,0.3784354031085968,-0.023022610694169998,0.09222922474145889,-0.10339667648077011,0.42933592200279236,-0.5672450661659241,-0.20936641097068787,-0.09657318145036697,-0.04891379177570343,-0.19502845406532288,0.21691328287124634,0.6044613122940063,0.5276668667793274,0.07297028601169586,0.056126918643713,0.2990958094596863,0.055436912924051285,-0.1245431900024414,0.005779851693660021,0.545741617679596,0.07843685150146484,0.3098468780517578,-0.46176210045814514,-0.28847023844718933,-0.5063391923904419,-0.14508429169654846,0.2656822204589844,0.037307046353816986,0.34037819504737854,-0.272005558013916,0.4687859117984772,-0.48918336629867554,0.012578180991113186,-0.41996312141418457,0.7709992527961731,-0.10779586434364319,0.6194127202033997,-0.7162685990333557,0.019052740186452866,-0.0013828977243974805,0.11553534120321274,0.33677926659584045,-0.27919718623161316,0.5280117988586426,0.22070913016796112,0.405864417552948,-0.11999263614416122,0.6013123393058777,-0.19856879115104675]},{"shape":[32],"values":[-0.3646189868450165,-0.23275355994701385,-0.19696438312530518,0.1381278783082962,0.11306975781917572,0.2664530277252197,-0.3307056725025177,0.23141387104988098,-0.3435715138912201,0.2889757454395294,-0.20832623541355133,-0.07333629578351974,-0.28237977623939514,-0.3846283555030823,-0.1951676458120346,0.011720940470695496,-0.11631827056407928,0.33236420154571533,-0.32432904839515686,-0.2739996612071991,0.22463595867156982,-0.23593688011169434,-0.2206803411245346,-0.2755385637283325,0.3112800121307373,0.368943452835083,0.029730889946222305,-0.31140416860580444,-0.3048286437988281,0.1533682644367218,0.28025662899017334,-0.361973375082016]},{"shape":[32,9],"values":[1.1321980953216553,1.5305442810058594,0.00705848028883338,0.4253564178943634,0.020772697404026985,-0.35703951120376587,-0.7297413349151611,-0.7700013518333435,-0.1780872941017151,0.38466838002204895,0.13952381908893585,0.9773430824279785,-0.12211254239082336,-0.03728374466300011,0.25532981753349304,0.08446710556745529,-0.376437246799469,-0.0977451428771019,-0.891140878200531,0.8879643082618713,0.4963837265968323,-0.14070963859558105,0.609622597694397,0.6351622343063354,-1.0422608852386475,-0.803249180316925,-0.1704234778881073,-0.07351627945899963,-0.24364469945430756,-0.5208213925361633,0.23616774380207062,0.6558461785316467,-0.736663281917572,0.35698461532592773,0.21979248523712158,-0.2565717101097107,-0.12382343411445618,-0.09651263058185577,0.1639566272497177,0.03833044692873955,-0.7643622756004333,0.16024374961853027,-0.33901992440223694,0.21005314588546753,1.4303724765777588,-0.08131178468465805,-1.249726414680481,-1.7699711322784424,-0.02271158993244171,-0.4258996248245239,0.2943311035633087,0.7181857228279114,0.798596203327179,1.2968815565109253,1.024699091911316,1.3750404119491577,-0.7895866632461548,0.9352514743804932,0.8271182179450989,0.31160053610801697,0.3265221416950226,-0.8586380481719971,-0.6434104442596436,0.10103225708007812,0.23095133900642395,-1.2133708000183105,0.14105835556983948,0.4722810387611389,-0.7665614485740662,0.34552496671676636,0.8018099665641785,-1.2628132104873657,1.2796521186828613,1.0038063526153564,0.22910085320472717,-0.2925153374671936,0.5596305727958679,0.20327581465244293,-1.2966746091842651,-0.7524474263191223,0.008497117087244987,-0.6800575852394104,0.14770027995109558,-0.3414963185787201,-0.8700482249259949,0.3479202389717102,0.17688344419002533,-0.8624973297119141,0.5627171993255615,0.4593718647956848,0.03815052658319473,0.5427672863006592,0.6593136191368103,-0.4736649990081787,-0.43022677302360535,1.1167088747024536,-0.5724502801895142,-0.88407301902771,-0.027413664385676384,-0.1461361050605774,-0.6562718152999878,0.7623012661933899,-0.017721854150295258,-0.8105885982513428,0.4297638237476349,-0.35656967759132385,-0.11267925798892975,0.6732327342033386,0.8922899961471558,-0.27407026290893555,-0.3384648561477661,0.9034131169319153,-0.5692970752716064,0.02526499517261982,0.6740309596061707,-0.583782434463501,-0.41061222553253174,0.12377256900072098,0.012479471042752266,1.5394620895385742,0.10903111845254898,-0.5586641430854797,0.34090912342071533,-0.3567485809326172,-0.36228108406066895,-0.26217806339263916,1.0747296810150146,-0.3618828356266022,0.1935056895017624,0.5376772284507751,-0.4242281913757324,-0.3851305842399597,1.3899078369140625,-0.06825977563858032,-0.34381169080734253,-0.06659262627363205,-0.10713746398687363,0.08022813498973846,-0.15790779888629913,-0.4049127995967865,-0.27644357085227966,-0.062378279864788055,-0.01638997159898281,1.152293086051941,0.8117357492446899,0.5523461103439331,1.1653854846954346,-0.49264106154441833,0.40586724877357483,-0.2250441610813141,-1.1351288557052612,-0.06912107765674591,-0.43972715735435486,-1.3740078210830688,0.32936525344848633,0.32026517391204834,-0.36503341794013977,0.2654803693294525,0.05300482362508774,-1.2205146551132202,0.28339892625808716,-0.14030559360980988,1.5211979150772095,1.0401501655578613,-0.1723300665616989,0.4978732764720917,-0.2150605469942093,-0.2495751976966858,0.062148381024599075,-0.5460108518600464,-0.5737151503562927,0.18084339797496796,-0.36945849657058716,1.2182010412216187,-0.35269296169281006,-0.43995603919029236,1.1427584886550903,-0.3340095281600952,-0.9317317605018616,1.1025032997131348,0.17358610033988953,0.39835265278816223,-0.5852848887443542,0.4166902005672455,-0.26224565505981445,-0.8352535963058472,0.6453092694282532,0.5090124607086182,-0.8868688941001892,0.5484223365783691,1.0087708234786987,1.9078339338302612,0.13730724155902863,0.6712769865989685,0.26014211773872375,-0.4965526759624481,-1.0495949983596802,-0.8400959968566895,0.36880552768707275,1.3530148267745972,0.45836687088012695,0.9652653336524963,0.1349659115076065,-0.41571637988090515,-0.7699537873268127,-0.5899187326431274,-0.6099827289581299,0.5967766642570496,0.361551433801651,0.2941984534263611,0.3787182867527008,0.005894796922802925,-0.36690905690193176,0.03057854063808918,-0.4406006932258606,-0.11492414772510529,-1.0535742044448853,0.6520701050758362,-0.16148199141025543,-1.273667812347412,0.42141544818878174,0.2890690863132477,-1.1317218542099,0.1125190332531929,0.2683117389678955,-0.21217608451843262,-1.3907581567764282,-1.6789933443069458,0.00883233267813921,0.053182490170001984,-0.8670696020126343,0.45743173360824585,0.7583332061767578,0.9545930027961731,-0.10150273144245148,-1.0637463331222534,0.4945316016674042,-0.6756633520126343,-0.4747207462787628,0.7210953831672668,-0.2173253893852234,0.08810830116271973,1.1488317251205444,-0.42284008860588074,-0.08533155173063278,0.5460558533668518,-0.7236572504043579,-0.39750248193740845,0.8549607992172241,-0.31735140085220337,-0.8152173757553101,0.5119004249572754,0.18095360696315765,0.30370083451271057,1.4374854564666748,-0.06927333027124405,-0.2912597358226776,0.35470137000083923,-0.4833981394767761,-0.6155627965927124,0.061202045530080795,0.4443051815032959,0.27163439989089966,-0.6250055432319641,0.38953566551208496,0.24568066000938416,-0.8102352023124695,0.08893966674804688,0.5322820544242859,-0.20770573616027832,-0.7596089243888855,0.15527260303497314,-0.019775329157710075,-0.5258130431175232,0.5054163336753845,0.43537265062332153,-0.7761201858520508,0.2768068015575409,0.44661059975624084,1.049540638923645,1.4143073558807373,0.02595270238816738,0.8108112812042236,0.11295762658119202,-0.39667823910713196,-0.3543289601802826,-0.6461988091468811,-0.4098600149154663]},{"shape":[9],"values":[-0.17186439037322998,-0.21999117732048035,-0.14696526527404785,-0.02076788805425167,0.054328180849552155,-0.09745687246322632,0.049714505672454834,0.16717027127742767,0.1457354575395584]}],"configuration":{"hiddenLayers":[32,32],"learningRate":0.003,"batchSize":128,"epochsPerRound":3,"samplesPerRound":4096}} \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/drivers/report-11.json b/packages/demo-car-circuit/src/public/drivers/report-11.json new file mode 100644 index 0000000..c2c36f5 --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/report-11.json @@ -0,0 +1,186 @@ +{ + "protocol": { + "id": "racing-learning-v1", + "driving": "circuit-racing-v1", + "observation": "racing-observation-v1", + "race": "circuit-race-v1", + "trainingSeeds": [ + 11, + 29, + 47 + ], + "checkpointSelection": "last scheduled update on training circuit only", + "evaluationSeeds": [ + 101, + 307, + 509 + ], + "success": { + "completedLaps": 3, + "maxRescues": 0, + "maxPenalties": 10, + "minimumCompletionRate": 0.8 + }, + "evaluation": "All three training seeds on both circuits, solo and three frozen rule-based traffic drivers. Report every run, including failures." + }, + "rounds": 16, + "untrained": { + "seed": 101, + "test": false, + "traffic": false, + "completed": false, + "success": false, + "laps": 0, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + "reports": [ + { + "seed": 101, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 81.3, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 307, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 81.28333333333333, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 509, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 81.26666666666667, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 101, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 180.5, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7039 + }, + { + "seed": 307, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 143.9, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 5016 + }, + { + "seed": 509, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 210.95, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 8493 + }, + { + "seed": 101, + "test": true, + "traffic": false, + "completed": false, + "success": false, + "laps": 0, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 307, + "test": true, + "traffic": false, + "completed": false, + "success": false, + "laps": 0, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 509, + "test": true, + "traffic": false, + "completed": false, + "success": false, + "laps": 0, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 101, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 209.4, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7883 + }, + { + "seed": 307, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 216.85, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 8379 + }, + { + "seed": 509, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 172, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 5980 + } + ], + "seconds": 32.18 +} \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/drivers/report-29.json b/packages/demo-car-circuit/src/public/drivers/report-29.json new file mode 100644 index 0000000..100992b --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/report-29.json @@ -0,0 +1,186 @@ +{ + "protocol": { + "id": "racing-learning-v1", + "driving": "circuit-racing-v1", + "observation": "racing-observation-v1", + "race": "circuit-race-v1", + "trainingSeeds": [ + 11, + 29, + 47 + ], + "checkpointSelection": "last scheduled update on training circuit only", + "evaluationSeeds": [ + 101, + 307, + 509 + ], + "success": { + "completedLaps": 3, + "maxRescues": 0, + "maxPenalties": 10, + "minimumCompletionRate": 0.8 + }, + "evaluation": "All three training seeds on both circuits, solo and three frozen rule-based traffic drivers. Report every run, including failures." + }, + "rounds": 16, + "untrained": { + "seed": 101, + "test": false, + "traffic": false, + "completed": false, + "success": false, + "laps": 0, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + "reports": [ + { + "seed": 101, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 76.3, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 307, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 76.38333333333333, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 509, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 76.31666666666666, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 101, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 280.1, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 12380 + }, + { + "seed": 307, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 205.21666666666667, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7884 + }, + { + "seed": 509, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 191.88333333333333, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7466 + }, + { + "seed": 101, + "test": true, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 95.5, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 307, + "test": true, + "traffic": false, + "completed": false, + "success": false, + "laps": 1, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 509, + "test": true, + "traffic": false, + "completed": false, + "success": false, + "laps": 1, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 101, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 181.03333333333333, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 5814 + }, + { + "seed": 307, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 185.18333333333334, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 6554 + }, + { + "seed": 509, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 196.35, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7100 + } + ], + "seconds": 32.045 +} \ No newline at end of file diff --git a/packages/demo-car-circuit/src/public/drivers/report-47.json b/packages/demo-car-circuit/src/public/drivers/report-47.json new file mode 100644 index 0000000..a90ce7a --- /dev/null +++ b/packages/demo-car-circuit/src/public/drivers/report-47.json @@ -0,0 +1,186 @@ +{ + "protocol": { + "id": "racing-learning-v1", + "driving": "circuit-racing-v1", + "observation": "racing-observation-v1", + "race": "circuit-race-v1", + "trainingSeeds": [ + 11, + 29, + 47 + ], + "checkpointSelection": "last scheduled update on training circuit only", + "evaluationSeeds": [ + 101, + 307, + 509 + ], + "success": { + "completedLaps": 3, + "maxRescues": 0, + "maxPenalties": 10, + "minimumCompletionRate": 0.8 + }, + "evaluation": "All three training seeds on both circuits, solo and three frozen rule-based traffic drivers. Report every run, including failures." + }, + "rounds": 16, + "untrained": { + "seed": 101, + "test": false, + "traffic": false, + "completed": false, + "success": false, + "laps": 0, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + "reports": [ + { + "seed": 101, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 70.86666666666666, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 307, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 70.91666666666667, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 509, + "test": false, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 70.93333333333334, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 101, + "test": false, + "traffic": true, + "completed": false, + "success": false, + "laps": 2, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 13805 + }, + { + "seed": 307, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 201.51666666666665, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7306 + }, + { + "seed": 509, + "test": false, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 142.38333333333333, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 4281 + }, + { + "seed": 101, + "test": true, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 81.46666666666667, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 307, + "test": true, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 81.45, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 509, + "test": true, + "traffic": false, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 81.48333333333333, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 0 + }, + { + "seed": 101, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 200.2, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 7507 + }, + { + "seed": 307, + "test": true, + "traffic": true, + "completed": true, + "success": true, + "laps": 3, + "finishSeconds": 186.48333333333332, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 6796 + }, + { + "seed": 509, + "test": true, + "traffic": true, + "completed": false, + "success": false, + "laps": 1, + "finishSeconds": null, + "penaltySeconds": 0, + "rescues": 0, + "contacts": 16304 + } + ], + "seconds": 29.652 +} \ No newline at end of file diff --git a/packages/demo-car-circuit/src/racing/RacingApp.tsx b/packages/demo-car-circuit/src/racing/RacingApp.tsx index 9e34e50..e7b192d 100644 --- a/packages/demo-car-circuit/src/racing/RacingApp.tsx +++ b/packages/demo-car-circuit/src/racing/RacingApp.tsx @@ -1,10 +1,20 @@ -import { Component, useEffect, useRef, useState, type ReactNode } from "react"; +import { + lazy, + Suspense, + Component, + useEffect, + useRef, + useState, + type ReactNode, +} from "react"; import { useProgress } from "@react-three/drei"; import { RaceWorld } from "./race"; import { DrivingWorld } from "./driving"; import { RacingScene } from "./RacingScene"; import "./racing.css"; +const TrainingPanel = lazy(() => import("./TrainingPanel")); + const copy = { fr: { garage: "Garage", @@ -109,8 +119,10 @@ export default function RacingApp() { const [world, setWorld] = useState(() => new DrivingWorld()); const [race, setRace] = useState(); const [reference, setReference] = useState(false); + const [trainingMode, setTrainingMode] = useState(false); const [, updateHUD] = useState(0); const resetSession = (competitive: boolean, references = false) => { + setTrainingMode(false); const next = competitive ? new RaceWorld({ count: references ? 4 : 1 }) : undefined; @@ -181,12 +193,21 @@ export default function RacingApp() {
+

{t.equal}

- ))} - -

{t.equal}

- - - {race && ( - + + {race && ( + + )} +
+
{t.controls}
+

+ ↑ / Z / W + {t.gas} +

+

+ ↓ / S + {t.brake} +

+

+ ← → + {t.steer} +

+

+ ESC + {t.pause} +

+
+
+ + {t.local} +
+ )} -
-
{t.controls}
-

- ↑ / Z / W - {t.gas} -

-

- ↓ / S - {t.brake} -

-

- ← → - {t.steer} -

-

- ESC - {t.pause} -

-
-
- - {t.local} -
@@ -325,7 +354,9 @@ export default function RacingApp() { {race ? `${reference ? (lang === "fr" ? "RÉFÉRENCES À RÈGLES" : "RULE-BASED REFERENCES") : t.race} · ${Math.min(3, race.drivers[0].laps + 1)}/3` - : t.mode} + : trainingMode + ? t.training + : t.mode} {race && started && !paused && race.countdown > 0 && (
@@ -379,7 +410,13 @@ export default function RacingApp() {
)} -
{t.hint}
+
+ {trainingMode + ? lang === "fr" + ? "Aperçu des trajectoires collectées · apprentissage solo puis trafic" + : "Collected trajectories · solo then traffic learning" + : t.hint} +
{t.speed} {Math.round(speed).toString().padStart(3, "0")} diff --git a/packages/demo-car-circuit/src/racing/TrainingPanel.tsx b/packages/demo-car-circuit/src/racing/TrainingPanel.tsx new file mode 100644 index 0000000..2f40585 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/TrainingPanel.tsx @@ -0,0 +1,313 @@ +import * as tf from "@tensorflow/tfjs"; +import { useEffect, useRef, useState } from "react"; +import { LearnedDriver } from "./learned-driver"; +import { + trainDriver, + evaluateDriver, + listDrivers, + saveDriver, + type SavedDriver, + type DriverEvaluation, +} from "./training"; +import { DrivingWorld } from "./driving"; + +const backendReady = tf.setBackend("cpu"); + +export default function TrainingPanel({ + lang, + onWorld, +}: { + lang: "fr" | "en"; + onWorld: (world: DrivingWorld) => void; +}) { + const fr = lang === "fr"; + const agent = useRef(undefined); + const operation = useRef | null>(null), + controller = useRef(undefined); + const [status, setStatus] = useState("idle"), + [error, setError] = useState(""), + [seed, setSeed] = useState(11); + const [progress, setProgress] = useState({ + round: 0, + loss: null as number | null, + samples: 0, + traffic: false, + }); + const [records, setRecords] = useState([]), + [reports, setReports] = useState([]); + const [selected, setSelected] = useState(""), + [saved, setSaved] = useState(""); + const [ready, setReady] = useState(false); + const busy = !ready || status === "training" || status === "evaluating"; + useEffect(() => { + let mounted = true; + void backendReady + .then(() => { + if (mounted) setReady(true); + }) + .catch((e) => { + if (mounted) setError(String(e)); + }); + try { + setRecords(listDrivers()); + } catch (e) { + setError(String(e)); + } + return () => { + mounted = false; + controller.current?.abort(); + const old = agent.current; + void (operation.current ?? Promise.resolve()).finally(() => + old?.dispose(), + ); + }; + }, []); + const start = (fresh: boolean) => { + if (busy) return; + if (fresh || !agent.current) { + agent.current?.dispose(); + agent.current = new LearnedDriver(seed); + setReports([]); + setSaved(""); + } + setReports([]); + setSaved(""); + + const driver = agent.current; + setSeed(driver.seed); + setProgress({round:0,loss:null,samples:driver.samples,traffic:driver.updates>=24}); + controller.current = new AbortController(); + setError(""); + setStatus("training"); + operation.current = trainDriver(driver, { + seed: driver.seed, + rounds: 16, + signal: controller.current.signal, + onWorld: (r) => onWorld(r.drivers[0].world), + onProgress: setProgress, + }) + .catch((e) => setError(String(e))) + .finally(() => setStatus("idle")); + }; + const evaluate = () => { + if (!agent.current || busy) return; + const frozen = LearnedDriver.fromCheckpoint( + agent.current.exportCheckpoint(), + ); + controller.current = new AbortController(); + setError(""); + setStatus("evaluating"); + setReports([]); + operation.current = (async () => { + const results: DriverEvaluation[] = []; + try { + for (const test of [false, true]) + for (const traffic of [false, true]) + for (const evaluationSeed of [101, 307, 509]) { + results.push( + await evaluateDriver(frozen, { + seed: evaluationSeed, + test, + traffic, + signal: controller.current!.signal, + }), + ); + setReports([...results]); + } + } finally { + frozen.dispose(); + } + })() + .catch((e) => { + if (!controller.current?.signal.aborted) setError(String(e)); + }) + .finally(() => setStatus("idle")); + }; + const save = () => { + if (!agent.current || busy) return; + try { + const id = `driver-${agent.current.seed}-${Date.now()}`; + saveDriver({ + id, + name: `Pilote ${agent.current.seed} · ${agent.current.updates}`, + checkpoint: agent.current.exportCheckpoint(), + reports, + }); + setRecords(listDrivers()); + setSelected(id); + setSaved( + fr + ? "Pilote sauvegardé dans ce navigateur." + : "Driver saved in this browser.", + ); + setError(""); + } catch (e) { + setError(String(e)); + } + }; + const load = () => { + const record = records.find((r) => r.id === selected); + if (!record || busy) return; + try { + const loaded = LearnedDriver.fromCheckpoint(record.checkpoint); + agent.current?.dispose(); + agent.current = loaded; + setSeed(loaded.seed); + setReports(record.reports); + setProgress({ + round: 0, + loss: null as number | null, + samples: loaded.samples, + traffic: loaded.updates >= 24, + }); + setError(""); + setSaved( + fr + ? "Poids rechargés. La reprise recrée l’optimiseur." + : "Weights loaded. Resuming creates a fresh optimizer.", + ); + } catch (e) { + setError(String(e)); + } + }; + return ( +
+
+ {fr ? "APPRENDRE À PILOTER" : "LEARN TO DRIVE"} +
+

+ {fr + ? "Un vrai pilote.\nDes progrès mesurés." + : "A real driver.\nMeasured progress."} +

+

+ {fr + ? "Imitation · réseau de neurones. Il apprend sur Alpine Park, seul puis avec du trafic de référence figé. En course, seul le réseau décide." + : "Imitation · neural network. Learn at Alpine Park, solo then with frozen reference traffic. Only the network decides during races."} +

+ +
+ + + +
+
+ + {status === "training" + ? fr + ? "Entraînement" + : "Training" + : status === "evaluating" + ? fr + ? "Évaluation figée" + : "Frozen evaluation" + : fr + ? "Prêt" + : "Ready"} + +

+ {progress.round}/16 · {progress.samples.toLocaleString(lang)}{" "} + {fr ? "exemples" : "samples"} +

+

+ {fr ? "Erreur d’imitation" : "Imitation loss"}:{" "} + {progress.loss === null ? "—" : progress.loss.toFixed(4)} +

+ + {progress.traffic + ? fr + ? "Trafic de référence" + : "Reference traffic" + : "Solo"} + +
+
+ + +
+ {reports.length > 0 && ( +
+ + {reports.filter((r) => r.success).length}/{reports.length}{" "} + {fr ? "courses réussies" : "successful races"} + +

+ {fr + ? "Réussite = 3 tours, aucune remise en piste, pénalité ≤ 10 s." + : "Success = 3 laps, no rescue, penalty ≤ 10 s."} +

+
    + {reports.map((r) => ( +
  • + {r.test ? "Harbour" : "Alpine"} · {r.traffic ? "×4" : "solo"} ·{" "} + {r.seed}:{" "} + {r.completed + ? `${r.finishSeconds?.toFixed(1)} s` + : fr + ? "inachevé" + : "unfinished"} +
  • + ))} +
+
+ )} + + + {saved &&

{saved}

} + {error &&

{error}

} +
+ ); +} diff --git a/packages/demo-car-circuit/src/racing/learned-driver.ts b/packages/demo-car-circuit/src/racing/learned-driver.ts new file mode 100644 index 0000000..7972660 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/learned-driver.ts @@ -0,0 +1,164 @@ +import * as tf from "@tensorflow/tfjs"; +import { DRIVING_CONTRACT } from "./driving"; +import { OBSERVATION_CONTRACT, OBSERVATION_SIZE } from "./observations"; +import { LEARNING_PROTOCOL } from "./learning-protocol"; + +export interface DriverCheckpoint { + format: "ignition-driver-v1"; + algorithm: "imitation-mlp"; + driving: string; + observation: string; + protocol: string; + seed: number; + updates: number; + samples: number; + createdAt: string; + weights: { shape: number[]; values: number[] }[]; + configuration?: {hiddenLayers:number[]; learningRate:number; batchSize:number; epochsPerRound:number; samplesPerRound:number}; + evaluation?: unknown; +} +const SHAPES = [[20, 32], [32], [32, 32], [32], [32, 9], [9]]; +/** A trained neural network. Inference contains no reference controller or fallback. */ +export class LearnedDriver { + private model: tf.Sequential; + private optimizer: tf.Optimizer; + updates = 0; + samples = 0; + constructor(readonly seed: number) { + if (!Number.isSafeInteger(seed) || seed < 1 || seed > 1000000) + throw new Error("Seed must be an integer from 1 to 1000000"); + this.model = tf.sequential(); + this.model.add( + tf.layers.dense({ + inputShape: [OBSERVATION_SIZE], + units: 32, + activation: "tanh", + kernelInitializer: tf.initializers.glorotUniform({ seed }), + }), + ); + this.model.add( + tf.layers.dense({ + units: 32, + activation: "tanh", + kernelInitializer: tf.initializers.glorotUniform({ seed: seed + 1 }), + }), + ); + this.model.add( + tf.layers.dense({ + units: 9, + activation: "softmax", + kernelInitializer: tf.initializers.glorotUniform({ seed: seed + 2 }), + }), + ); + this.optimizer = tf.train.adam(0.003); + this.model.compile({ + optimizer: this.optimizer, + loss: "categoricalCrossentropy", + }); + } + action(observation: number[]): number { + if ( + observation.length !== OBSERVATION_SIZE || + !observation.every(Number.isFinite) + ) + throw new Error("Invalid driving observation"); + return tf.tidy(() => { + const values = ( + this.model.predict(tf.tensor2d([observation])) as tf.Tensor + ).dataSync(); + return values.indexOf(Math.max(...values)); + }); + } + async learn( + observations: number[][], + actions: number[], + epochs = 1, + ): Promise { + if ( + !observations.length || + observations.length !== actions.length || + actions.some((a) => !Number.isInteger(a) || a < 0 || a > 8) + ) + throw new Error("Invalid training samples"); + const x = tf.tensor2d(observations), + y = tf.tidy(() => tf.oneHot(tf.tensor1d(actions, "int32"), 9)); + try { + const history = await this.model.fit(x, y, { + epochs, + batchSize: 128, + shuffle: false, + verbose: 0, + }); + this.updates += epochs; + this.samples += observations.length; + return Number(history.history.loss.at(-1)); + } finally { + x.dispose(); + y.dispose(); + } + } + exportCheckpoint(): DriverCheckpoint { + return { + configuration: {hiddenLayers:[32,32],learningRate:.003,batchSize:128,epochsPerRound:3,samplesPerRound:4096}, + format: "ignition-driver-v1", + algorithm: "imitation-mlp", + driving: DRIVING_CONTRACT.id, + observation: OBSERVATION_CONTRACT, + protocol: LEARNING_PROTOCOL.id, + seed: this.seed, + updates: this.updates, + samples: this.samples, + createdAt: new Date().toISOString(), + weights: this.model + .getWeights() + .map((t) => ({ shape: t.shape, values: Array.from(t.dataSync()) })), + }; + } + static fromCheckpoint(value: unknown): LearnedDriver { + const c = value as DriverCheckpoint; + if ( + !c || + c.format !== "ignition-driver-v1" || + c.algorithm !== "imitation-mlp" || + c.driving !== DRIVING_CONTRACT.id || + c.observation !== OBSERVATION_CONTRACT || + c.protocol !== LEARNING_PROTOCOL.id + ) + throw new Error("Incompatible driver contract"); + if ( + !Number.isInteger(c.seed) || + !Number.isInteger(c.updates) || + c.updates < 0 || + !Number.isInteger(c.samples) || + c.samples < 0 + ) + throw new Error("Invalid driver metadata"); + if ( + !Array.isArray(c.weights) || + c.weights.length !== SHAPES.length || + c.weights.some( + (w, i) => + !w || + JSON.stringify(w.shape) !== JSON.stringify(SHAPES[i]) || + !Array.isArray(w.values) || + w.values.length !== SHAPES[i].reduce((a, b) => a * b, 1) || + !w.values.every(Number.isFinite), + ) + ) + throw new Error("Invalid driver weights"); + const driver = new LearnedDriver(c.seed); + const weights = c.weights.map((w) => tf.tensor(w.values, w.shape)); + try { + driver.model.setWeights(weights); + } finally { + tf.dispose(weights); + } + driver.updates = c.updates; + driver.samples = c.samples; + return driver; + } + dispose() { + this.model.dispose(); + this.optimizer.dispose(); + } +} diff --git a/packages/demo-car-circuit/src/racing/learning-protocol.ts b/packages/demo-car-circuit/src/racing/learning-protocol.ts new file mode 100644 index 0000000..55e36c2 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/learning-protocol.ts @@ -0,0 +1,22 @@ +import { DRIVING_CONTRACT } from "./driving"; +import { OBSERVATION_CONTRACT } from "./observations"; +import { RACE_PROTOCOL } from "./race"; + +/** Fixed before training runs; held-out results must never choose the checkpoint. */ +export const LEARNING_PROTOCOL = Object.freeze({ + id: "racing-learning-v1", + driving: DRIVING_CONTRACT.id, + observation: OBSERVATION_CONTRACT, + race: RACE_PROTOCOL.id, + trainingSeeds: [11, 29, 47], + checkpointSelection: "last scheduled update on training circuit only", + evaluationSeeds: [101, 307, 509], + success: { + completedLaps: 3, + maxRescues: 0, + maxPenalties: 10, + minimumCompletionRate: 0.8, + }, + evaluation: + "All three training seeds on both circuits, solo and three frozen rule-based traffic drivers. Report every run, including failures.", +}); diff --git a/packages/demo-car-circuit/src/racing/observations.ts b/packages/demo-car-circuit/src/racing/observations.ts new file mode 100644 index 0000000..05368e4 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/observations.ts @@ -0,0 +1,52 @@ +import { RaceWorld } from "./race"; + +export const OBSERVATION_CONTRACT = "racing-observation-v1"; +export const OBSERVATION_SIZE = 20; +const clamp = (n: number) => Math.max(-1, Math.min(1, n)); +/** Track-relative sensors, identical in collection, evaluation and races. */ +export function observeDriver(race: RaceWorld, id = 0): number[] { + const car = race.drivers[id].world.car; + const near = race.track.nearest(car.x, car.z); + const heading = Math.atan2( + Math.sin(near.angle - car.angle), + Math.cos(near.angle - car.angle), + ); + const lateral = + (car.x - near.x) * -Math.sin(near.angle) + + (car.z - near.z) * Math.cos(near.angle); + const result = [ + car.speed / 32, + car.steering, + clamp(lateral / (race.track.width / 2)), + heading / Math.PI, + ]; + for (const meters of [3 + car.speed * 0.15, 10, 20, 35]) { + const target = race.track.sample( + near.progress + meters / race.track.length, + ); + const error = Math.atan2( + Math.sin(Math.atan2(target.z - car.z, target.x - car.x) - car.angle), + Math.cos(Math.atan2(target.z - car.z, target.x - car.x) - car.angle), + ); + result.push(clamp(error / 0.6)); + } + const opponents = race.drivers + .filter((d) => d.id !== id) + .map((d) => { + const dx = d.world.car.x - car.x, + dz = d.world.car.z - car.z; + return { + distance: Math.hypot(dx, dz), + values: [ + clamp((dx * Math.cos(car.angle) + dz * Math.sin(car.angle)) / 30), + clamp((-dx * Math.sin(car.angle) + dz * Math.cos(car.angle)) / 10), + (d.world.car.speed - car.speed) / 32, + 1, + ], + }; + }) + .sort((a, b) => a.distance - b.distance); + for (let i = 0; i < 3; i++) + result.push(...(opponents[i]?.values ?? [0, 0, 0, 0])); + return result; +} diff --git a/packages/demo-car-circuit/src/racing/racing.css b/packages/demo-car-circuit/src/racing/racing.css index ba102aa..10ebb58 100644 --- a/packages/demo-car-circuit/src/racing/racing.css +++ b/packages/demo-car-circuit/src/racing/racing.css @@ -445,6 +445,79 @@ } } -.pause-overlay ol { list-style:none; width:min(360px,80%); padding:0; margin:20px 0; counter-reset:position; } -.pause-overlay li { counter-increment:position; display:flex; align-items:center; gap:18px; background:#182329; padding:12px 18px; margin:4px 0; font-variant-numeric:tabular-nums; border:1px solid #394449; } -.pause-overlay li::before { content:counter(position); color:var(--race-accent); font-weight:bold; } +.pause-overlay ol { + list-style: none; + width: min(360px, 80%); + padding: 0; + margin: 20px 0; + counter-reset: position; +} +.pause-overlay li { + counter-increment: position; + display: flex; + align-items: center; + gap: 18px; + background: #182329; + padding: 12px 18px; + margin: 4px 0; + font-variant-numeric: tabular-nums; + border: 1px solid #394449; +} +.pause-overlay li::before { + content: counter(position); + color: var(--race-accent); + font-weight: bold; +} + +.training-panel label { + display: grid; + gap: 8px; + font-size: 12px; + margin: 16px 0; +} +.training-panel input, +.training-panel select { + width: 100%; + background: var(--race-panel); + color: var(--race-text); + border: 1px solid var(--race-line); + padding: 10px; +} +.training-actions { + display: grid; + gap: 8px; + margin: 16px 0; +} +.training-actions button { + border: 1px solid var(--race-line); + padding: 12px; + background: var(--race-panel); + color: var(--race-text); + text-align: left; +} +.training-actions .drive-button { + background: var(--race-accent); + color: var(--race-bg); +} +.training-actions button:disabled { + opacity: 0.4; + cursor: not-allowed; +} +.training-metrics { + border: 1px solid var(--race-line); + padding: 16px; + font-size: 12px; + line-height: 1.6; +} +.training-metrics p { + margin: 8px 0; + color: var(--race-muted); +} +.training-metrics ul { + padding-left: 16px; + font-size: 10px; +} +.training-panel [role="alert"] { + color: #ffb3a2; + font-size: 12px; +} diff --git a/packages/demo-car-circuit/src/racing/training.ts b/packages/demo-car-circuit/src/racing/training.ts new file mode 100644 index 0000000..6bdd9bf --- /dev/null +++ b/packages/demo-car-circuit/src/racing/training.ts @@ -0,0 +1,175 @@ +import { RaceWorld } from "./race"; +import { observeDriver } from "./observations"; +import { referenceAction } from "./reference"; +import { LearnedDriver, type DriverCheckpoint } from "./learned-driver"; + +export function seededRandom(seed: number) { + let state = seed >>> 0; + return () => { + state = (Math.imul(state, 1664525) + 1013904223) >>> 0; + return state / 4294967296; + }; +} +export interface TrainingProgress { + round: number; + loss: number; + samples: number; + traffic: boolean; +} +/** Dataset aggregation: one learner, labels from frozen rule references, no test track. */ +export async function trainDriver( + driver: LearnedDriver, + options: { + rounds: number; + seed: number; + signal?: AbortSignal; + onWorld?: (world: RaceWorld) => void; + onProgress?: (p: TrainingProgress) => void; + }, +) { + const random = seededRandom(options.seed + driver.updates * 997); + for (let round = 0; round < options.rounds; round++) { + if (options.signal?.aborted) return; + const traffic = driver.updates >= 24; + const observations: number[][] = [], + labels: number[] = []; + let race = new RaceWorld({ count: traffic ? 4 : 1, countdown: 0 }); + // Diverse start states are training-only curriculum, with unchanged race physics. + for (let sample = 0; sample < 4096; sample++) { + if (sample % 256 === 0) { + await new Promise((resolve) => setTimeout(resolve, 0)); + if (options.signal?.aborted) return; + } + if (sample % 128 === 0) { + race = new RaceWorld({ count: traffic ? 4 : 1, countdown: 0 }); + const d = race.drivers[0], + p = race.track.sample(random()); + const side = (random() - 0.5) * 4; + Object.assign(d.world.car, { + x: p.x - Math.sin(p.angle) * side, + z: p.z + Math.cos(p.angle) * side, + angle: p.angle + (random() - 0.5) * 0.6, + speed: random() * 24, + steering: (random() - 0.5) * 2, + }); + d.lastProgress = p.progress; + d.gates = Math.floor(p.progress * 20); + d.safeProgress = d.gates / 20; + } + const observation = observeDriver(race), + label = referenceAction(race.drivers[0].world); + observations.push(observation); + labels.push(label); + const useLearner = driver.updates > 0 && random() < 0.5; + const actions = race.drivers.map((d, i) => + i === 0 && useLearner + ? driver.action(observation) + : referenceAction(d.world), + ); + race.step(actions); + } + // Deterministic shuffle keeps sequential trajectories from biasing mini-batches. + for (let i = observations.length - 1; i > 0; i--) { + const j = Math.floor(random() * (i + 1)); + [observations[i], observations[j]] = [observations[j], observations[i]]; + [labels[i], labels[j]] = [labels[j], labels[i]]; + } + if (options.signal?.aborted) return; + const loss = await driver.learn(observations, labels, 3); + if (options.signal?.aborted) return; + options.onWorld?.(race); + options.onProgress?.({ + round: round + 1, + loss, + samples: driver.samples, + traffic, + }); + await new Promise((resolve) => setTimeout(resolve, 0)); + } +} +export interface DriverEvaluation { + seed: number; + test: boolean; + traffic: boolean; + completed: boolean; + success: boolean; + laps: number; + finishSeconds: number | null; + penaltySeconds: number; + rescues: number; + contacts: number; +} +/** Receives a separately loaded network and never updates it. */ +export async function evaluateDriver( + driver: LearnedDriver, + { + seed, + test = false, + traffic = false, + signal, + }: { seed: number; test?: boolean; traffic?: boolean; signal?: AbortSignal }, +): Promise { + const race = new RaceWorld({ count: traffic ? 4 : 1, test, countdown: 0 }); + const random = seededRandom(seed), + car = race.drivers[0].world.car; + car.angle += (random() - 0.5) * 0.06; + let steps = 0; + while (!race.finished && race.drivers[0].finishSeconds === null) { + if (signal?.aborted) throw new Error("Evaluation cancelled"); + const action = driver.action(observeDriver(race)); + race.step( + race.drivers.map((d, i) => (i === 0 ? action : referenceAction(d.world))), + ); + if (++steps % 240 === 0) + await new Promise((resolve) => setTimeout(resolve, 0)); + } + const d = race.drivers[0], + completed = d.finishSeconds !== null; + return { + seed, + test, + traffic, + completed, + success: completed && d.rescues === 0 && d.penaltySeconds <= 10, + laps: d.laps, + finishSeconds: d.finishSeconds, + penaltySeconds: d.penaltySeconds, + rescues: d.rescues, + contacts: d.collisions, + }; +} +export interface SavedDriver { + id: string; + name: string; + checkpoint: DriverCheckpoint; + reports: DriverEvaluation[]; +} +const STORE_KEY = "ignition-racing-drivers-v1"; +export function listDrivers( + storage: Pick = localStorage, +): SavedDriver[] { + const raw = storage.getItem(STORE_KEY); + if (!raw) return []; + const records: unknown = JSON.parse(raw); + if ( + !Array.isArray(records) || + records.some( + (r) => + !r || + typeof r.id !== "string" || + typeof r.name !== "string" || + !r.checkpoint || + !Array.isArray(r.reports), + ) + ) + throw new Error("Invalid local driver garage"); + return records as SavedDriver[]; +} +export function saveDriver( + record: SavedDriver, + storage: Pick = localStorage, +) { + const records = listDrivers(storage).filter((d) => d.id !== record.id); + // A single write preserves the previous garage if storage quota is exceeded. + storage.setItem(STORE_KEY, JSON.stringify([...records, record])); +} diff --git a/packages/demo-car-circuit/test/racing-learning.test.ts b/packages/demo-car-circuit/test/racing-learning.test.ts new file mode 100644 index 0000000..84f0ebb --- /dev/null +++ b/packages/demo-car-circuit/test/racing-learning.test.ts @@ -0,0 +1,134 @@ +import { expect, it } from "vitest"; +import { RaceWorld } from "../src/racing/race"; +import { observeDriver, OBSERVATION_SIZE } from "../src/racing/observations"; +import { LearnedDriver } from "../src/racing/learned-driver"; + +it("uses the same finite observation schema with solo and traffic worlds", () => { + for (const count of [1, 4]) { + const race = new RaceWorld({ count }); + expect(observeDriver(race)).toHaveLength(OBSERVATION_SIZE); + expect(observeDriver(race).every(Number.isFinite)).toBe(true); + } +}); +it("learns, serializes actual weights, and reloads identical frozen decisions", async () => { + const driver = new LearnedDriver(11); + const observation = observeDriver(new RaceWorld()); + const before = driver.exportCheckpoint(); + await driver.learn( + Array.from({ length: 32 }, () => observation), + Array(32).fill(7), + 15, + ); + const saved = driver.exportCheckpoint(); + expect(saved.weights).not.toEqual(before.weights); + const loaded = LearnedDriver.fromCheckpoint( + JSON.parse(JSON.stringify(saved)), + ); + expect(loaded.action(observation)).toBe(driver.action(observation)); + expect(loaded.exportCheckpoint().weights).toEqual(saved.weights); + expect(saved.updates).toBeGreaterThan(0); + driver.dispose(); + loaded.dispose(); +}); +it("rejects old contracts and malformed weight payloads before loading", () => { + const driver = new LearnedDriver(11), + saved = driver.exportCheckpoint(); + expect(() => + LearnedDriver.fromCheckpoint({ ...saved, driving: "constant-speed-v1" }), + ).toThrow(/contract/i); + expect(() => LearnedDriver.fromCheckpoint({ ...saved, weights: [] })).toThrow( + /weights/i, + ); + driver.dispose(); +}); + +import { readFileSync } from "node:fs"; +import { + evaluateDriver, + listDrivers, + saveDriver, +} from "../src/racing/training"; +it("completes a race using persisted learned weights without changing them", async () => { + const checkpoint = JSON.parse( + readFileSync( + new URL("../src/public/drivers/driver-11.json", import.meta.url), + "utf8", + ), + ); + const driver = LearnedDriver.fromCheckpoint(checkpoint); + const before = driver.exportCheckpoint().weights; + const result = await evaluateDriver(driver, { seed: 101 }); + expect(result.laps).toBe(3); + expect(result.completed).toBe(true); + expect(driver.exportCheckpoint().weights).toEqual(before); + driver.dispose(); +}, 30000); +it("round-trips local driver metadata and preserves the garage on a quota failure", () => { + let value: string | null = null; + const storage = { + getItem: () => value, + setItem: (_key: string, next: string) => { + value = next; + }, + }; + const driver = new LearnedDriver(11); + const saved = { + id: "my-driver", + name: "My driver", + checkpoint: driver.exportCheckpoint(), + reports: [], + }; + saveDriver(saved, storage); + expect(listDrivers(storage)).toEqual([saved]); + const before = value; + expect(() => + saveDriver( + { ...saved, id: "second" }, + { + ...storage, + setItem: () => { + throw new Error("quota"); + }, + }, + ), + ).toThrow("quota"); + expect(value).toBe(before); + expect(() => listDrivers({ getItem: () => "[null]" })).toThrow(/garage/); + driver.dispose(); +}); + +import { vi } from "vitest"; +import { trainDriver } from "../src/racing/training"; +it("does not publish an obsolete training world when cancelled during the update", async () => { + const driver = new LearnedDriver(11), + controller = new AbortController(); + const onWorld = vi.fn(), + onProgress = vi.fn(); + vi.spyOn(driver, "learn").mockImplementation(async () => { + controller.abort(); + return 0; + }); + await trainDriver(driver, { + seed: 11, + rounds: 1, + signal: controller.signal, + onWorld, + onProgress, + }); + expect(onWorld).not.toHaveBeenCalled(); + expect(onProgress).not.toHaveBeenCalled(); + driver.dispose(); +}); +it("rejects noninteger seeds before creating an unloadable checkpoint", () => { + expect(() => new LearnedDriver(11.5)).toThrow(/Seed/); +}); + +import * as tf from '@tensorflow/tfjs'; +it('keeps tensor usage stable across checkpoint exports and frozen inference',()=> { + const driver=new LearnedDriver(11),observation=observeDriver(new RaceWorld()); + driver.action(observation); + const before=tf.memory().numTensors; + for(let i=0;i<50;i++) {driver.exportCheckpoint();driver.action(observation);} + expect(tf.memory().numTensors).toBe(before); + driver.dispose(); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c188b4..c350f49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,6 +141,9 @@ importers: '@react-three/fiber': specifier: ^9.1.2 version: 9.1.2(@types/react@19.1.2)(immer@11.1.4)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(three@0.162.0) + '@tensorflow/tfjs': + specifier: 4.22.0 + version: 4.22.0(encoding@0.1.13)(seedrandom@3.0.5) react: specifier: ^19.0.0 version: 19.1.0 diff --git a/scripts/train-racing.mts b/scripts/train-racing.mts new file mode 100644 index 0000000..bb1a63b --- /dev/null +++ b/scripts/train-racing.mts @@ -0,0 +1,63 @@ +import { createRequire } from "node:module"; +import { mkdir, writeFile } from "node:fs/promises"; +const require = createRequire( + new URL("../packages/backend-tfjs/package.json", import.meta.url), +); +require("@tensorflow/tfjs-node"); +import { LearnedDriver } from "../packages/demo-car-circuit/src/racing/learned-driver"; +import { + trainDriver, + evaluateDriver, +} from "../packages/demo-car-circuit/src/racing/training"; +import { LEARNING_PROTOCOL } from "../packages/demo-car-circuit/src/racing/learning-protocol"; +const seeds = (process.env.RACING_SEEDS ?? "11,29,47").split(",").map(Number); +const rounds = Number(process.env.RACING_ROUNDS ?? 16); +const folder = + process.env.RACING_OUTPUT ?? ".scratch/circuit-racing/learned-v1"; +await mkdir(folder, { recursive: true }); +for (const seed of seeds) { + const driver = new LearnedDriver(seed), + start = Date.now(); + const untrained = await evaluateDriver(driver, { seed: 101 }); + await trainDriver(driver, { + seed, + rounds, + onProgress: (p) => + console.log( + JSON.stringify({ seed, ...p, seconds: (Date.now() - start) / 1000 }), + ), + }); + const checkpoint = driver.exportCheckpoint(); + await writeFile(`${folder}/driver-${seed}.json`, JSON.stringify(checkpoint)); + const frozen = LearnedDriver.fromCheckpoint( + JSON.parse(JSON.stringify(checkpoint)), + ); + const reports = []; + for (const test of [false, true]) + for (const traffic of [false, true]) + for (const evaluationSeed of LEARNING_PROTOCOL.evaluationSeeds) { + const report = await evaluateDriver(frozen, { + seed: evaluationSeed, + test, + traffic, + }); + reports.push(report); + console.log(JSON.stringify({ trainingSeed: seed, ...report })); + } + await writeFile( + `${folder}/report-${seed}.json`, + JSON.stringify( + { + protocol: LEARNING_PROTOCOL, + rounds, + untrained, + reports, + seconds: (Date.now() - start) / 1000, + }, + null, + 2, + ), + ); + frozen.dispose(); + driver.dispose(); +} From ce4443d7792fcfc7fab90f3fc6d6ecf756e3dcd9 Mon Sep 17 00:00:00 2001 From: salimlaimeche Date: Sun, 6 Sep 2026 18:31:16 +0200 Subject: [PATCH 06/18] feat: race saved learned drivers with frozen policies (#16) --- packages/demo-car-circuit/README.md | 4 +- .../src/racing/RaceGarage.tsx | 173 ++++++++++++++++++ .../demo-car-circuit/src/racing/RacingApp.tsx | 109 +++++++++-- .../src/racing/RacingScene.tsx | 29 ++- .../demo-car-circuit/src/racing/garage.ts | 50 +++++ .../src/racing/learned-race.ts | 53 ++++++ .../demo-car-circuit/src/racing/racing.css | 5 + .../test/learned-race.test.ts | 39 ++++ .../test/racing-garage.test.ts | 31 ++++ 9 files changed, 466 insertions(+), 27 deletions(-) create mode 100644 packages/demo-car-circuit/src/racing/RaceGarage.tsx create mode 100644 packages/demo-car-circuit/src/racing/garage.ts create mode 100644 packages/demo-car-circuit/src/racing/learned-race.ts create mode 100644 packages/demo-car-circuit/test/learned-race.test.ts create mode 100644 packages/demo-car-circuit/test/racing-garage.test.ts diff --git a/packages/demo-car-circuit/README.md b/packages/demo-car-circuit/README.md index 461bca3..a60d6ac 100644 --- a/packages/demo-car-circuit/README.md +++ b/packages/demo-car-circuit/README.md @@ -16,7 +16,9 @@ Evaluation reconstructs a separate frozen network and checks both circuits, solo `src/public/drivers/` contains all three scheduled training seeds and their complete reports, including failures. Aggregate measured success is 29/36; no checkpoint was selected by test-track performance. These imitation drivers are imperfect and often make contact in traffic. Run `pnpm dlx tsx scripts/train-racing.mts` at the root to reproduce native training/evaluation (requires the installed TFJS Node addon). -Player-versus-trained-driver inference and races between saved learned drivers are the next slices. +Course now opens a garage for two to four learned competitors. Select a local saved checkpoint or any of the three supplied trained networks, choose each vehicle independently, and start the AI race. The camera selector follows each driver's own speed, lap and penalty metrics. Results identify each checkpoint. Each race owns separately loaded frozen networks and disposes them on session changes. Missing bundled files are reported without hiding available local drivers; no rule-based substitute is used. + +Browser proof: supplied checkpoints 29 and 11 both completed three laps, in 105.45 s and 118.92 s respectively. The public integration test verifies completion with unchanged weights. Player-versus-trained-driver keyboard inference is the next slice. The frozen racing evaluation protocol names Alpine Park for training, Harbour for held-out testing, and seeds 101/307/509 before learned checkpoint selection. The original constant-speed evaluation has its separate version. diff --git a/packages/demo-car-circuit/src/racing/RaceGarage.tsx b/packages/demo-car-circuit/src/racing/RaceGarage.tsx new file mode 100644 index 0000000..40ba333 --- /dev/null +++ b/packages/demo-car-circuit/src/racing/RaceGarage.tsx @@ -0,0 +1,173 @@ +import { useEffect, useState } from "react"; +import * as tf from "@tensorflow/tfjs"; +import { LearnedRace, type RaceEntry } from "./learned-race"; +import { type SavedDriver } from "./training"; +import { loadGarage } from "./garage"; +const backendReady = tf.setBackend("cpu"); + +export default function RaceGarage({ + lang, + onStart, + onPractice, +}: { + lang: "fr" | "en"; + onStart: (race: LearnedRace) => void; + onPractice: (references: boolean) => void; +}) { + const fr = lang === "fr"; + const [drivers, setDrivers] = useState([]), + [error, setError] = useState(""), + [ready, setReady] = useState(false); + const [slots, setSlots] = useState(["bundled-11", "bundled-29"]); + const [models, setModels] = useState<("race" | "sedan-sports")[]>([ + "race", + "sedan-sports", + "race", + "sedan-sports", + ]); + useEffect(() => { + let mounted = true; + void (async () => { + await backendReady; + const garage = await loadGarage({ + storage: localStorage, + base: import.meta.env.BASE_URL, + }); + if (mounted) { + setDrivers(garage.drivers); + setError(garage.errors.join(" · ")); + setReady(true); + } + })().catch((e) => { + if (mounted) setError(String(e)); + }); + return () => { + mounted = false; + }; + }, [fr]); + const start = () => { + try { + const entries: RaceEntry[] = slots.map((id, i) => { + const saved = drivers.find((d) => d.id === id); + if (!saved) + throw new Error( + fr ? "Choisissez un pilote valide." : "Choose a valid driver.", + ); + return { + id: saved.id, + name: saved.name, + model: models[i], + checkpoint: saved.checkpoint, + }; + }); + const session = new LearnedRace(entries); + onStart(session); + setError(""); + (document.activeElement as HTMLElement)?.blur(); + } catch (e) { + setError(String(e)); + } + }; + return ( +
+
+ {fr ? "LA GRILLE DE DÉPART" : "THE STARTING GRID"} +
+

+ {fr ? "Les pilotes\nentrent en piste." : "Drivers take\nthe track."} +

+

+ {fr + ? "Choisissez 2 à 4 checkpoints. Chaque voiture utilise sa propre copie figée du réseau appris." + : "Choose 2–4 checkpoints. Every car uses its own frozen copy of the learned network."} +

+ + {slots.map((id, i) => { + const selected = drivers.find((d) => d.id === id); + return ( +
+ + + {selected && ( + + {selected.reports.filter((r) => r.success).length}/ + {selected.reports.length}{" "} + {fr ? "évaluations réussies" : "successful evaluations"} ·{" "} + {selected.id} + + )} +
+ ); + })} +
+ +
+

+ {fr + ? "Les pilotes fournis sont imparfaits : leurs échecs d’évaluation restent visibles. Aucun contrôleur à règles ne les remplace." + : "Bundled drivers are imperfect: evaluation failures remain visible. No rule controller replaces them."} +

+ + + {error &&

{error}

} +
+ ); +} diff --git a/packages/demo-car-circuit/src/racing/RacingApp.tsx b/packages/demo-car-circuit/src/racing/RacingApp.tsx index e7b192d..3351710 100644 --- a/packages/demo-car-circuit/src/racing/RacingApp.tsx +++ b/packages/demo-car-circuit/src/racing/RacingApp.tsx @@ -13,6 +13,8 @@ import { DrivingWorld } from "./driving"; import { RacingScene } from "./RacingScene"; import "./racing.css"; +import type { LearnedRace } from "./learned-race"; +const RaceGarage = lazy(() => import("./RaceGarage")); const TrainingPanel = lazy(() => import("./TrainingPanel")); const copy = { @@ -120,9 +122,17 @@ export default function RacingApp() { const [race, setRace] = useState(); const [reference, setReference] = useState(false); const [trainingMode, setTrainingMode] = useState(false); + const [garageMode, setGarageMode] = useState(false); + const [learned, setLearned] = useState(); + const [follow, setFollow] = useState(0); + const displayedDriver = race?.drivers[learned ? follow : 0]; + useEffect(() => () => learned?.dispose(), [learned]); const [, updateHUD] = useState(0); const resetSession = (competitive: boolean, references = false) => { setTrainingMode(false); + setGarageMode(false); + setLearned(undefined); + setFollow(0); const next = competitive ? new RaceWorld({ count: references ? 4 : 1 }) : undefined; @@ -193,7 +203,7 @@ export default function RacingApp() {
diff --git a/packages/demo-car-circuit/src/racing/TrainingPanel.tsx b/packages/demo-car-circuit/src/racing/TrainingPanel.tsx index 2f40585..4b855e4 100644 --- a/packages/demo-car-circuit/src/racing/TrainingPanel.tsx +++ b/packages/demo-car-circuit/src/racing/TrainingPanel.tsx @@ -1,6 +1,8 @@ import * as tf from "@tensorflow/tfjs"; import { useEffect, useRef, useState } from "react"; -import { LearnedDriver } from "./learned-driver"; +import { trainQDriver } from "./q-training"; +import { Q_PROTOCOL } from "./q-protocol"; +import { LearnedDriver, type DriverCheckpoint } from "./learned-driver"; import { trainDriver, evaluateDriver, @@ -37,7 +39,9 @@ export default function TrainingPanel({ [reports, setReports] = useState([]); const [selected, setSelected] = useState(""), [saved, setSaved] = useState(""); + const [algorithm, setAlgorithm] = useState("imitation-mlp"); const [ready, setReady] = useState(false); + const activeAlgorithm = agent.current?.algorithm ?? algorithm; const busy = !ready || status === "training" || status === "evaluating"; useEffect(() => { let mounted = true; @@ -56,9 +60,8 @@ export default function TrainingPanel({ return () => { mounted = false; controller.current?.abort(); - const old = agent.current; void (operation.current ?? Promise.resolve()).finally(() => - old?.dispose(), + agent.current?.dispose(), ); }; }, []); @@ -66,7 +69,7 @@ export default function TrainingPanel({ if (busy) return; if (fresh || !agent.current) { agent.current?.dispose(); - agent.current = new LearnedDriver(seed); + agent.current = new LearnedDriver(seed, algorithm); setReports([]); setSaved(""); } @@ -75,17 +78,23 @@ export default function TrainingPanel({ const driver = agent.current; setSeed(driver.seed); - setProgress({round:0,loss:null,samples:driver.samples,traffic:driver.updates>=24}); + setAlgorithm(driver.algorithm); + setProgress({round:0,loss:null,samples:driver.samples,traffic:driver.algorithm === "imitation-mlp" ? driver.updates>=24 : driver.samples>=Q_PROTOCOL.trafficAfterTransitions}); controller.current = new AbortController(); setError(""); setStatus("training"); - operation.current = trainDriver(driver, { - seed: driver.seed, - rounds: 16, + const common = { signal: controller.current.signal, - onWorld: (r) => onWorld(r.drivers[0].world), + onWorld: (r: import("./race").RaceWorld) => onWorld(r.drivers[0].world), onProgress: setProgress, - }) + }; + operation.current = (driver.algorithm === "imitation-mlp" + ? trainDriver(driver, { ...common, seed:driver.seed, rounds:16 }) + : trainQDriver(driver.exportCheckpoint(), common).then(checkpoint => { + const updated = LearnedDriver.fromCheckpoint(checkpoint); + driver.dispose(); + agent.current = updated; + })) .catch((e) => setError(String(e))) .finally(() => setStatus("idle")); }; @@ -103,7 +112,7 @@ export default function TrainingPanel({ try { for (const test of [false, true]) for (const traffic of [false, true]) - for (const evaluationSeed of [101, 307, 509]) { + for (const evaluationSeed of frozen.algorithm === "imitation-mlp" ? [101, 307, 509] : Q_PROTOCOL.evaluationSeeds) { results.push( await evaluateDriver(frozen, { seed: evaluationSeed, @@ -129,7 +138,7 @@ export default function TrainingPanel({ const id = `driver-${agent.current.seed}-${Date.now()}`; saveDriver({ id, - name: `Pilote ${agent.current.seed} · ${agent.current.updates}`, + name: `${agent.current.algorithm} · ${agent.current.seed} · ${agent.current.updates}`, checkpoint: agent.current.exportCheckpoint(), reports, }); @@ -153,12 +162,13 @@ export default function TrainingPanel({ agent.current?.dispose(); agent.current = loaded; setSeed(loaded.seed); + setAlgorithm(loaded.algorithm); setReports(record.reports); setProgress({ round: 0, loss: null as number | null, samples: loaded.samples, - traffic: loaded.updates >= 24, + traffic: loaded.algorithm === "imitation-mlp" ? loaded.updates >= 24 : loaded.samples >= Q_PROTOCOL.trafficAfterTransitions, }); setError(""); setSaved( @@ -181,10 +191,21 @@ export default function TrainingPanel({ : "A real driver.\nMeasured progress."}

- {fr + {algorithm !== "imitation-mlp" ? (fr ? "DQN / Double DQN · apprentissage par récompenses sur Alpine Park. Budget : 20 000 transitions. Les résultats restent à mesurer ; ce pilote peut ne pas finir une course." : "DQN / Double DQN · reward learning at Alpine Park. Budget: 20,000 transitions. Results must be measured; this driver may not finish a race.") : fr ? "Imitation · réseau de neurones. Il apprend sur Alpine Park, seul puis avec du trafic de référence figé. En course, seul le réseau décide." : "Imitation · neural network. Learn at Alpine Park, solo then with frozen reference traffic. Only the network decides during races."}

+

+ {fr ? "DQN / Double DQN : voir les résultats comparés ↗" : "DQN / Double DQN: compare measured results ↗"} +

+
)} {race?.finished && displayedDriver && ( @@ -455,9 +457,11 @@ export default function RacingApp() {
    {race.standings.map((d) => (
  1. - {learned?.competitors[d.id]?.name ?? `#${d.id + 1}`}{" "} - {learned && ({learned.competitors[d.id].id})}{" "} - · {d.finishSeconds === null ? "DNF" : `${d.finishSeconds.toFixed(2)} s`} + + {learned?.competitors[d.id]?.name ?? `#${d.id + 1}`} + {learned && {learned.competitors[d.id].id}} + + {d.finishSeconds === null ? "DNF" : `${d.finishSeconds.toFixed(2)} s`}
  2. ))}
diff --git a/packages/demo-car-circuit/src/racing/TrainingPanel.tsx b/packages/demo-car-circuit/src/racing/TrainingPanel.tsx index 4b855e4..1c0a815 100644 --- a/packages/demo-car-circuit/src/racing/TrainingPanel.tsx +++ b/packages/demo-car-circuit/src/racing/TrainingPanel.tsx @@ -195,7 +195,7 @@ export default function TrainingPanel({ ? "Imitation · réseau de neurones. Il apprend sur Alpine Park, seul puis avec du trafic de référence figé. En course, seul le réseau décide." : "Imitation · neural network. Learn at Alpine Park, solo then with frozen reference traffic. Only the network decides during races."}

-

+

{fr ? "DQN / Double DQN : voir les résultats comparés ↗" : "DQN / Double DQN: compare measured results ↗"}