@inventure71/paddockjs 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/INSTALL_AND_UPDATE.md +4 -3
- package/README.md +194 -12
- package/docs/architecture.md +119 -44
- package/docs/concepts.md +4 -2
- package/docs/custom_model_controller.md +329 -0
- package/docs/data_contract.md +299 -17
- package/docs/index.md +1 -0
- package/docs/learnings.md +22 -0
- package/docs/rules.md +37 -10
- package/docs/system_specs.md +23 -10
- package/docs/training.md +272 -30
- package/examples/python/README.md +62 -0
- package/examples/python/base_policy_server.py +191 -0
- package/examples/python/requirements.txt +2 -0
- package/package.json +12 -4
- package/src/api/PaddockSimulatorController.js +4 -0
- package/src/app/BrowserExpertAdapter.js +367 -9
- package/src/app/F1SimulatorApp.js +305 -1603
- package/src/app/banners/bannerFormatters.js +55 -0
- package/src/app/banners/raceDataBannerController.js +141 -0
- package/src/app/banners/stewardMessageController.js +99 -0
- package/src/app/camera/cameraConstants.js +21 -0
- package/src/app/camera/cameraController.js +265 -0
- package/src/app/camera/cameraGeometry.js +176 -0
- package/src/app/domBindings.js +4 -0
- package/src/app/readouts/carOverviewRenderer.js +109 -0
- package/src/app/readouts/controlStateRenderer.js +37 -0
- package/src/app/readouts/raceStatusRenderer.js +133 -0
- package/src/app/readouts/readoutFormatters.js +58 -0
- package/src/app/readouts/telemetryRenderer.js +113 -0
- package/src/app/readouts/timingTowerRenderer.js +177 -0
- package/src/app/rendering/appAssets.js +38 -0
- package/src/app/rendering/carRenderer.js +286 -0
- package/src/app/rendering/displayUtils.js +22 -0
- package/src/app/rendering/drsTrailRenderer.js +90 -0
- package/src/app/rendering/expertSensorRenderer.js +126 -0
- package/src/app/rendering/pitLaneStatusRenderer.js +50 -0
- package/src/app/rendering/replayGhostRenderer.js +124 -0
- package/src/app/rendering/trackRenderer.js +25 -0
- package/src/app/runtime/frameLoop.js +66 -0
- package/src/app/runtime/runtimeTiming.js +34 -0
- package/src/app/runtime/runtimeVisibility.js +45 -0
- package/src/config/defaultOptions.js +20 -0
- package/src/environment/actions.js +13 -5
- package/src/environment/controllerLoop.js +246 -0
- package/src/environment/episode.js +86 -2
- package/src/environment/evaluation.js +54 -32
- package/src/environment/index.d.ts +361 -9
- package/src/environment/index.js +1 -0
- package/src/environment/metrics.js +159 -0
- package/src/environment/observationOptions.js +22 -1
- package/src/environment/observations.js +325 -40
- package/src/environment/options.js +37 -5
- package/src/environment/runtime.js +222 -21
- package/src/environment/scenarios.js +37 -8
- package/src/environment/sensors/bodySenses.js +23 -0
- package/src/environment/sensors/boundarySenses.js +17 -0
- package/src/environment/sensors/carRays.js +75 -0
- package/src/environment/sensors/contactSenses.js +32 -0
- package/src/environment/sensors/index.js +157 -0
- package/src/environment/sensors/indexedRayBands.js +103 -0
- package/src/environment/sensors/nearbyCars.js +52 -0
- package/src/environment/sensors/opponentRadar.js +65 -0
- package/src/environment/sensors/rayConfig.js +110 -0
- package/src/environment/sensors/rayDefaults.js +6 -0
- package/src/environment/sensors/rayGeometry.js +60 -0
- package/src/environment/sensors/rayGuards.js +19 -0
- package/src/environment/sensors/sensorTargets.js +60 -0
- package/src/environment/sensors/surfaceRays.js +296 -0
- package/src/environment/sensors/trackRays.js +261 -0
- package/src/environment/sensors.js +1 -367
- package/src/environment/specs.js +100 -5
- package/src/environment/workerProtocol.js +13 -1
- package/src/index.d.ts +169 -6
- package/src/index.js +5 -0
- package/src/rendering/proceduralTrackAsset.js +20 -823
- package/src/rendering/renderSnapshot.js +38 -12
- package/src/rendering/track/finishLineRenderer.js +34 -0
- package/src/rendering/track/gridRenderer.js +47 -0
- package/src/rendering/track/offsetStrokeSafety.js +136 -0
- package/src/rendering/track/pitLaneRenderer.js +248 -0
- package/src/rendering/track/trackMaterialRenderer.js +326 -0
- package/src/rendering/track/trackRenderConstants.js +38 -0
- package/src/rendering/track/trackRenderGeometry.js +94 -0
- package/src/simulation/collisionGeometry.js +1 -1
- package/src/simulation/driver/driverControlConstants.js +28 -0
- package/src/simulation/driver/driverController.js +47 -0
- package/src/simulation/driver/driverInput.js +28 -0
- package/src/simulation/driver/driverMath.js +6 -0
- package/src/simulation/driver/driverPersonality.js +25 -0
- package/src/simulation/driver/edgeRecovery.js +48 -0
- package/src/simulation/driver/racingControls.js +399 -0
- package/src/simulation/driver/racingLinePlan.js +166 -0
- package/src/simulation/driver/recoveryDynamics.js +38 -0
- package/src/simulation/driver/rejoinControls.js +213 -0
- package/src/simulation/driver/safetyCarControls.js +35 -0
- package/src/simulation/driver/trafficScan.js +51 -0
- package/src/simulation/driverController.js +4 -619
- package/src/simulation/participants/participantInteractions.js +120 -0
- package/src/simulation/pit/pitFlow.js +234 -0
- package/src/simulation/pit/pitIntent.js +92 -0
- package/src/simulation/pit/pitOccupancy.js +47 -0
- package/src/simulation/pit/pitPenaltyService.js +71 -0
- package/src/simulation/pit/pitQueue.js +55 -0
- package/src/simulation/pit/pitRouteMovement.js +160 -0
- package/src/simulation/pit/pitRouting.js +319 -0
- package/src/simulation/pit/pitService.js +41 -0
- package/src/simulation/pit/pitServiceConstants.js +21 -0
- package/src/simulation/pit/pitServiceProfile.js +45 -0
- package/src/simulation/pit/pitSnapshots.js +61 -0
- package/src/simulation/pit/pitState.js +113 -0
- package/src/simulation/pit/pitTireService.js +92 -0
- package/src/simulation/race/classification.js +104 -0
- package/src/simulation/race/gridPenalties.js +37 -0
- package/src/simulation/race/raceControlState.js +38 -0
- package/src/simulation/race/raceDistance.js +32 -0
- package/src/simulation/race/raceFinish.js +65 -0
- package/src/simulation/race/raceLifecycle.js +71 -0
- package/src/simulation/race/raceOrder.js +113 -0
- package/src/simulation/race/raceProgress.js +124 -0
- package/src/simulation/race/raceSetup.js +93 -0
- package/src/simulation/race/raceStep.js +80 -0
- package/src/simulation/race/redFlag.js +15 -0
- package/src/simulation/race/retirements.js +44 -0
- package/src/simulation/race/safetyCar.js +86 -0
- package/src/simulation/race/stalledDnf.js +86 -0
- package/src/simulation/raceSimulation.js +230 -2817
- package/src/simulation/replay/replayGhosts.js +128 -0
- package/src/simulation/rules/moduleConfig.js +72 -0
- package/src/simulation/rules/penaltyConfig.js +99 -0
- package/src/simulation/rules/penaltyEvents.js +18 -0
- package/src/simulation/rules/penaltyStats.js +26 -0
- package/src/simulation/rules/ruleConfigMerge.js +41 -0
- package/src/simulation/rules/ruleDefaults.js +85 -0
- package/src/simulation/rules/rulesReview.js +141 -0
- package/src/simulation/rules/rulesetPresets.js +47 -0
- package/src/simulation/rules/trackLimitsSteward.js +1 -1
- package/src/simulation/rulesConfig.js +5 -323
- package/src/simulation/snapshots/raceSnapshots.js +192 -0
- package/src/simulation/timing/drsTiming.js +87 -0
- package/src/simulation/timing/gapEstimation.js +38 -0
- package/src/simulation/timing/lapTelemetry.js +182 -0
- package/src/simulation/timing/raceTiming.js +15 -0
- package/src/simulation/timing/sectorPerformance.js +60 -0
- package/src/simulation/timing/timingConstants.js +5 -0
- package/src/simulation/timing/timingHistory.js +46 -0
- package/src/simulation/timing/timingLines.js +79 -0
- package/src/simulation/track/centerlineSpline.js +59 -0
- package/src/simulation/track/drsZones.js +67 -0
- package/src/simulation/track/pitLaneAccess.js +103 -0
- package/src/simulation/track/pitLaneLayout.js +395 -0
- package/src/simulation/track/pitLaneState.js +160 -0
- package/src/simulation/track/proceduralCenterline.js +237 -0
- package/src/simulation/track/sampleGeometry.js +49 -0
- package/src/simulation/track/spatialQueries.js +154 -0
- package/src/simulation/track/startStraight.js +141 -0
- package/src/simulation/track/trackConstants.js +109 -0
- package/src/simulation/track/trackGenerationOptions.js +283 -0
- package/src/simulation/track/trackMath.js +241 -0
- package/src/simulation/track/trackModel.js +240 -0
- package/src/simulation/track/trackQueryIndex.js +945 -0
- package/src/simulation/track/trackSectors.js +19 -0
- package/src/simulation/track/trackStatePolicy.js +24 -0
- package/src/simulation/track/trackValidation.js +98 -0
- package/src/simulation/trackModel.js +1 -1706
- package/src/simulation/vehicle/contactResolution.js +116 -0
- package/src/simulation/vehicle/mainTrackWheelSurface.js +82 -0
- package/src/simulation/vehicle/pitWheelSurface.js +69 -0
- package/src/simulation/vehicle/runoffResponse.js +88 -0
- package/src/simulation/vehicle/surfacePriority.js +28 -0
- package/src/simulation/vehicle/vehicleGeometry.js +247 -0
- package/src/simulation/vehicle/vehiclePhysics.js +363 -0
- package/src/simulation/vehicle/vehicleSnapshots.js +316 -0
- package/src/simulation/vehicle/vehicleState.js +186 -0
- package/src/simulation/vehicle/wheelSurface.js +150 -0
- package/src/simulation/vehicleGeometry.js +1 -247
- package/src/simulation/vehiclePhysics.js +1 -136
- package/src/simulation/warmup/runtimeWarmup.js +224 -0
- package/src/simulation/wheelSurface.js +1 -338
- package/src/styles.css +42 -0
- package/src/ui/cameraControlsTemplate.js +20 -0
- package/src/ui/carOverviewTemplate.js +33 -0
- package/src/ui/componentTemplates.js +14 -360
- package/src/ui/raceCanvasTemplate.js +69 -0
- package/src/ui/raceControlsTemplate.js +36 -0
- package/src/ui/shellTemplate.js +5 -4
- package/src/ui/telemetryDrawerTemplate.js +55 -0
- package/src/ui/telemetryTemplates.js +136 -0
- package/src/ui/timingTowerTemplate.js +34 -0
package/docs/training.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
PaddockJS does not train models for you. It provides a simulator environment contract so users can train with any toolchain, then run the resulting policy in the visual simulator.
|
|
4
4
|
|
|
5
|
+
If you already have a trained model and need to connect it to PaddockJS, see [Custom Model Controller Guide](custom_model_controller.md).
|
|
6
|
+
|
|
5
7
|
## Boundary
|
|
6
8
|
|
|
7
9
|
PaddockJS owns:
|
|
@@ -29,69 +31,169 @@ Users own:
|
|
|
29
31
|
The current expert API is a JavaScript environment contract. It supports:
|
|
30
32
|
|
|
31
33
|
- `createPaddockEnvironment()` from `@inventure71/paddockjs/environment`
|
|
34
|
+
- `createPaddockDriverControllerLoop()` for shared browser/headless controller orchestration
|
|
32
35
|
- explicit `controlledDrivers`
|
|
33
36
|
- normalized actions: `steering`, `throttle`, `brake`, optional `pitIntent`, and optional `pitCompound`
|
|
34
37
|
- manual stepping with optional `frameSkip`
|
|
35
|
-
- object observations in real units plus a versioned numeric vector and schema, including rays, nearby cars, track lookahead/curvature, pit-lane surface, pit target compound, pit service state, pit-lane open state, and red-flag state
|
|
38
|
+
- object observations in real units plus a versioned numeric vector and schema, including rays, nearby cars, track lookahead/curvature, local physical driver senses, pit-lane surface, pit target compound, pit service state, pit-lane open state, and red-flag state
|
|
39
|
+
- `observation.object.self.appliedControls`, which reports the normalized steering, throttle, and brake controls that actually drove the latest physics step
|
|
40
|
+
- compact observation output modes for vector-only or object-only training loops
|
|
36
41
|
- full simulator state under `result.state.snapshot`
|
|
37
42
|
- global and per-controlled-driver events
|
|
38
43
|
- optional host-owned `reward(context)` callbacks, or no reward
|
|
39
44
|
- `getActionSpec()` and `getObservationSpec()`
|
|
40
45
|
- scenarios with `participants: 'all'`, `'controlled-only'`, or an explicit driver-id list
|
|
41
|
-
- reset-only scenario placement through `scenario.preset`, `scenario.placements`,
|
|
46
|
+
- reset-only scenario placement through `scenario.preset`, `scenario.placements`, `scenario.traffic`, and episode-boundary `resetDrivers(placements)`
|
|
47
|
+
- multi-car interaction profiles through `participantInteractions`, including no-collision training cars that are hidden from other cars' ray and nearby-car sensors by default
|
|
48
|
+
- per-driver episode state and neutral step metrics for batched training loops
|
|
42
49
|
- neutral rollout recording through `createRolloutRecorder()`
|
|
43
50
|
- deterministic evaluation metrics through `runEnvironmentEvaluation()` and `createEvaluationTracker()`
|
|
44
51
|
- a JSON-serializable worker protocol wrapper through `createEnvironmentWorkerProtocol()`
|
|
52
|
+
- optional frame observers through `externalRenderer` for transport-owned visual streaming (`{ snapshot, observation, meta }`)
|
|
45
53
|
- built-in AI for non-controlled cars
|
|
46
54
|
- browser expert mode through `mountF1Simulator(..., { expert })`
|
|
47
55
|
- opt-in browser ray visualization with `expert.visualizeSensors`
|
|
48
56
|
|
|
49
57
|
The environment also accepts `rules` as a narrow override of the existing race rules, such as `standingStart: false` for training loops. Rule overrides change simulator behavior; keep them fixed when comparing policy runs.
|
|
50
58
|
|
|
59
|
+
`reward(context)` is a user-owned formula over package-owned facts. The context includes the same neutral `metrics` and per-driver `episode` state returned by each step result, so training code does not need to re-derive legality, destruction, or episode termination from raw snapshots:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const env = createPaddockEnvironment({
|
|
63
|
+
drivers,
|
|
64
|
+
entries,
|
|
65
|
+
controlledDrivers: ['budget'],
|
|
66
|
+
reward({ metrics, episode }) {
|
|
67
|
+
if (metrics.destroyed) return -200;
|
|
68
|
+
if (metrics.offTrack) return -12;
|
|
69
|
+
if (episode.terminated) return 0;
|
|
70
|
+
return metrics.legalProgressDeltaMeters;
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
PaddockJS does not define an official reward. It exposes neutral simulator facts; users choose the objective.
|
|
76
|
+
|
|
51
77
|
## Deferred
|
|
52
78
|
|
|
53
79
|
These are not part of the current package API:
|
|
54
80
|
|
|
55
81
|
- Python Gymnasium wrapper
|
|
56
|
-
- static obstacle
|
|
82
|
+
- static obstacle scenario modes
|
|
57
83
|
- debug mutation APIs for arbitrary simulator state
|
|
58
84
|
- assisted-control modes that blend model output with built-in AI
|
|
59
85
|
- model loading, model storage, model registries, or trained policies
|
|
60
86
|
|
|
61
87
|
Those features need separate design work because they change scenario ownership, reproducibility, and the safety boundary between user experiments and package internals. The worker protocol is intentionally only a message wrapper around the JavaScript environment; it is not a Gymnasium package and does not choose a Python RL stack.
|
|
62
88
|
|
|
63
|
-
##
|
|
89
|
+
## Controller Shape
|
|
64
90
|
|
|
65
|
-
Use
|
|
91
|
+
Use a user-owned controller for browser playback and JavaScript training-style loops:
|
|
66
92
|
|
|
67
93
|
```js
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
94
|
+
import {
|
|
95
|
+
createPaddockDriverControllerLoop,
|
|
96
|
+
} from '@inventure71/paddockjs';
|
|
97
|
+
|
|
98
|
+
const controller = {
|
|
99
|
+
async init(ctx) {
|
|
100
|
+
this.model = await loadUserModel(ctx.observationSpec);
|
|
101
|
+
},
|
|
102
|
+
async reset(ctx) {
|
|
103
|
+
this.hidden = resetHidden(ctx.controlledDrivers.length);
|
|
104
|
+
},
|
|
105
|
+
async decideBatch(ctx) {
|
|
106
|
+
const vectors = ctx.orderedObservations.map((item) => item.vector);
|
|
107
|
+
const outputs = await this.model.predictBatch(vectors, this.hidden);
|
|
108
|
+
return Object.fromEntries(ctx.controlledDrivers.map((driverId, index) => [
|
|
109
|
+
driverId,
|
|
110
|
+
{
|
|
111
|
+
steering: outputs[index].steering,
|
|
112
|
+
throttle: Math.max(outputs[index].accel, 0),
|
|
113
|
+
brake: Math.max(-outputs[index].accel, 0),
|
|
114
|
+
},
|
|
115
|
+
]));
|
|
116
|
+
},
|
|
117
|
+
onStep(ctx) {
|
|
118
|
+
recordUserOwnedRewardOrStats(ctx.metrics, ctx.actions);
|
|
75
119
|
},
|
|
76
120
|
};
|
|
121
|
+
|
|
122
|
+
const loop = createPaddockDriverControllerLoop({
|
|
123
|
+
runtime: env,
|
|
124
|
+
controller,
|
|
125
|
+
actionRepeat: 4,
|
|
126
|
+
});
|
|
77
127
|
```
|
|
78
128
|
|
|
79
|
-
`
|
|
129
|
+
`decideBatch()` receives all controlled drivers in a stable order and returns one normalized action map:
|
|
80
130
|
|
|
81
|
-
- `steering`: `-1`
|
|
131
|
+
- `steering`: absolute steering target, where `-1` is maximum left, `0` is centered, `1` is maximum right, and values between are percentages of the maximum steering angle
|
|
82
132
|
- `throttle`: `0` to `1`
|
|
83
133
|
- `brake`: `0` to `1`
|
|
84
134
|
- `pitIntent`: optional `0`, `1`, or `2`; `0` means no pit request and is accepted as a no-op even when pit stops are disabled, `1` means keep trying until a free-enough pit-entry window appears, and `2` means commit to entering at the next pit-entry window even if pit-lane capacity or gap checks would block mode `1`
|
|
85
135
|
- `pitCompound`: optional tire target such as `'S'`, `'M'`, or `'H'`; it must be one of `rules.modules.tireStrategy.compounds`
|
|
86
136
|
|
|
87
|
-
|
|
137
|
+
`steering`, `throttle`, and `brake` are required for every controlled-driver action. Missing or non-finite vehicle-control fields fail validation instead of silently defaulting to zero, so broken model output does not become a hidden coast or brake command.
|
|
138
|
+
|
|
139
|
+
The controller owns model loading, hidden state, reward/stat logging, and any checkpoint format. PaddockJS owns only cached action/observation specs, compact observation delivery, action validation, batching order, optional action repeat, and normal physics stepping. A one-car `policy.predict(observation)` function can still be wrapped inside `decideBatch()`, but package examples should prefer the batched controller shape.
|
|
140
|
+
|
|
141
|
+
In `onStep(ctx)`, `ctx.actions` is the action map used for the current physics step and `ctx.previousActions` is the last action map actually applied before it. The first frame after reset has no previous per-driver action. Scheduled `loop.start()` playback may begin from a fresh loop without an explicit `reset()`, and controller/runtime errors stop playback while leaving the thrown value in `loop.stats.lastError`.
|
|
142
|
+
|
|
143
|
+
Controlled drivers do not receive tire-threshold automatic pit calls from the built-in strategy. A model must request a stop with `pitIntent`, and may choose the tire with `pitCompound`; then the simulator owns the pit entry, queue, service, penalty hold, tire change, and pit exit sequence until `pitStopStatus` returns to `completed`. For deterministic training, keep `rules.modules.pitStops.variability.enabled` false or set `rules.modules.pitStops.variability.perfect: true`; when variability is enabled without `perfect`, team `pitCrew` speed, consistency, and reliability affect service time from the same seeded simulation RNG. For single-car driving skill work where tyre management is out of scope, set `rules.modules.tireDegradation.enabled: false` so `tireEnergy` stays fixed and the model does not learn a hidden degradation schedule.
|
|
88
144
|
|
|
89
145
|
`observation.object.self.onTrack` follows the simulator's wheel-level legality rules: track, kerb, and legal pit-lane/box surfaces are on-track for reward/observation purposes, while gravel, grass, and barrier surfaces are off-track. Invalid `observation.lookaheadMeters` values fall back to `[20, 50, 100, 150]` so fixed observation schemas stay usable.
|
|
90
146
|
|
|
147
|
+
Use `observation.profile: 'physical-driver'` when training a policy that should rely on local driver-like senses instead of privileged future track data. This profile keeps the normal action contract, exposes yaw rate, local left/right boundary distance, four contact-patch surface readings, richer opponent radar fields, and surface-aware ray channels in the vector schema. Unless `lookaheadMeters` is explicitly provided, the physical-driver profile uses no track lookahead samples.
|
|
148
|
+
|
|
149
|
+
`observation.object.self.appliedControls` is diagnostic/reference data for the
|
|
150
|
+
latest physics step. For controlled cars it mirrors the normalized action after
|
|
151
|
+
environment validation. In `actionPolicy: 'report'` mode, stepping with a
|
|
152
|
+
missing or invalid vehicle action releases stale manual controls, lets the
|
|
153
|
+
built-in driver own that step, and exposes the exact normalized controls the
|
|
154
|
+
built-in driver applied. This is intended for audits or clean local imitation
|
|
155
|
+
datasets; it does not blend built-in AI with a model during normal controlled
|
|
156
|
+
stepping.
|
|
157
|
+
|
|
158
|
+
For model training, prefer `physicsMode: 'simulator'` so the policy learns against the same grip, yaw-rate, contact-patch, kerb, and runoff behavior exposed by those senses. Arcade physics can still be useful as a debugging baseline, but it should not be treated as the main training target for realistic driver policies.
|
|
159
|
+
|
|
160
|
+
Surface-aware rays are opt-in because they are more expensive than the default road-edge/car channels. The environment normalizes both the legacy compact ray shape and the newer per-ray layout:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const env = createPaddockEnvironment({
|
|
164
|
+
drivers,
|
|
165
|
+
controlledDrivers: ['budget'],
|
|
166
|
+
physicsMode: 'simulator',
|
|
167
|
+
observation: { profile: 'physical-driver' },
|
|
168
|
+
sensors: {
|
|
169
|
+
rays: {
|
|
170
|
+
layout: 'driver-front-heavy',
|
|
171
|
+
channels: ['roadEdge', 'kerb', 'illegalSurface', 'car'],
|
|
172
|
+
precision: 'driver',
|
|
173
|
+
rays: [
|
|
174
|
+
{ id: 'front', angleDegrees: 0, lengthMeters: 260 },
|
|
175
|
+
{ id: 'right', angleDegrees: 90, lengthMeters: 80 },
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Only requested ray channels are computed. The default compact config still computes the existing road-edge and car readings, so existing policy vectors keep their current shape unless the profile or sensor config opts into richer fields. Barrier walls are not a ray channel; they are visible track geometry and a terminal destruction condition in both physics modes. Active ray objects, vectors, schemas, and visualizations must not expose barrier ray fields.
|
|
183
|
+
|
|
184
|
+
`precision: 'driver'` is the default and the recommended model-facing ray contract. It returns the normal sampled driver-sensor distances without extra refinement. `precision: 'debug'` exists only for clearly labeled diagnostics with additional edge refinement. If the Policy Runner or expert visualization is showing model senses, it must render the active observation values exactly and must not replace them with debug-precision readings.
|
|
185
|
+
|
|
186
|
+
Track-position, wheel-surface, pit-lane, and ray fallback queries can be backed by an internal startup-built track query index. Browser/expert mounts, headless environments, and direct race-simulation construction now default to indexed queries and can opt out with `trackQueryIndex: false` for comparison runs. Compact vector-only, schema-free, no-state environment runs enable that index on the environment-owned simulation, and `batch-training` ray runs keep it enabled even when full state output is returned for inspection. Internal benchmarks can explicitly compare indexed and legacy-disabled modes. This does not change the observation contract; it makes repeated `nearestTrackState()` style queries use small spatial candidate sets instead of scanning the whole track when the indexed path is active. Rays also use indexed boundary intersections for nearby off-track recovery cases, so the same driver-facing sensor values stay fast without switching to a debug-only precision contract.
|
|
187
|
+
|
|
188
|
+
`resetDrivers()` is part of episode control, not policy action. After reset placement, the environment immediately reclassifies the selected cars against the same runoff/barrier rules used during stepping before building observations. Recovery starts inside the legal recovery band remain normal physical starts. Placements that are already inside the barrier destruction boundary return a terminal per-driver episode state and miss-valued rays, so a batch loop can assign its own negative reward and reset that driver without paying for far-out ray geometry.
|
|
189
|
+
|
|
91
190
|
## Headless Training Loop
|
|
92
191
|
|
|
93
192
|
```js
|
|
94
|
-
import {
|
|
193
|
+
import {
|
|
194
|
+
createPaddockDriverControllerLoop,
|
|
195
|
+
createPaddockEnvironment,
|
|
196
|
+
} from '@inventure71/paddockjs/environment';
|
|
95
197
|
|
|
96
198
|
const env = createPaddockEnvironment({
|
|
97
199
|
drivers,
|
|
@@ -104,14 +206,112 @@ const env = createPaddockEnvironment({
|
|
|
104
206
|
reward: myReward,
|
|
105
207
|
});
|
|
106
208
|
|
|
209
|
+
const loop = createPaddockDriverControllerLoop({
|
|
210
|
+
runtime: env,
|
|
211
|
+
controller,
|
|
212
|
+
actionRepeat: 4,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
let result = await loop.reset();
|
|
216
|
+
while (!result.done) result = await loop.step();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Multi-Car No-Collision Training
|
|
220
|
+
|
|
221
|
+
Use `participantInteractions` when you want several physics-driven cars in the same environment without having them collide or contaminate each other's sensors. For batched same-environment learning, use `batch-training`.
|
|
222
|
+
|
|
223
|
+
```js
|
|
224
|
+
import { createPaddockEnvironment } from '@inventure71/paddockjs/environment';
|
|
225
|
+
|
|
226
|
+
const env = createPaddockEnvironment({
|
|
227
|
+
drivers,
|
|
228
|
+
entries,
|
|
229
|
+
controlledDrivers: ['model-a', 'model-b'],
|
|
230
|
+
frameSkip: 4,
|
|
231
|
+
scenario: {
|
|
232
|
+
participants: ['model-a', 'model-b'],
|
|
233
|
+
},
|
|
234
|
+
participantInteractions: {
|
|
235
|
+
defaultProfile: 'batch-training',
|
|
236
|
+
},
|
|
237
|
+
observation: {
|
|
238
|
+
profile: 'physical-driver',
|
|
239
|
+
output: 'vector',
|
|
240
|
+
includeSchema: false,
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
107
244
|
let result = env.reset();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
245
|
+
result = env.step({
|
|
246
|
+
'model-a': policyA.predict(result.observation['model-a']),
|
|
247
|
+
'model-b': policyB.predict(result.observation['model-b']),
|
|
248
|
+
});
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
`batch-training` is still a real car mode, not a replay ghost and not a teleport path:
|
|
252
|
+
|
|
253
|
+
- the car remains in `result.state.snapshot.cars`
|
|
254
|
+
- the car still moves only through steering, throttle, brake, and optional pit intent
|
|
255
|
+
- the car still has tire, pit, timing, lap, and rules state
|
|
256
|
+
- the car is excluded from race order/classification by default
|
|
257
|
+
- the car does not collide with other cars
|
|
258
|
+
- the car does not block pit-lane service occupancy
|
|
259
|
+
- the car is hidden from other cars' ray sensors by default
|
|
260
|
+
- the car is hidden from other cars' `nearbyCars` observations by default
|
|
261
|
+
|
|
262
|
+
In the browser renderer, no-collision participants still look like solid cars, but get a blue non-collision outline marker. Replay ghosts remain visually separate: translucent trajectory overlays from `snapshot.replayGhosts`, not physics cars from `snapshot.cars`.
|
|
263
|
+
|
|
264
|
+
Sensor hiding is per target car. If `model-b` is `batch-training`, then `model-a`'s rays and `nearbyCars` ignore `model-b`. If every training car should be invisible to every other training car, set `participantInteractions.defaultProfile: 'batch-training'`.
|
|
265
|
+
|
|
266
|
+
Replay ghosts are also sensor-hidden by default. If a replay reference should be visible to policy sensors for comparison experiments, opt it in through its own `sensors.detectableByRays` or `sensors.detectableAsNearby` flags. Sensor results mark those targets as replay ghosts; they still do not collide, rank, pit, or receive controls.
|
|
267
|
+
|
|
268
|
+
If you need a non-colliding car that still appears in sensors, use `phantom-race` instead. If you need a one-off exception, override the flag explicitly:
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
participantInteractions: {
|
|
272
|
+
drivers: {
|
|
273
|
+
'model-b': {
|
|
274
|
+
profile: 'batch-training',
|
|
275
|
+
detectableByRays: true,
|
|
276
|
+
detectableAsNearby: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
112
279
|
}
|
|
113
280
|
```
|
|
114
281
|
|
|
282
|
+
Use that override deliberately. The default no-collision training profile is sensor-hidden because parallel training rollouts usually should not alter each other's observation tensors.
|
|
283
|
+
|
|
284
|
+
Compact vector mode is intended for high-throughput loops. `env.getObservationSpec()` remains the canonical schema source, so external code can request `output: 'vector'` and `includeSchema: false` without serializing object observations and schema data on every step. JavaScript training loops can also request `vectorType: 'float32'` for typed numeric buffers. Keep the default array output for JSON-only bridges unless the bridge explicitly packs typed arrays.
|
|
285
|
+
|
|
286
|
+
Use `result.stateOutput` to avoid returning more state than the loop needs:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
const env = createPaddockEnvironment({
|
|
290
|
+
drivers,
|
|
291
|
+
entries,
|
|
292
|
+
controlledDrivers: agentIds,
|
|
293
|
+
observation: { profile: 'physical-driver', output: 'vector', includeSchema: false },
|
|
294
|
+
result: {
|
|
295
|
+
stateOutput: 'none',
|
|
296
|
+
resetDriversObservationScope: 'reset',
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`stateOutput: 'minimal'` returns the lean public observation snapshot. `stateOutput: 'none'` returns `state: null`. The default is still `full` for existing callers. When `stateOutput: 'none'`, `observation.output: 'vector'`, and `includeSchema: false`, the environment uses an internal compact training snapshot before building observations and metrics. That optimization is not exposed as policy state, and reward callbacks still receive the documented reward context rather than a browser/render-only snapshot.
|
|
302
|
+
|
|
303
|
+
On the local 20-car batch-training benchmark used for this package work (`physicsMode: 'simulator'`, `frameSkip: 4`, `physical-driver`, `driver-front-heavy` rays), the measured environment action cost after compact output and indexed track queries was approximately:
|
|
304
|
+
|
|
305
|
+
```txt
|
|
306
|
+
nearest track states 1.52x faster than legacy-disabled
|
|
307
|
+
pit lane states 2.02x faster than legacy-disabled
|
|
308
|
+
sampled high-curvature off-track rays 14.81x faster than legacy-disabled
|
|
309
|
+
wheel surface near pit connector 1.12x faster than legacy-disabled
|
|
310
|
+
20-car batch recovery environment 1.03x faster than legacy-disabled
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
These numbers are hardware- and track-dependent, but the relative result is the useful guidance: compact state/observation output reduces serialization/allocation overhead, and indexed track queries prevent off-track recovery rays from becoming the dominant cost center.
|
|
314
|
+
|
|
115
315
|
## Scenario Reset Control
|
|
116
316
|
|
|
117
317
|
Scenario placement belongs to environment reset, not to model control. It is useful for deterministic training or evaluation starts such as cornering, recovery, overtaking packs, and pit entry:
|
|
@@ -146,6 +346,42 @@ const env = createPaddockEnvironment({
|
|
|
146
346
|
|
|
147
347
|
Supported fixed presets are `cornering`, `off-track-recovery`, `overtaking-pack`, and `pit-entry`. Explicit `placements` override preset placement for the same driver. `traffic` places cars relative to another placed or existing car. After reset, `step(actions)` still advances controlled cars only through the normal action contract.
|
|
148
348
|
|
|
349
|
+
For batched training, individual controlled cars can be reset between episodes without recreating the whole simulation:
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
env.resetDrivers({
|
|
353
|
+
'model-a': {
|
|
354
|
+
distanceMeters: 1200,
|
|
355
|
+
offsetMeters: 3,
|
|
356
|
+
speedKph: 80,
|
|
357
|
+
headingErrorRadians: 0.2,
|
|
358
|
+
},
|
|
359
|
+
}, {
|
|
360
|
+
observationScope: 'reset',
|
|
361
|
+
stateOutput: 'none',
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
`resetDrivers()` is an episode-boundary API. It increments that driver's `episodeId`, resets its `episodeStep`, clears manual controls and pit intent, and places the car through the same simulator state-reset path used by scenarios. It does not rewind global `info.step`; max-step truncation is evaluated from the selected driver's own `episodeStep`, so one truncated driver can be reset while the rest of the batch keeps its current episode state. It is not available through `step(actions)` and is not a movement shortcut during an episode. Passing `{ observationScope: 'reset', stateOutput: 'none' }` keeps reset responses small for batched runners.
|
|
366
|
+
|
|
367
|
+
The worker protocol mirrors this with a `resetDrivers` message:
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
protocol.handle({
|
|
371
|
+
id: 'reset-agent-3',
|
|
372
|
+
type: 'resetDrivers',
|
|
373
|
+
placements: {
|
|
374
|
+
'agent-3': { distanceMeters: 1200, offsetMeters: 0, speedKph: 80 },
|
|
375
|
+
},
|
|
376
|
+
resultOptions: {
|
|
377
|
+
observationScope: 'reset',
|
|
378
|
+
stateOutput: 'none',
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
For state polling, worker `getState` messages accept `stateOptions: { output: 'full' | 'minimal' | 'none' }`; `resultOptions.stateOutput` is accepted as a compatibility alias on that message type.
|
|
384
|
+
|
|
149
385
|
## Rollouts And Evaluation
|
|
150
386
|
|
|
151
387
|
PaddockJS can export neutral transition data without owning a trainer:
|
|
@@ -155,14 +391,16 @@ import { createRolloutRecorder } from '@inventure71/paddockjs/environment';
|
|
|
155
391
|
|
|
156
392
|
const recorder = createRolloutRecorder();
|
|
157
393
|
let result = env.reset();
|
|
158
|
-
const action = { budget:
|
|
394
|
+
const action = { budget: { steering: 0, throttle: 1, brake: 0 } };
|
|
159
395
|
const next = env.step(action);
|
|
160
396
|
recorder.recordStep(result, action, next);
|
|
161
397
|
```
|
|
162
398
|
|
|
163
399
|
Each transition has `{ observation, action, reward, nextObservation, terminated, truncated, info }`. If the environment has no reward callback, `reward` is still `null`; the recorder does not invent one.
|
|
164
400
|
|
|
165
|
-
|
|
401
|
+
Environment step results also include reward-neutral `metrics[driverId]` facts such as progress delta, legal progress delta, kerb use, illegal/off-track state, severe cuts, destruction state, under-30-kph state, spin/backwards/instability state, lap completion, lap time, and contact count. `spinOrBackwards` is true for large heading-error backwards cases and for active simulator instability labels such as `spin-risk`, `understeer`, `oversteer`, and `destroyed`. `contactCount` counts each normalized physical contact event once per involved driver even when the event carries both legacy `carId`/`otherCarId` fields and normalized `driverIds`. These metrics are facts for logging, evaluation, and user-defined reward functions; PaddockJS does not turn them into a built-in objective. If a car touches the rendered barrier wall's inner face in either physics mode, that driver receives `metrics[driverId].destroyed: true` and `info.drivers[driverId].endReason: 'destroyed'`; external training code should assign any super-negative crash reward itself and then call `resetDrivers()` for the next episode. If opt-in `rules.modules.stalledDnf.enabled: true` retires a controlled car for being stuck off legal surfaces, the episode terminates with `endReason: 'stalled-off-track'` while `metrics[driverId].destroyed` stays `false`.
|
|
402
|
+
|
|
403
|
+
Deterministic evaluation helpers report simulator quality metrics such as distance, off-track steps, contacts, recovery success, pass count, and first lap time when available. They use the same wheel-level legal-surface and contact-event semantics as environment metrics. `runEnvironmentEvaluation()` can accept compact base options with `result.stateOutput: 'none'`; it internally requests the minimal snapshot needed for evaluation bookkeeping. Direct `createEvaluationTracker(result)` calls require a result with `state.snapshot`, so use `stateOutput: 'minimal'` or `full` for manual tracker wiring. Evaluation helpers do not update a model or compute rewards.
|
|
166
404
|
|
|
167
405
|
## Example Rewards
|
|
168
406
|
|
|
@@ -171,7 +409,10 @@ Deterministic evaluation helpers report simulator quality metrics such as distan
|
|
|
171
409
|
## Visual Playback Loop
|
|
172
410
|
|
|
173
411
|
```js
|
|
174
|
-
import {
|
|
412
|
+
import {
|
|
413
|
+
createPaddockDriverControllerLoop,
|
|
414
|
+
mountF1Simulator,
|
|
415
|
+
} from '@inventure71/paddockjs';
|
|
175
416
|
|
|
176
417
|
const simulator = await mountF1Simulator(root, {
|
|
177
418
|
drivers,
|
|
@@ -184,14 +425,15 @@ const simulator = await mountF1Simulator(root, {
|
|
|
184
425
|
},
|
|
185
426
|
});
|
|
186
427
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
428
|
+
const loop = createPaddockDriverControllerLoop({
|
|
429
|
+
runtime: simulator.expert,
|
|
430
|
+
controller,
|
|
431
|
+
actionRepeat: 4,
|
|
432
|
+
mode: 'browser',
|
|
433
|
+
});
|
|
193
434
|
|
|
194
|
-
|
|
435
|
+
await loop.reset();
|
|
436
|
+
loop.start();
|
|
195
437
|
```
|
|
196
438
|
|
|
197
439
|
## Specs
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Python Base Policy Server Example
|
|
2
|
+
|
|
3
|
+
This folder provides a neutral extension point for browser Policy Runner integrations.
|
|
4
|
+
|
|
5
|
+
It is not a trainer and not a model/checkpoint format owner. It only shows transport contracts:
|
|
6
|
+
|
|
7
|
+
- HTTP inference stubs
|
|
8
|
+
- `POST /policy/reset`
|
|
9
|
+
- `POST /policy/reset-state`
|
|
10
|
+
- `POST /policy/decide-batch`
|
|
11
|
+
- Preview WebSocket stream
|
|
12
|
+
- `ws://127.0.0.1:8787/preview`
|
|
13
|
+
- packet shape: `{ type: "preview:snapshot", snapshot, observation, meta }`
|
|
14
|
+
- Preview latest-frame HTTP endpoint (for polling visualizers)
|
|
15
|
+
- `http://127.0.0.1:8787/preview/frame`
|
|
16
|
+
|
|
17
|
+
## Run
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install -r examples/python/requirements.txt
|
|
21
|
+
python examples/python/base_policy_server.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then in Policy Runner:
|
|
25
|
+
|
|
26
|
+
For browser-owned stepping:
|
|
27
|
+
|
|
28
|
+
1. Select controller `Policy server`
|
|
29
|
+
2. Use `http://127.0.0.1:8787`
|
|
30
|
+
|
|
31
|
+
For external render-only preview:
|
|
32
|
+
|
|
33
|
+
1. Select controller `Live preview stream`
|
|
34
|
+
2. Use either:
|
|
35
|
+
- `ws://127.0.0.1:8787/preview` (WebSocket push)
|
|
36
|
+
- `http://127.0.0.1:8787/preview/frame` (HTTP polling)
|
|
37
|
+
3. Connect
|
|
38
|
+
|
|
39
|
+
The base server enables CORS for local browser previews so `policy-runner.html`
|
|
40
|
+
can call it from the Vite preview origin.
|
|
41
|
+
|
|
42
|
+
Direct launch URL example (when local preview runs on `5174`):
|
|
43
|
+
|
|
44
|
+
`http://127.0.0.1:5174/policy-runner.html?controller=live-node-view&liveUrl=ws://127.0.0.1:8787/preview`
|
|
45
|
+
|
|
46
|
+
## Extend
|
|
47
|
+
|
|
48
|
+
Subclass `BasePolicyServer` and override:
|
|
49
|
+
|
|
50
|
+
- `init_policy(ctx)`
|
|
51
|
+
- `reset_policy(ctx)`
|
|
52
|
+
- `reset_policy_state(driver_ids)`
|
|
53
|
+
- `decide_batch(ctx)`
|
|
54
|
+
- `publish_preview_frame(frame)` (optional)
|
|
55
|
+
|
|
56
|
+
Use `broadcast_preview_frame(snapshot, observation, meta)` to push authoritative frames to the browser renderer.
|
|
57
|
+
|
|
58
|
+
## Important Boundary
|
|
59
|
+
|
|
60
|
+
- This server does not step the simulator by itself.
|
|
61
|
+
- Your training/runtime bridge (Python orchestration + JS environment) must provide authoritative frames by calling `broadcast_preview_frame(...)` after environment `reset/step/resetDrivers`.
|
|
62
|
+
- In browser `Live preview stream`, the simulator is render-only while attached; local browser stepping controls are intentionally disabled.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Neutral PaddockJS policy server base example.
|
|
3
|
+
|
|
4
|
+
This file is intentionally minimal and transport-focused:
|
|
5
|
+
- HTTP policy endpoints:
|
|
6
|
+
- POST /policy/reset
|
|
7
|
+
- POST /policy/reset-state
|
|
8
|
+
- POST /policy/decide-batch
|
|
9
|
+
- WebSocket preview endpoint:
|
|
10
|
+
- /preview (broadcasts {snapshot, observation, meta})
|
|
11
|
+
|
|
12
|
+
Extend `BasePolicyServer` and override hook methods to plug your own model/runtime.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional
|
|
21
|
+
|
|
22
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
23
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
24
|
+
from fastapi.responses import JSONResponse
|
|
25
|
+
import uvicorn
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _zero_action() -> Dict[str, float]:
|
|
29
|
+
return {
|
|
30
|
+
"steering": 0.0,
|
|
31
|
+
"throttle": 0.0,
|
|
32
|
+
"brake": 0.0,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ServerState:
|
|
38
|
+
session_id: int = 0
|
|
39
|
+
controlled_drivers: List[str] = field(default_factory=list)
|
|
40
|
+
connections: List[WebSocket] = field(default_factory=list)
|
|
41
|
+
latest_preview_frame: Optional[Dict[str, Any]] = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BasePolicyServer:
|
|
45
|
+
"""Base extension surface for custom policy/runtime integrations."""
|
|
46
|
+
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self.app = FastAPI(title="PaddockJS Base Policy Server")
|
|
49
|
+
self.app.add_middleware(
|
|
50
|
+
CORSMiddleware,
|
|
51
|
+
allow_origins=["*"],
|
|
52
|
+
allow_methods=["*"],
|
|
53
|
+
allow_headers=["*"],
|
|
54
|
+
)
|
|
55
|
+
self.state = ServerState()
|
|
56
|
+
self._broadcast_lock = asyncio.Lock()
|
|
57
|
+
self._register_routes()
|
|
58
|
+
|
|
59
|
+
# ----- Extension hooks -------------------------------------------------
|
|
60
|
+
|
|
61
|
+
async def init_policy(self, context: Mapping[str, Any]) -> None:
|
|
62
|
+
"""Initialize policy/runtime resources on full reset."""
|
|
63
|
+
|
|
64
|
+
async def reset_policy(self, context: Mapping[str, Any]) -> None:
|
|
65
|
+
"""Reset all policy runtime state for a new run/session."""
|
|
66
|
+
|
|
67
|
+
async def reset_policy_state(self, driver_ids: Iterable[str]) -> None:
|
|
68
|
+
"""Reset only selected driver-local model state."""
|
|
69
|
+
|
|
70
|
+
async def decide_batch(self, context: Mapping[str, Any]) -> Dict[str, Dict[str, float]]:
|
|
71
|
+
"""Return per-driver controls for one batched policy step."""
|
|
72
|
+
driver_ids = list(context.get("driverIds") or [])
|
|
73
|
+
return {driver_id: _zero_action() for driver_id in driver_ids}
|
|
74
|
+
|
|
75
|
+
async def publish_preview_frame(self, frame: Mapping[str, Any]) -> None:
|
|
76
|
+
"""Optional extension hook called before broadcasting preview frames."""
|
|
77
|
+
|
|
78
|
+
# ----- Public helpers --------------------------------------------------
|
|
79
|
+
|
|
80
|
+
async def broadcast_preview_frame(
|
|
81
|
+
self,
|
|
82
|
+
snapshot: Mapping[str, Any],
|
|
83
|
+
observation: Mapping[str, Any],
|
|
84
|
+
meta: Optional[Mapping[str, Any]] = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Broadcast one authoritative preview frame to websocket clients."""
|
|
87
|
+
frame = {
|
|
88
|
+
"type": "preview:snapshot",
|
|
89
|
+
"snapshot": dict(snapshot),
|
|
90
|
+
"observation": dict(observation),
|
|
91
|
+
"meta": dict(meta or {}),
|
|
92
|
+
}
|
|
93
|
+
self.state.latest_preview_frame = frame
|
|
94
|
+
await self.publish_preview_frame(frame)
|
|
95
|
+
payload = frame
|
|
96
|
+
async with self._broadcast_lock:
|
|
97
|
+
stale: List[WebSocket] = []
|
|
98
|
+
for ws in self.state.connections:
|
|
99
|
+
try:
|
|
100
|
+
await ws.send_json(payload)
|
|
101
|
+
except Exception:
|
|
102
|
+
stale.append(ws)
|
|
103
|
+
if stale:
|
|
104
|
+
self.state.connections = [ws for ws in self.state.connections if ws not in stale]
|
|
105
|
+
|
|
106
|
+
# ----- Internal routing ------------------------------------------------
|
|
107
|
+
|
|
108
|
+
def _register_routes(self) -> None:
|
|
109
|
+
@self.app.get("/preview/frame")
|
|
110
|
+
async def preview_frame() -> JSONResponse:
|
|
111
|
+
if self.state.latest_preview_frame:
|
|
112
|
+
return JSONResponse(self.state.latest_preview_frame)
|
|
113
|
+
return JSONResponse(
|
|
114
|
+
{
|
|
115
|
+
"type": "preview:status",
|
|
116
|
+
"status": "waiting-for-frame",
|
|
117
|
+
"session": {"id": self.state.session_id},
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@self.app.post("/policy/reset")
|
|
122
|
+
async def policy_reset(body: MutableMapping[str, Any]) -> JSONResponse:
|
|
123
|
+
self.state.session_id += 1
|
|
124
|
+
self.state.controlled_drivers = list(body.get("driverIds") or [])
|
|
125
|
+
await self.init_policy(body)
|
|
126
|
+
await self.reset_policy(body)
|
|
127
|
+
return JSONResponse(
|
|
128
|
+
{
|
|
129
|
+
"ok": True,
|
|
130
|
+
"session": {
|
|
131
|
+
"id": self.state.session_id,
|
|
132
|
+
"controlledDrivers": self.state.controlled_drivers,
|
|
133
|
+
},
|
|
134
|
+
}
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@self.app.post("/policy/reset-state")
|
|
138
|
+
async def policy_reset_state(body: MutableMapping[str, Any]) -> JSONResponse:
|
|
139
|
+
driver_ids = list(body.get("driverIds") or [])
|
|
140
|
+
await self.reset_policy_state(driver_ids)
|
|
141
|
+
return JSONResponse(
|
|
142
|
+
{
|
|
143
|
+
"ok": True,
|
|
144
|
+
"session": {"id": self.state.session_id},
|
|
145
|
+
"resetDrivers": driver_ids,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
@self.app.post("/policy/decide-batch")
|
|
150
|
+
async def policy_decide_batch(body: MutableMapping[str, Any]) -> JSONResponse:
|
|
151
|
+
actions = await self.decide_batch(body)
|
|
152
|
+
if not isinstance(actions, dict):
|
|
153
|
+
actions = {}
|
|
154
|
+
now_ms = int(time.time() * 1000)
|
|
155
|
+
return JSONResponse(
|
|
156
|
+
{
|
|
157
|
+
"ok": True,
|
|
158
|
+
"session": {"id": self.state.session_id},
|
|
159
|
+
"actions": actions,
|
|
160
|
+
"meta": {"serverTsMs": now_ms},
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@self.app.websocket("/preview")
|
|
165
|
+
async def preview_stream(ws: WebSocket) -> None:
|
|
166
|
+
await ws.accept()
|
|
167
|
+
self.state.connections.append(ws)
|
|
168
|
+
try:
|
|
169
|
+
await ws.send_json(
|
|
170
|
+
{
|
|
171
|
+
"type": "preview:status",
|
|
172
|
+
"status": "connected",
|
|
173
|
+
"session": {"id": self.state.session_id},
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
while True:
|
|
177
|
+
# Keep connection alive; ignore client messages.
|
|
178
|
+
await ws.receive_text()
|
|
179
|
+
except WebSocketDisconnect:
|
|
180
|
+
pass
|
|
181
|
+
finally:
|
|
182
|
+
self.state.connections = [conn for conn in self.state.connections if conn is not ws]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run(host: str = "127.0.0.1", port: int = 8787) -> None:
|
|
186
|
+
server = BasePolicyServer()
|
|
187
|
+
uvicorn.run(server.app, host=host, port=port, log_level="info")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
run()
|