dsh-eval-guard 0.6.1 → 0.7.1
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/assets/neural-pendulum/SPEC.md +470 -0
- package/assets/neural-pendulum/TASK.md +360 -0
- package/assets/neural-pendulum/hidden/test_infra.cpp +631 -0
- package/assets/neural-pendulum/include/nn/nn.hpp +88 -0
- package/assets/neural-pendulum/sim.cpp +126 -0
- package/assets/neural-pendulum/sim_eval +0 -0
- package/assets/neural-pendulum/workspace.zip +0 -0
- package/eval/neural-pendulum.py +15 -0
- package/lib/client/index.js +41 -4
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +454 -60
- package/neural-cifar-eval/README.md +57 -0
- package/neural-cifar-eval/cifar_cache.py +201 -0
- package/neural-cifar-eval/common.py +67 -0
- package/neural-cifar-eval/fetch_cifar.py +31 -0
- package/neural-cifar-eval/hidden/.gitkeep +1 -0
- package/neural-cifar-eval/ops_eval.py +502 -0
- package/neural-cifar-eval/run_eval.py +208 -0
- package/neural-pendulum-eval/run_eval.py +302 -0
- package/neural-pendulum-eval/sim_eval +0 -0
- package/package.json +4 -2
- package/eval/__pycache__/common.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-cifar.cpython-310.pyc +0 -0
- package/eval/__pycache__/neural-fp32.cpython-310.pyc +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Standalone Neural-CIFAR evaluator.
|
|
3
|
+
|
|
4
|
+
Copies the workspace into a temporary build directory, runs the original
|
|
5
|
+
operator benchmark, then loads the already-trained model and measures CIFAR-10
|
|
6
|
+
validation accuracy/FPS at 4 threads and at all logical CPUs.
|
|
7
|
+
|
|
8
|
+
The hidden validation images/labels live beside this script in hidden/ and are
|
|
9
|
+
never written into the agent workspace.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import math
|
|
16
|
+
import os
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tempfile
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
HERE = Path(__file__).resolve().parent
|
|
27
|
+
sys.path.insert(0, str(HERE))
|
|
28
|
+
from common import clean_copy
|
|
29
|
+
|
|
30
|
+
HIDDEN = HERE / 'hidden'
|
|
31
|
+
OPS = HERE / 'ops_eval.py'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def run(cmd, cwd, timeout=1800):
|
|
35
|
+
env = dict(os.environ)
|
|
36
|
+
env.pop('PYTHONPATH', None)
|
|
37
|
+
env.pop('PYTHONHOME', None)
|
|
38
|
+
return subprocess.run(cmd, cwd=str(cwd), capture_output=True, text=True,
|
|
39
|
+
encoding='utf-8', errors='replace', env=env, timeout=timeout)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def build_candidate(workspace: Path, root: Path):
|
|
43
|
+
candidate = root / 'candidate'
|
|
44
|
+
clean_copy(workspace, candidate, ignore_names=(
|
|
45
|
+
'data', 'build', 'work', '.tmp', '.cache', '.home', '.config', '.local',
|
|
46
|
+
'.git', '.venv', 'node_modules',
|
|
47
|
+
))
|
|
48
|
+
source = candidate
|
|
49
|
+
if not (source / 'CMakeLists.txt').is_file() and (candidate / 'workspace' / 'CMakeLists.txt').is_file():
|
|
50
|
+
source = candidate / 'workspace'
|
|
51
|
+
if not (source / 'CMakeLists.txt').is_file():
|
|
52
|
+
raise RuntimeError('CMakeLists.txt missing at workspace root or workspace/')
|
|
53
|
+
build = source / 'build'
|
|
54
|
+
if build.exists():
|
|
55
|
+
shutil.rmtree(build, ignore_errors=True)
|
|
56
|
+
build.mkdir(parents=True)
|
|
57
|
+
cfg = run(['cmake', '-S', '.', '-B', 'build', '-DCMAKE_BUILD_TYPE=Release'], source, 180)
|
|
58
|
+
if cfg.returncode != 0:
|
|
59
|
+
raise RuntimeError('cmake configure failed\n' + (cfg.stdout + cfg.stderr)[-4000:])
|
|
60
|
+
made = run(['cmake', '--build', 'build', '-j4'], source, 1800)
|
|
61
|
+
if made.returncode != 0:
|
|
62
|
+
raise RuntimeError('build failed\n' + (made.stdout + made.stderr)[-4000:])
|
|
63
|
+
return source, build
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def read_preds(path: Path, n: int) -> np.ndarray:
|
|
67
|
+
a = np.frombuffer(path.read_bytes(), dtype=np.uint8).copy()
|
|
68
|
+
if a.size != n:
|
|
69
|
+
raise RuntimeError(f'prediction size {a.size} != {n}')
|
|
70
|
+
return a
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def model_metrics(source: Path, threads: int, budget: float | None = None) -> dict:
|
|
74
|
+
infer = source / 'build' / 'infer'
|
|
75
|
+
images = HIDDEN / 'val_images.bin'
|
|
76
|
+
labels = HIDDEN / 'val_labels.bin'
|
|
77
|
+
if not infer.is_file():
|
|
78
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
79
|
+
'mean_seconds_per_image': None, 'error': 'build/infer missing',
|
|
80
|
+
'meets_4thread_budget': None}
|
|
81
|
+
if not images.is_file() or not labels.is_file():
|
|
82
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
83
|
+
'mean_seconds_per_image': None, 'error': 'hidden validation data missing; run fetch_cifar.py',
|
|
84
|
+
'meets_4thread_budget': None}
|
|
85
|
+
n = labels.stat().st_size
|
|
86
|
+
out = source / f'predictions_{threads}.bin'
|
|
87
|
+
if out.exists():
|
|
88
|
+
out.unlink()
|
|
89
|
+
cmd = [str(infer), '--model', str(source / 'model'),
|
|
90
|
+
'--images', str(images), '--out', str(out), '--threads', str(threads)]
|
|
91
|
+
started = time.perf_counter()
|
|
92
|
+
try:
|
|
93
|
+
proc = run(cmd, source, 1200)
|
|
94
|
+
except subprocess.TimeoutExpired:
|
|
95
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
96
|
+
'mean_seconds_per_image': None, 'error': 'inference timeout',
|
|
97
|
+
'meets_4thread_budget': None}
|
|
98
|
+
elapsed = time.perf_counter() - started
|
|
99
|
+
if proc.returncode != 0:
|
|
100
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
101
|
+
'mean_seconds_per_image': None,
|
|
102
|
+
'error': (proc.stderr or proc.stdout or 'infer failed')[-1000:],
|
|
103
|
+
'meets_4thread_budget': None}
|
|
104
|
+
try:
|
|
105
|
+
pred = read_preds(out, n)
|
|
106
|
+
true = np.frombuffer(labels.read_bytes(), dtype=np.uint8)
|
|
107
|
+
acc = float((pred == true).mean() * 100.0)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
return {'executable': False, 'accuracy': None, 'fps': None,
|
|
110
|
+
'mean_seconds_per_image': None, 'error': str(exc),
|
|
111
|
+
'meets_4thread_budget': None}
|
|
112
|
+
fps = n / elapsed if elapsed > 0 else None
|
|
113
|
+
mean_s = elapsed / n if n else None
|
|
114
|
+
result = {'executable': True, 'accuracy': acc, 'fps': fps,
|
|
115
|
+
'mean_seconds_per_image': mean_s, 'images': n,
|
|
116
|
+
'threads': threads, 'error': ''}
|
|
117
|
+
if budget is not None:
|
|
118
|
+
result['meets_4thread_budget'] = bool(mean_s is not None and mean_s <= budget)
|
|
119
|
+
return result
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main() -> int:
|
|
123
|
+
parser = argparse.ArgumentParser()
|
|
124
|
+
parser.add_argument('--workspace', required=True)
|
|
125
|
+
args = parser.parse_args()
|
|
126
|
+
workspace = Path(args.workspace).resolve()
|
|
127
|
+
|
|
128
|
+
if not OPS.is_file():
|
|
129
|
+
return fail({'task': 'neural-cifar', 'ok': False, 'error': 'ops_eval.py missing'})
|
|
130
|
+
|
|
131
|
+
with tempfile.TemporaryDirectory(prefix='neural-cifar-eval-') as td:
|
|
132
|
+
root = Path(td)
|
|
133
|
+
# Reuse the existing FP32 operator evaluator, but run it in a temp
|
|
134
|
+
# copy so its build output does not affect the model workspace.
|
|
135
|
+
ops_result = root / 'ops.json'
|
|
136
|
+
env = dict(os.environ)
|
|
137
|
+
env['EVAL_RESULT_FILE'] = str(ops_result)
|
|
138
|
+
env.pop('PYTHONPATH', None)
|
|
139
|
+
env.pop('PYTHONHOME', None)
|
|
140
|
+
try:
|
|
141
|
+
proc = subprocess.run([sys.executable, '-X', 'utf8', str(OPS), '--workspace', str(workspace)],
|
|
142
|
+
cwd=str(HERE), capture_output=True, text=True,
|
|
143
|
+
encoding='utf-8', errors='replace', env=env, timeout=2400)
|
|
144
|
+
except subprocess.TimeoutExpired:
|
|
145
|
+
return fail({'task': 'neural-cifar', 'ok': False, 'error': 'operator evaluation timed out'})
|
|
146
|
+
if proc.returncode != 0 or not ops_result.is_file():
|
|
147
|
+
return fail({'task': 'neural-cifar', 'ok': False, 'error': 'operator evaluation failed',
|
|
148
|
+
'output': (proc.stdout + proc.stderr)[-4000:]})
|
|
149
|
+
ops = json.loads(ops_result.read_text(encoding='utf-8'))
|
|
150
|
+
|
|
151
|
+
try:
|
|
152
|
+
source, _build = build_candidate(workspace, root)
|
|
153
|
+
except Exception as exc:
|
|
154
|
+
return fail({'task': 'neural-cifar', 'ok': False, 'error': str(exc)})
|
|
155
|
+
|
|
156
|
+
if not (HIDDEN / 'val_images.bin').is_file() or not (HIDDEN / 'val_labels.bin').is_file():
|
|
157
|
+
try:
|
|
158
|
+
fetch = subprocess.run([sys.executable, '-X', 'utf8', str(HERE / 'fetch_cifar.py')],
|
|
159
|
+
cwd=str(HERE), capture_output=True, text=True,
|
|
160
|
+
encoding='utf-8', errors='replace', timeout=1800)
|
|
161
|
+
except subprocess.TimeoutExpired:
|
|
162
|
+
fetch = None
|
|
163
|
+
if fetch is None or fetch.returncode != 0:
|
|
164
|
+
return fail({'task': 'neural-cifar', 'ok': False,
|
|
165
|
+
'error': 'hidden validation data missing and fetch failed',
|
|
166
|
+
'output': (fetch.stdout + fetch.stderr)[-3000:] if fetch is not None else 'timeout'})
|
|
167
|
+
|
|
168
|
+
model4 = model_metrics(source, 4, budget=0.02)
|
|
169
|
+
modelall = model_metrics(source, os.cpu_count() or 1)
|
|
170
|
+
|
|
171
|
+
print(f"all_operations_executable = {str(ops.get('all_operations_executable')).lower()}")
|
|
172
|
+
print(f"max_weighted_mean_relative_error = {ops.get('max_weighted_mean_relative_error')}")
|
|
173
|
+
print(f"speed_geomean = {ops.get('speed_geomean')}")
|
|
174
|
+
print(f"model_4_thread = {json.dumps(model4, ensure_ascii=False)}")
|
|
175
|
+
print(f"model_all_threads = {json.dumps(modelall, ensure_ascii=False)}")
|
|
176
|
+
for row in ops.get('operations', []):
|
|
177
|
+
print(f"{row.get('name')}: executable={row.get('executable')} pass={row.get('passed')} "
|
|
178
|
+
f"mre={row.get('weighted_mean_relative_error')} speed_gflops={row.get('speed_gflops')}")
|
|
179
|
+
|
|
180
|
+
result = {
|
|
181
|
+
'task': 'neural-cifar',
|
|
182
|
+
'ok': True,
|
|
183
|
+
'all_operations_executable': ops.get('all_operations_executable'),
|
|
184
|
+
'max_weighted_mean_relative_error': ops.get('max_weighted_mean_relative_error'),
|
|
185
|
+
'speed_geomean': ops.get('speed_geomean'),
|
|
186
|
+
'model_4_thread': model4,
|
|
187
|
+
'model_all_threads': modelall,
|
|
188
|
+
'operations': ops.get('operations', []),
|
|
189
|
+
}
|
|
190
|
+
target = os.environ.get('EVAL_RESULT_FILE')
|
|
191
|
+
if target:
|
|
192
|
+
Path(target).write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
193
|
+
else:
|
|
194
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
195
|
+
return 0
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def fail(payload: dict) -> int:
|
|
199
|
+
target = os.environ.get('EVAL_RESULT_FILE')
|
|
200
|
+
if target:
|
|
201
|
+
Path(target).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
202
|
+
else:
|
|
203
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
204
|
+
return 1
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == '__main__':
|
|
208
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Standalone evaluator for the Neural-Pendulum task.
|
|
3
|
+
|
|
4
|
+
It uses the latest preserved submission under .eval/submissions (falling back to
|
|
5
|
+
the live workspace), builds it from source, runs the public self-test, drives
|
|
6
|
+
the controller against a fixed-initial-state evaluation simulator, and computes
|
|
7
|
+
the task's time-integral score from .eval/events.jsonl.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
import selectors
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import time
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
HERE = Path(__file__).resolve().parent
|
|
23
|
+
SIM = HERE / 'sim_eval'
|
|
24
|
+
DT = 0.01
|
|
25
|
+
MAX_STEPS = 3000
|
|
26
|
+
T_TGT = 10.0
|
|
27
|
+
UP_TOL = 0.05
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_line(proc: subprocess.Popen, timeout: float) -> str:
|
|
31
|
+
selector = selectors.DefaultSelector()
|
|
32
|
+
selector.register(proc.stdout, selectors.EVENT_READ)
|
|
33
|
+
try:
|
|
34
|
+
if not selector.select(timeout):
|
|
35
|
+
raise RuntimeError('timeout')
|
|
36
|
+
line = proc.stdout.readline()
|
|
37
|
+
if line == '':
|
|
38
|
+
raise RuntimeError('process closed')
|
|
39
|
+
return line.strip()
|
|
40
|
+
finally:
|
|
41
|
+
selector.close()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def write_line(proc: subprocess.Popen, obj: dict) -> None:
|
|
45
|
+
assert proc.stdin is not None
|
|
46
|
+
proc.stdin.write(json.dumps(obj) + '\n')
|
|
47
|
+
proc.stdin.flush()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def read_events(workspace: Path) -> list[dict]:
|
|
51
|
+
path = workspace / '.eval' / 'events.jsonl'
|
|
52
|
+
out: list[dict] = []
|
|
53
|
+
if path.is_file():
|
|
54
|
+
for line in path.read_text(encoding='utf-8', errors='replace').splitlines():
|
|
55
|
+
try:
|
|
56
|
+
value = json.loads(line)
|
|
57
|
+
if isinstance(value, dict):
|
|
58
|
+
out.append(value)
|
|
59
|
+
except Exception:
|
|
60
|
+
continue
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def task_start(workspace: Path, events: list[dict]) -> float:
|
|
65
|
+
meta = workspace / '.eval' / 'meta.json'
|
|
66
|
+
if meta.is_file():
|
|
67
|
+
try:
|
|
68
|
+
return float(json.loads(meta.read_text(encoding='utf-8')).get('started_at'))
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
if events:
|
|
72
|
+
return float(events[0].get('time', events[0].get('at', time.time())))
|
|
73
|
+
return time.time()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def score_for(event: dict) -> float:
|
|
77
|
+
stage = event.get('stage', '')
|
|
78
|
+
if stage not in ('scoring', 'completed') and event.get('valid') is not True:
|
|
79
|
+
return -1.0
|
|
80
|
+
try:
|
|
81
|
+
t_can = float(event.get('t_can', 0.0))
|
|
82
|
+
except Exception:
|
|
83
|
+
return -1.0
|
|
84
|
+
if not math.isfinite(t_can):
|
|
85
|
+
return -1.0
|
|
86
|
+
return (min(max(t_can, 0.0), T_TGT) - T_TGT) / T_TGT
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def final_score(workspace: Path, events: list[dict], now: float | None = None) -> tuple[float, float | None]:
|
|
90
|
+
"""Return (integral, T_done); negative score is penalized over time."""
|
|
91
|
+
now = time.time() if now is None else now
|
|
92
|
+
start = task_start(workspace, events)
|
|
93
|
+
tests = [e for e in events if (e.get('event') or e.get('type')) == 'test']
|
|
94
|
+
if not tests:
|
|
95
|
+
return -1.0 * max(0.0, now - start), None
|
|
96
|
+
done_at = None
|
|
97
|
+
prev_at = start
|
|
98
|
+
current = -1.0
|
|
99
|
+
total = 0.0
|
|
100
|
+
for e in tests:
|
|
101
|
+
at = float(e.get('time', e.get('at', prev_at)))
|
|
102
|
+
if at < prev_at:
|
|
103
|
+
at = prev_at
|
|
104
|
+
total += current * (at - prev_at)
|
|
105
|
+
current = score_for(e)
|
|
106
|
+
prev_at = at
|
|
107
|
+
if e.get('completed'):
|
|
108
|
+
done_at = at
|
|
109
|
+
break
|
|
110
|
+
end = done_at if done_at is not None else now
|
|
111
|
+
total += current * max(0.0, end - prev_at)
|
|
112
|
+
return total, done_at
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def archive_submissions(workspace: Path) -> int:
|
|
116
|
+
root = os.environ.get('EVAL_SUBMISSIONS_ROOT', '')
|
|
117
|
+
src_root = workspace / '.eval' / 'submissions'
|
|
118
|
+
if not root or not src_root.is_dir():
|
|
119
|
+
return 0
|
|
120
|
+
target = Path(root)
|
|
121
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
count = 0
|
|
123
|
+
for source in src_root.iterdir():
|
|
124
|
+
if not source.is_dir():
|
|
125
|
+
continue
|
|
126
|
+
destination = target / source.name
|
|
127
|
+
if destination.exists():
|
|
128
|
+
continue
|
|
129
|
+
shutil.copytree(source, destination, symlinks=True)
|
|
130
|
+
count += 1
|
|
131
|
+
return count
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def latest_submission(workspace: Path) -> Path | None:
|
|
135
|
+
root = workspace / '.eval' / 'submissions'
|
|
136
|
+
if not root.is_dir():
|
|
137
|
+
return None
|
|
138
|
+
dirs = [p for p in root.iterdir() if p.is_dir()]
|
|
139
|
+
if not dirs:
|
|
140
|
+
return None
|
|
141
|
+
return max(dirs, key=lambda p: p.stat().st_mtime)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def fail(message: str, infra_ok: bool = False, workspace: Path | None = None) -> dict:
|
|
145
|
+
result = {'infra_ok': infra_ok, 't_can': 0.0, 'success': False, 'error': message}
|
|
146
|
+
if workspace is not None:
|
|
147
|
+
events = read_events(workspace)
|
|
148
|
+
result['final_score'], done = final_score(workspace, events)
|
|
149
|
+
result['done_at'] = done
|
|
150
|
+
result['submissions'] = len(list((workspace / '.eval' / 'submissions').glob('*'))) if (workspace / '.eval' / 'submissions').is_dir() else 0
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def main() -> int:
|
|
155
|
+
import argparse
|
|
156
|
+
parser = argparse.ArgumentParser()
|
|
157
|
+
parser.add_argument('--workspace', required=True)
|
|
158
|
+
args = parser.parse_args()
|
|
159
|
+
|
|
160
|
+
workspace = Path(args.workspace).resolve()
|
|
161
|
+
events = read_events(workspace)
|
|
162
|
+
root = Path(tempfile.mkdtemp(prefix='neural-pendulum-eval-'))
|
|
163
|
+
candidate = root / 'candidate'
|
|
164
|
+
result = {'infra_ok': False, 't_can': 0.0, 'success': False, 'error': ''}
|
|
165
|
+
sim = None
|
|
166
|
+
control = None
|
|
167
|
+
try:
|
|
168
|
+
archived = archive_submissions(workspace)
|
|
169
|
+
selected = latest_submission(workspace)
|
|
170
|
+
if selected is None:
|
|
171
|
+
return emit(fail('no submission', workspace=workspace))
|
|
172
|
+
source = selected
|
|
173
|
+
shutil.copytree(source, candidate, symlinks=True)
|
|
174
|
+
if not (candidate / 'CMakeLists.txt').is_file():
|
|
175
|
+
return emit(fail('CMakeLists.txt missing', workspace=workspace))
|
|
176
|
+
|
|
177
|
+
env = dict(os.environ)
|
|
178
|
+
env.pop('PYTHONPATH', None)
|
|
179
|
+
env.pop('PYTHONHOME', None)
|
|
180
|
+
build = candidate / 'build'
|
|
181
|
+
if build.exists():
|
|
182
|
+
shutil.rmtree(build, ignore_errors=True)
|
|
183
|
+
cfg = subprocess.run(
|
|
184
|
+
['cmake', '-S', '.', '-B', 'build', '-DCMAKE_BUILD_TYPE=Release'],
|
|
185
|
+
cwd=str(candidate), capture_output=True, text=True, env=env, timeout=180,
|
|
186
|
+
)
|
|
187
|
+
if cfg.returncode != 0:
|
|
188
|
+
return emit(fail('cmake configure failed\n' + (cfg.stdout + cfg.stderr)[-4000:], workspace=workspace))
|
|
189
|
+
made = subprocess.run(
|
|
190
|
+
['cmake', '--build', 'build', '-j4'],
|
|
191
|
+
cwd=str(candidate), capture_output=True, text=True, env=env, timeout=1800,
|
|
192
|
+
)
|
|
193
|
+
if made.returncode != 0:
|
|
194
|
+
return emit(fail('build failed\n' + (made.stdout + made.stderr)[-4000:], workspace=workspace))
|
|
195
|
+
|
|
196
|
+
lib = candidate / 'build' / 'libnn.a'
|
|
197
|
+
if not lib.is_file():
|
|
198
|
+
return emit(fail('build/libnn.a missing', workspace=workspace))
|
|
199
|
+
hidden = HERE.parent / 'assets' / 'neural-pendulum' / 'hidden' / 'test_infra.cpp'
|
|
200
|
+
if not hidden.is_file():
|
|
201
|
+
return emit(fail('hidden infrastructure test missing', workspace=workspace))
|
|
202
|
+
infra_exe = root / 'test_infra'
|
|
203
|
+
compiler = shutil.which('g++')
|
|
204
|
+
if compiler is None:
|
|
205
|
+
return emit(fail('g++ missing', workspace=workspace))
|
|
206
|
+
ic = subprocess.run([compiler, '-std=c++17', '-O3', '-pthread',
|
|
207
|
+
'-I', str(candidate / 'include'), str(hidden), str(lib),
|
|
208
|
+
'-lm', '-o', str(infra_exe)], cwd=str(root), capture_output=True,
|
|
209
|
+
text=True, env=env, timeout=300)
|
|
210
|
+
if ic.returncode != 0:
|
|
211
|
+
return emit(fail('hidden infrastructure test compile failed\n' + (ic.stdout + ic.stderr)[-4000:], workspace=workspace))
|
|
212
|
+
it = subprocess.run([str(infra_exe)], cwd=str(root), capture_output=True,
|
|
213
|
+
text=True, env=env, timeout=600)
|
|
214
|
+
if it.returncode != 0:
|
|
215
|
+
return emit(fail('hidden infrastructure test failed\n' + (it.stdout + it.stderr)[-4000:], workspace=workspace))
|
|
216
|
+
|
|
217
|
+
control_path = candidate / 'build' / 'control'
|
|
218
|
+
if not control_path.is_file():
|
|
219
|
+
return emit(fail('build/control missing', infra_ok=True, workspace=workspace))
|
|
220
|
+
|
|
221
|
+
sim = subprocess.Popen([str(SIM), '--eval'], stdin=subprocess.PIPE,
|
|
222
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
223
|
+
text=True, bufsize=1)
|
|
224
|
+
control = subprocess.Popen([str(control_path)], cwd=str(candidate),
|
|
225
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
226
|
+
stderr=subprocess.PIPE, text=True, bufsize=1)
|
|
227
|
+
|
|
228
|
+
write_line(sim, {'cmd': 'reset'})
|
|
229
|
+
obs = json.loads(read_line(sim, 10.0))['obs']
|
|
230
|
+
write_line(control, {'type': 'reset'})
|
|
231
|
+
ready = json.loads(read_line(control, 10.0))
|
|
232
|
+
if ready.get('type') != 'ready':
|
|
233
|
+
return emit(fail('controller did not reply ready', infra_ok=True, workspace=workspace))
|
|
234
|
+
|
|
235
|
+
t_can = 0.0
|
|
236
|
+
up_start = None
|
|
237
|
+
completed = False
|
|
238
|
+
error = ''
|
|
239
|
+
for step in range(MAX_STEPS):
|
|
240
|
+
if not isinstance(obs, list) or len(obs) != 3:
|
|
241
|
+
error = 'simulator returned bad observation'
|
|
242
|
+
break
|
|
243
|
+
if max(abs(float(x)) for x in obs) < UP_TOL:
|
|
244
|
+
if up_start is None:
|
|
245
|
+
up_start = step * DT
|
|
246
|
+
t_can = max(t_can, step * DT - up_start)
|
|
247
|
+
else:
|
|
248
|
+
up_start = None
|
|
249
|
+
if t_can >= T_TGT:
|
|
250
|
+
completed = True
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
write_line(control, {'type': 'step', 'step': step, 'state': obs})
|
|
254
|
+
action = json.loads(read_line(control, 0.1))
|
|
255
|
+
torque = action.get('torque')
|
|
256
|
+
if (not isinstance(torque, list) or len(torque) != 2
|
|
257
|
+
or not all(isinstance(v, (int, float)) and math.isfinite(v) for v in torque)
|
|
258
|
+
or any(abs(float(v)) > 1.0 for v in torque)):
|
|
259
|
+
error = 'controller returned invalid torque'
|
|
260
|
+
break
|
|
261
|
+
write_line(sim, {'cmd': 'step', 'torque': torque})
|
|
262
|
+
reply = json.loads(read_line(sim, 10.0))
|
|
263
|
+
if 'error' in reply:
|
|
264
|
+
break
|
|
265
|
+
obs = reply['obs']
|
|
266
|
+
|
|
267
|
+
integral, done_at = final_score(workspace, events)
|
|
268
|
+
result = {'infra_ok': True, 't_can': float(t_can),
|
|
269
|
+
'success': bool(completed), 'error': error,
|
|
270
|
+
'final_score': integral, 'done_at': done_at,
|
|
271
|
+
'submissions': len(list((workspace / '.eval' / 'submissions').glob('*'))) if (workspace / '.eval' / 'submissions').is_dir() else 0,
|
|
272
|
+
'archived_submissions': archived,
|
|
273
|
+
'source': str(selected.relative_to(workspace)) if selected is not None else 'live-workspace'}
|
|
274
|
+
return emit(result)
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
return emit(fail(str(exc), workspace=workspace))
|
|
277
|
+
finally:
|
|
278
|
+
for proc in (control, sim):
|
|
279
|
+
if proc is not None:
|
|
280
|
+
try:
|
|
281
|
+
proc.terminate()
|
|
282
|
+
proc.wait(timeout=2)
|
|
283
|
+
except Exception:
|
|
284
|
+
try:
|
|
285
|
+
proc.kill()
|
|
286
|
+
except Exception:
|
|
287
|
+
pass
|
|
288
|
+
shutil.rmtree(root, ignore_errors=True)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def emit(result: dict) -> int:
|
|
292
|
+
target = os.environ.get('EVAL_RESULT_FILE')
|
|
293
|
+
text = json.dumps(result, ensure_ascii=False, indent=2)
|
|
294
|
+
if target:
|
|
295
|
+
Path(target).write_text(text, encoding='utf-8')
|
|
296
|
+
else:
|
|
297
|
+
print(text)
|
|
298
|
+
return 0
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
if __name__ == '__main__':
|
|
302
|
+
raise SystemExit(main())
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-eval-guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Evaluation guard for DSH: workspace-only filesystem, no network/subagents, wall-clock limits, and task installation for the meow-ai-arena tank task.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -45,7 +45,9 @@
|
|
|
45
45
|
"eval",
|
|
46
46
|
"cordis.patch.yml",
|
|
47
47
|
"README.md",
|
|
48
|
-
"scripts"
|
|
48
|
+
"scripts",
|
|
49
|
+
"neural-cifar-eval",
|
|
50
|
+
"neural-pendulum-eval"
|
|
49
51
|
],
|
|
50
52
|
"scripts": {
|
|
51
53
|
"build": "tsdown",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|