mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-11-14 10:42:45 -05:00
slop
This commit is contained in:
@@ -2,27 +2,21 @@
|
||||
|
||||
## Resume here
|
||||
|
||||
Latest change: format 3 command-event policy replaces format 2 continuous wheel
|
||||
speeds. All supported controls, including drive and recorded zero-speed stops,
|
||||
use scored command templates and the same threshold/checked command pipe. The
|
||||
network produces scores and numeric values; it never generates JSON text.
|
||||
Latest change (2026-09-29 IPI): format 4 seeds known user-control command shapes
|
||||
before training. Generic peripherals are excluded; sensorStream is ignored without
|
||||
invalidating footage. Drive includes zero-speed drive, with no separate stop type.
|
||||
Drive, camera, brushes/vacuum, headlight, laser, horn, songs and operating modes
|
||||
remain supported. Raw recordings and the existing checked command pipe stay intact.
|
||||
|
||||
Implemented: remove wheel-speed telemetry and previous drive-command history
|
||||
from inputs (raw recordings unchanged); preserve exact numeric zeros; reuse
|
||||
recordings with a fresh resume-v4.pt / schedule-v4.json lineage. Startup deletes recognized older-format snapshots/checkpoints and old training
|
||||
lineages; recordings and current-format models are preserved. One ongoing learner, gated
|
||||
Latest/Previous, and admin-created permanent snapshots remain.
|
||||
Held-out data cannot expand the schema. Unknown control variants exclude nearby
|
||||
windows only, with command/count/reason diagnostics and a coverage gate that blocks
|
||||
promotion. Supported portions still receive evaluation. Current training lineage
|
||||
is resume-v5.pt / schedule-v5.json. Startup removes obsolete format 1-3 models and
|
||||
old lineages through v4; recordings and current-format models are preserved.
|
||||
|
||||
Main card shows decoded command parameters, score, applied threshold marker, and
|
||||
submission results, including below-threshold proposals. Diagnostics reportVersion
|
||||
3 includes command predictions/submissions and event evaluation. Manual inputs
|
||||
still never stop or pause autonomy. Disabled remains dormant/off by default.
|
||||
|
||||
Focused Node syntax, Python production worker entry points, and card ESLint
|
||||
passed. New policy: 803,440 parameters, 30 sensor fields, stateSize 700. No live
|
||||
recordings/camera/rover pipeline or authenticated browser were available locally.
|
||||
Actual training, stop behavior, concurrent performance and visual appearance
|
||||
remain unverified. No tests were added and no processes were left running.
|
||||
Verification for this increment is recorded at the end. Earlier version-specific
|
||||
notes are historical. Manual inputs never pause/cancel autonomy; disabled service
|
||||
remains dormant. Actual autonomous performance has not been established.
|
||||
|
||||
This document preserves the conversation's decisions across compactions and
|
||||
usage cutoffs. Keep the checklist and handoff section current during authorized
|
||||
@@ -930,3 +924,41 @@ Cleanup runs even when training is disabled, but not when the whole service is o
|
||||
Production deletion occurs on the next enabled-service startup after deployment.
|
||||
Verification: focused Node syntax and diff whitespace checks passed. No production
|
||||
storage was accessed; actual deletion awaits enabled-service startup on the server.
|
||||
|
||||
### User-control scope and evaluation coverage (2026-09-29)
|
||||
|
||||
Authorized by latest IPI after user clarified that controls include camera, brushes,
|
||||
lights, laser, horn and songs but NOT generic peripherals. No new command pipe or
|
||||
separate stop command. sensorStream is neither input/target nor a boundary. Actual
|
||||
reset/reboot/update remain temporal boundaries because they interrupt observations.
|
||||
|
||||
Format 4 seeds drive and motor zero/nonzero field combinations, servo, light/laser
|
||||
actions, the two four-frequency horn waveforms and horn release, OI start/safe/full/
|
||||
dock bytes and default song. Seed values describe shapes, not artificial training
|
||||
examples. Horn frequencies are numeric parameters; integral JSON numbers share
|
||||
shapes with equivalent floating representations for drive/motors/servo/horn. Songs
|
||||
remain exact demonstrated note arrays; other song/raw variants can extend the
|
||||
training schema within its existing 64-slot bound. No promise to pre-enumerate all
|
||||
possible tunes. Seeded slots receive negative examples even in sessions where that
|
||||
control was not used, avoiding untrained random seeded outputs.
|
||||
|
||||
Evaluation freezes the training schema. Unsupported commands are reported with
|
||||
payload, reason, occurrence count (up to 32 distinct descriptions), total count and
|
||||
affected-window count. The affected observation history/target interval is excluded
|
||||
without labeling the unknown command as inaction or skipping the entire recording.
|
||||
Any unsupported held-out command blocks promotion; metrics describe only supported
|
||||
windows. If none survive, dataset diagnostics still report the coverage problem.
|
||||
Existing permission checks, threshold mechanics, recording and manual input behavior
|
||||
are unchanged. Generic peripherals are also removed from execution allowlist and
|
||||
rover compatibility profile. Previous policy models are automatically cleaned up;
|
||||
no configuration edit is required.
|
||||
|
||||
Verification completed: train.py --check-dependencies loaded the production
|
||||
worker and constructed format 4 with 803,440 parameters and predefined command
|
||||
shapes. infer.py --help, Node syntax checks for the four changed service modules,
|
||||
focused VipRoverLearningCard ESLint and diff whitespace checks passed.
|
||||
No live server data/hardware/browser available locally. Actual training, driving,
|
||||
and browser appearance remain unverified. No new tests or background processes.
|
||||
Deploying starts a new learner using existing recordings; enabled-service startup
|
||||
removes older policy models and optimizer/scheduler lineages through v4. Current
|
||||
lineage is v5. No settings changes or prescribed human driving are required.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Match actuator configuration, not a rover's display name or changing telemetry.
|
||||
const { isDeepStrictEqual } = require('node:util');
|
||||
const POLICY_VERSION = 3;
|
||||
const POLICY_VERSION = 4;
|
||||
|
||||
function controlProfile(meta = {}) {
|
||||
return Object.fromEntries(['cameraServo', 'peripherals', 'headlight', 'laser', 'roomba', 'platform']
|
||||
return Object.fromEntries(['cameraServo', 'headlight', 'laser', 'roomba', 'platform']
|
||||
.map((key) => [key, meta?.[key] ?? null]));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ const { recordCommandRequest } = require('../commandService/recording');
|
||||
const { acquireModel } = require('./models');
|
||||
const { compatible } = require('./capabilities');
|
||||
|
||||
const OPERATING = new Set(['drive', 'motors', 'servo', 'headlight', 'laser', 'horn', 'peripheral', 'song', 'raw']);
|
||||
const OPERATING = new Set(['drive', 'motors', 'servo', 'headlight', 'laser', 'horn', 'song', 'raw']);
|
||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, Number(value) || 0));
|
||||
|
||||
function canControl(socket, roverId) {
|
||||
|
||||
@@ -142,8 +142,8 @@ function removeObsoleteModels(root) {
|
||||
else { await fs.rm(target, { recursive: true, force: true }); retired.delete(target); }
|
||||
}
|
||||
// These filenames belong to superseded optimizer/scheduler lineages, not
|
||||
// policy versions. The current command policy uses lineage v4.
|
||||
for (const suffix of ['', '-v1', '-v2', '-v3']) {
|
||||
// policy versions. The current command policy uses lineage v5.
|
||||
for (const suffix of ['', '-v1', '-v2', '-v3', '-v4']) {
|
||||
for (const filename of [`resume${suffix}.pt`, `resume${suffix}.tmp`,
|
||||
`schedule${suffix}.json`, `schedule${suffix}.json.tmp`]) {
|
||||
await fs.rm(path.join(root, 'training', filename), { force: true });
|
||||
|
||||
@@ -33,7 +33,7 @@ function createTrainer({ root, config, actionThreshold, logger }) {
|
||||
}
|
||||
}
|
||||
try {
|
||||
const saved = JSON.parse(await fs.readFile(path.join(trainingRoot, 'schedule-v4.json'), 'utf8'));
|
||||
const saved = JSON.parse(await fs.readFile(path.join(trainingRoot, 'schedule-v5.json'), 'utf8'));
|
||||
ledger = saved.sessions;
|
||||
state = { ...saved.progress, status: 'waiting' };
|
||||
if (!ledger || typeof ledger !== 'object') throw new Error('Invalid training schedule');
|
||||
@@ -46,7 +46,7 @@ function createTrainer({ root, config, actionThreshold, logger }) {
|
||||
}
|
||||
|
||||
async function saveLedger() {
|
||||
const filename = path.join(trainingRoot, 'schedule-v4.json');
|
||||
const filename = path.join(trainingRoot, 'schedule-v5.json');
|
||||
await fs.writeFile(`${filename}.tmp`, JSON.stringify({ sessions: ledger, progress: state }));
|
||||
await fs.rename(`${filename}.tmp`, filename);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ def load_events(directory):
|
||||
return metadata, sorted(events, key=lambda e: e['ts'])
|
||||
|
||||
|
||||
def prepare(directory, schema, stats):
|
||||
def prepare(directory, schema, stats, extend):
|
||||
metadata, events = load_events(directory)
|
||||
requests = {}
|
||||
accepted = set()
|
||||
@@ -72,14 +72,27 @@ def prepare(directory, schema, stats):
|
||||
if not sensors:
|
||||
raise ValueError('No sensor observations')
|
||||
candidate = copy.deepcopy(schema)
|
||||
history, targets = [], []
|
||||
history, targets, unsupported_times = [], [], []
|
||||
unsupported = {}
|
||||
stats.update({'sensorFrames': len(sensors), 'dispatches': len(dispatches),
|
||||
'failedCommands': len(failed), 'sensorPresentFraction': np.mean([row[1][len(SENSORS):] for row in sensors], axis=0).tolist()})
|
||||
for data in dispatches:
|
||||
command = data.get('command') or {}
|
||||
if command.get('type') in ('reset', 'reboot', 'update', 'sensorStream'):
|
||||
if command.get('type') in ('reset', 'reboot', 'update'):
|
||||
boundaries.append(data['ts'])
|
||||
encoded = encode_command(command, candidate, extend=True)
|
||||
try:
|
||||
encoded = encode_command(command, candidate, extend=extend)
|
||||
except ValueError as error:
|
||||
# Unrepresentable controls invalidate only nearby observations/targets,
|
||||
# not the rest of a recording. Never turn them into negative labels.
|
||||
request = requests.get(data.get('requestId'))
|
||||
unsupported_times.extend([data['ts'], request['ts'] if request else data['ts']])
|
||||
key = json.dumps(command, sort_keys=True)
|
||||
if key not in unsupported and len(unsupported) < 32:
|
||||
unsupported[key] = {'command': command, 'reason': str(error), 'count': 0}
|
||||
if key in unsupported:
|
||||
unsupported[key]['count'] += 1
|
||||
continue
|
||||
if encoded is None:
|
||||
continue
|
||||
stamp = data['ts']
|
||||
@@ -95,24 +108,25 @@ def prepare(directory, schema, stats):
|
||||
# Server safety overrides belong in history, never demonstration
|
||||
# targets. Exclude windows around them rather than teaching inaction.
|
||||
boundaries.append(stamp)
|
||||
if not targets:
|
||||
stats['unsupportedCommands'] = list(unsupported.values())
|
||||
stats['unsupportedCommandEvents'] = len(unsupported_times) // 2
|
||||
if not targets and not unsupported_times:
|
||||
raise ValueError('No accepted client operating commands')
|
||||
schema[:] = candidate
|
||||
return metadata, sensors, sorted(history, key=lambda x: x[0]), sorted(targets, key=lambda x: x[0]), sorted(boundaries)
|
||||
return metadata, sensors, sorted(history, key=lambda x: x[0]), sorted(targets, key=lambda x: x[0]), sorted(boundaries), sorted(unsupported_times)
|
||||
|
||||
|
||||
def windows(directory, schema, cancelled=lambda: False, stats=None):
|
||||
def windows(directory, schema, cancelled=lambda: False, stats=None, extend=True):
|
||||
directory = Path(directory)
|
||||
stats = stats if stats is not None else {}
|
||||
metadata, sensors, history, targets, boundaries = prepare(directory, schema, stats)
|
||||
metadata, sensors, history, targets, boundaries, unsupported_times = prepare(directory, schema, stats, extend)
|
||||
stats.update({'usableWindows': 0, 'commandWindows': 0, 'stopWindows': 0, 'noCommandWindows': 0,
|
||||
'staleSensorWindows': 0, 'boundaryWindows': 0,
|
||||
'ambiguousWindows': 0, 'videoFrames': 0, 'maxVideoGapMs': 0})
|
||||
'unsupportedWindows': 0, 'ambiguousWindows': 0, 'videoFrames': 0, 'maxVideoGapMs': 0})
|
||||
sensor_times = [row[0] for row in sensors]
|
||||
target_times = [row[0] for row in targets]
|
||||
available = np.zeros(SLOTS, dtype=np.float32)
|
||||
for _, slot, _ in history:
|
||||
available[slot] = 1
|
||||
available[:len(schema)] = 1
|
||||
command_values = np.zeros((SLOTS, VALUES), dtype=np.float32)
|
||||
command_times = np.full(SLOTS, -np.inf)
|
||||
cursor = 0
|
||||
@@ -172,6 +186,9 @@ def windows(directory, schema, cancelled=lambda: False, stats=None):
|
||||
chosen.append(options[-1])
|
||||
if len(chosen) != 4 or stamp + 100 > metadata['endedAt']:
|
||||
continue
|
||||
if bisect.bisect_right(unsupported_times, stamp + 100) > bisect.bisect_left(unsupported_times, chosen[0][0]):
|
||||
stats['unsupportedWindows'] += 1
|
||||
continue
|
||||
if bisect.bisect_right(boundaries, stamp + 100) > bisect.bisect_left(boundaries, chosen[0][0]):
|
||||
stats['boundaryWindows'] += 1
|
||||
continue
|
||||
|
||||
@@ -4,6 +4,8 @@ Slots describe command shapes, not arbitrary executable instructions. Numeric
|
||||
leaves are regressed; string/bool/list values remain exact template constants.
|
||||
No command is sent by this module. Execution must use server authorization.
|
||||
"""
|
||||
import base64
|
||||
import itertools
|
||||
import copy
|
||||
import json
|
||||
import math
|
||||
@@ -12,12 +14,12 @@ import numpy as np
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
VERSION = 3
|
||||
VERSION = 4
|
||||
SLOTS = 64
|
||||
VALUES = 8
|
||||
FRAMES = 4
|
||||
HEIGHT, WIDTH = 120, 160
|
||||
OPERATING_TYPES = {"drive", "motors", "servo", "headlight", "laser", "horn", "peripheral", "song", "raw"}
|
||||
OPERATING_TYPES = {"drive", "motors", "servo", "headlight", "laser", "horn", "song", "raw"}
|
||||
SENSORS = [
|
||||
("bumpsAndWheelDrops.bumpLeft", 1), ("bumpsAndWheelDrops.bumpRight", 1),
|
||||
("bumpsAndWheelDrops.wheelDropLeft", 1), ("bumpsAndWheelDrops.wheelDropRight", 1),
|
||||
@@ -59,18 +61,20 @@ def describe_command(command):
|
||||
values, fields = [], []
|
||||
|
||||
def visit(value, path=()):
|
||||
if isinstance(value, list) and command['type'] == 'horn' and path == ('horn', 'freqs'):
|
||||
return [visit(item, (*path, index)) for index, item in enumerate(value)]
|
||||
if isinstance(value, dict):
|
||||
return {key: visit(item, (*path, key)) for key, item in sorted(value.items())}
|
||||
# Exact zeros preserve demonstrated releases/stops. Identity fields must
|
||||
# also stay exact: regression must never invent a peripheral address.
|
||||
if value == 0 or (path and path[-1] in ('id', 'control')):
|
||||
# Exact actuator zeros preserve demonstrated releases. Horn frequency
|
||||
# zeros are parameters so changing a chord does not require a new slot.
|
||||
if value == 0 and command['type'] != 'horn':
|
||||
return copy.deepcopy(value)
|
||||
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
||||
if not math.isfinite(value) or len(values) >= VALUES:
|
||||
raise ValueError('Nonfinite command or too many numeric control fields')
|
||||
scale = {'drive': 500, 'motors': 255, 'servo': 180, 'peripheral': 255,
|
||||
scale = {'drive': 500, 'motors': 255, 'servo': 180,
|
||||
'horn': 1000, 'song': 127}.get(command['type'], 1)
|
||||
fields.append({'path': list(path), 'scale': scale, 'integer': isinstance(value, int)})
|
||||
fields.append({'path': list(path), 'scale': scale, 'integer': command['type'] in ('drive', 'motors', 'servo', 'horn') or isinstance(value, int)})
|
||||
values.append(normalize(value, scale))
|
||||
return {'$number': len(values) - 1}
|
||||
# Arrays (e.g. songs) are discrete exact demonstrations, not variable
|
||||
@@ -92,7 +96,7 @@ def encode_command(command, schema, extend=False):
|
||||
if not extend:
|
||||
raise ValueError('Command shape missing from model schema')
|
||||
if len(schema) >= SLOTS:
|
||||
raise ValueError('64 command shapes exhausted; recording skipped, not silently truncated')
|
||||
raise ValueError('64 command shapes exhausted; affected windows excluded')
|
||||
slot = len(schema)
|
||||
schema.append(descriptor)
|
||||
vector = np.zeros(VALUES, dtype=np.float32)
|
||||
@@ -100,10 +104,32 @@ def encode_command(command, schema, extend=False):
|
||||
return slot, vector
|
||||
|
||||
|
||||
def initial_schema():
|
||||
# Match commandPipeline.js envelopes, OI_COMMANDS and the four-frequency
|
||||
# horn control. Values are examples describing fields, not training labels.
|
||||
commands = []
|
||||
for left, right in itertools.product((0, 1), repeat=2):
|
||||
commands.append({'type': 'drive', 'driveDirect': {'left': left, 'right': right}})
|
||||
for main, side, vacuum in itertools.product((0, 1), repeat=3):
|
||||
commands.append({'type': 'motors', 'motorPwm': {'main': main, 'side': side, 'vacuum': vacuum}})
|
||||
for angle in (0, 1):
|
||||
commands.append({'type': 'servo', 'servo': {'angle': angle}})
|
||||
for kind in ('headlight', 'laser'):
|
||||
for action in ('on', 'off', 'toggle'):
|
||||
commands.append({'type': kind, kind: {'action': action}})
|
||||
for waveform in ('sine', 'saw'):
|
||||
commands.append({'type': 'horn', 'horn': {'action': 'start', 'waveform': waveform, 'freqs': [1, 1, 1, 1]}})
|
||||
commands.append({'type': 'horn', 'horn': {'action': 'stop'}})
|
||||
for opcode in (128, 131, 132, 143):
|
||||
commands.append({'type': 'raw', 'raw': base64.b64encode(bytes([opcode])).decode()})
|
||||
commands.append({'type': 'song', 'song': {'notes': [{'note': 60, 'duration': 8}]}})
|
||||
schema = []
|
||||
for command in commands:
|
||||
encode_command(command, schema, extend=True)
|
||||
return schema
|
||||
|
||||
|
||||
def command_family(command):
|
||||
if command['type'] == 'peripheral':
|
||||
peripheral = command.get('peripheral') or {}
|
||||
return ('peripheral', str(peripheral.get('id')), str(peripheral.get('control')))
|
||||
return (command['type'],)
|
||||
|
||||
|
||||
@@ -162,7 +188,7 @@ class Policy(nn.Module):
|
||||
|
||||
|
||||
def specification(schema):
|
||||
return {'version': VERSION, 'architecture': 'spatial-cnn-gru-events-v3',
|
||||
return {'version': VERSION, 'architecture': 'spatial-cnn-gru-events-v4',
|
||||
'frames': FRAMES, 'height': HEIGHT, 'width': WIDTH,
|
||||
'historyStrideMs': 300, 'decisionIntervalMs': 100,
|
||||
'stateSize': STATE_SIZE, 'sensorFields': SENSORS,
|
||||
|
||||
@@ -41,11 +41,11 @@ def main():
|
||||
import torch
|
||||
from torch.nn import functional as functional
|
||||
from dataset import windows
|
||||
from policy import Policy, VERSION, SLOTS, VALUES, specification, is_stop, select_slots
|
||||
from policy import Policy, VERSION, SLOTS, VALUES, specification, is_stop, select_slots, initial_schema
|
||||
|
||||
if args.check_dependencies:
|
||||
emit('dependencies', torch=str(torch.__version__), numpy=str(np.__version__), av=str(av.__version__),
|
||||
parameters=sum(p.numel() for p in Policy().parameters()), specification=specification([]))
|
||||
parameters=sum(p.numel() for p in Policy().parameters()), specification=specification(initial_schema()))
|
||||
return
|
||||
if not args.job:
|
||||
parser.error('--job is required unless --check-dependencies is used')
|
||||
@@ -72,8 +72,8 @@ def main():
|
||||
optimizer = torch.optim.AdamW(model.parameters(), lr=0.0001)
|
||||
# The command-event policy has different inputs, targets and weights. Keep its
|
||||
# optimizer lineage separate; reuse recordings with their unchanged partition.
|
||||
resume_path = root / 'training' / 'resume-v4.pt'
|
||||
schema, steps, examples, last_published = [], 0, 0, 0
|
||||
resume_path = root / 'training' / 'resume-v5.pt'
|
||||
schema, steps, examples, last_published = initial_schema(), 0, 0, 0
|
||||
trained_rovers = set()
|
||||
control_profiles = []
|
||||
distinct_windows = {}
|
||||
@@ -113,7 +113,7 @@ def main():
|
||||
held_out = is_held_out(session_id)
|
||||
try:
|
||||
directory = root / 'recordings' / session_id
|
||||
for sample in windows(directory, candidate_schema, cancelled, stats):
|
||||
for sample in windows(directory, candidate_schema, cancelled, stats, extend=not held_out):
|
||||
seen += 1
|
||||
# Keep evaluation naturally distributed. Training reserves space
|
||||
# for recorded commands and releases so periods without commands
|
||||
@@ -137,15 +137,15 @@ def main():
|
||||
if cancelled():
|
||||
break # Do not call a partially decoded recording validated.
|
||||
if not samples:
|
||||
if held_out and stats.get('unsupportedCommandEvents'):
|
||||
dataset_stats.append({**stats, 'partition': 'heldOut', 'sampledWindows': 0,
|
||||
'skipReason': 'No supported aligned windows'})
|
||||
continue
|
||||
raise ValueError('No complete, aligned observation/action windows')
|
||||
# Entire sessions stay in one partition across all jobs. Neighboring
|
||||
# video windows must never straddle training and validation.
|
||||
held_out = is_held_out(session_id)
|
||||
if held_out:
|
||||
if candidate_schema != schema:
|
||||
dataset_stats.append({**stats, 'partition': 'heldOut', 'sampledWindows': len(samples),
|
||||
'skipReason': 'Vocabulary absent from training data'})
|
||||
continue # Retry after training data introduces these command shapes.
|
||||
validation.extend(samples)
|
||||
else:
|
||||
schema = candidate_schema
|
||||
@@ -219,7 +219,12 @@ def main():
|
||||
evaluation_started = time.monotonic()
|
||||
distinct_windows.update(observed_windows)
|
||||
unique_count = sum(distinct_windows.values())
|
||||
evaluation = {'status': 'waiting for held-out sessions', 'windows': len(validation)}
|
||||
coverage = [row for row in dataset_stats if row.get('partition') == 'heldOut']
|
||||
unsupported_events = sum(row.get('unsupportedCommandEvents', 0) for row in coverage)
|
||||
unsupported_windows = sum(row.get('unsupportedWindows', 0) for row in coverage)
|
||||
evaluation = {'status': 'No supported held-out windows' if unsupported_events and not validation else 'waiting for held-out sessions',
|
||||
'windows': len(validation), 'unsupportedCommandEvents': unsupported_events,
|
||||
'unsupportedWindows': unsupported_windows}
|
||||
if validation and not cancelled():
|
||||
model.eval()
|
||||
true_positive = predicted_positive = actual_positive = 0
|
||||
@@ -276,6 +281,7 @@ def main():
|
||||
held_out_sessions = sum(row.get('partition') == 'heldOut' and not row.get('skipReason') for row in dataset_stats)
|
||||
gate_reasons = []
|
||||
for condition, reason in [
|
||||
(unsupported_events == 0, 'Held-out controls are not fully represented; see unsupportedCommands'),
|
||||
(evaluated == len(validation), 'Evaluation interrupted'),
|
||||
(held_out_sessions >= 2, 'Need two usable held-out recordings'),
|
||||
(evaluated >= 128, 'Need 128 held-out windows'),
|
||||
@@ -291,6 +297,7 @@ def main():
|
||||
if not condition:
|
||||
gate_reasons.append(reason)
|
||||
evaluation = {'status': 'imitation gate not met' if gate_reasons else 'passed imitation gate',
|
||||
'unsupportedCommandEvents': unsupported_events, 'unsupportedWindows': unsupported_windows,
|
||||
'method': 'Held-out 100ms command events; not a closed-loop rollout',
|
||||
'threshold': job['actionThreshold'], 'heldOutSessions': held_out_sessions,
|
||||
'windows': evaluated, 'precision': precision, 'recall': recall,
|
||||
|
||||
@@ -133,7 +133,7 @@ function LearningCard({ roverId }) {
|
||||
<div className="flex flex-wrap items-center justify-center gap-1">
|
||||
<select aria-label="Rover model" className={fieldClass} value={chosen?.id || ''} onChange={(event) => setSelectedId(event.target.value)} disabled={working || !models.length}>
|
||||
{!models.length && <option value="">No published models yet</option>}
|
||||
{models.map((model) => <option key={model.id} value={model.id}>{favorites.includes(model.id) ? '★ ' : ''}{model.name}{model.experimental ? ' (experimental)' : ''}{model.compatible ? '' : model.formatVersion !== 3 ? ' (older model format)' : ' (incompatible)'}</option>)}
|
||||
{models.map((model) => <option key={model.id} value={model.id}>{favorites.includes(model.id) ? '★ ' : ''}{model.name}{model.experimental ? ' (experimental)' : ''}{model.compatible ? '' : model.formatVersion !== 4 ? ' (older model format)' : ' (incompatible)'}</option>)}
|
||||
</select>
|
||||
<button type="button" className="button-dark text-sm disabled:opacity-50" aria-pressed={Boolean(chosen && favorites.includes(chosen.id))} disabled={!chosen} onClick={favorite}>
|
||||
{chosen && favorites.includes(chosen.id) ? '★ Favorite' : '☆ Favorite'}
|
||||
|
||||
Reference in New Issue
Block a user