cotterbot 0.1.0__tar.gz → 0.3.0__tar.gz
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.
- {cotterbot-0.1.0 → cotterbot-0.3.0}/PKG-INFO +293 -12
- {cotterbot-0.1.0 → cotterbot-0.3.0}/README.md +286 -11
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/__init__.py +88 -1
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/cli.py +155 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/config.py +137 -1
- cotterbot-0.3.0/cotter/coverage.py +127 -0
- cotterbot-0.3.0/cotter/diff.py +118 -0
- cotterbot-0.3.0/cotter/falsify.py +169 -0
- cotterbot-0.3.0/cotter/junit.py +70 -0
- cotterbot-0.3.0/cotter/manifest.py +95 -0
- cotterbot-0.3.0/cotter/pipeline.py +391 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/policy.py +54 -3
- cotterbot-0.3.0/cotter/render.py +240 -0
- cotterbot-0.3.0/cotter/render_md.py +75 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/report.py +105 -3
- cotterbot-0.3.0/cotter/tests/action_adversarial.py +291 -0
- cotterbot-0.3.0/cotter/tests/iso15066.py +289 -0
- cotterbot-0.3.0/cotter/tests/stl.py +159 -0
- cotterbot-0.3.0/cotter/tests/transfer.py +104 -0
- cotterbot-0.3.0/cotter/traceability.py +113 -0
- cotterbot-0.3.0/cotter/verify.py +93 -0
- cotterbot-0.3.0/cotter/zoo/pretrained.py +159 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/pyproject.toml +12 -1
- cotterbot-0.1.0/cotter/pipeline.py +0 -214
- {cotterbot-0.1.0 → cotterbot-0.3.0}/LICENSE +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/backends.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/compliance/__init__.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/compliance/base.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/envs/__init__.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/envs/factory.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/envs/registry.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/envs/wrapper.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/runner.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/stats.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/success.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/tests/__init__.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/tests/adversarial.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/tests/regression.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/tests/safety.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/tests/sprt.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/zoo/__init__.py +0 -0
- {cotterbot-0.1.0 → cotterbot-0.3.0}/cotter/zoo/registry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cotterbot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Compliance-testing framework for AI-controlled robot policies — pytest for robot policies.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -9,11 +9,17 @@ Author-email: hyihan2007hong@gmail.com
|
|
|
9
9
|
Requires-Python: >=3.11,<3.12
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Provides-Extra: falsify
|
|
13
|
+
Provides-Extra: onnx
|
|
14
|
+
Provides-Extra: stl
|
|
15
|
+
Requires-Dist: cma (>=4.0,<5.0) ; extra == "falsify"
|
|
12
16
|
Requires-Dist: gymnasium-robotics (>=1.3,<2.0)
|
|
13
17
|
Requires-Dist: gymnasium[mujoco] (>=1.0,<2.0)
|
|
14
18
|
Requires-Dist: mujoco (>=3.2,<4.0)
|
|
15
19
|
Requires-Dist: numpy (>=1.26,<2.0)
|
|
20
|
+
Requires-Dist: onnxruntime (>=1.17,<2.0) ; extra == "onnx"
|
|
16
21
|
Requires-Dist: pyyaml (>=6.0,<7.0)
|
|
22
|
+
Requires-Dist: rtamt (>=0.3,<0.4) ; extra == "stl"
|
|
17
23
|
Requires-Dist: scipy (>=1.13,<2.0)
|
|
18
24
|
Requires-Dist: stable-baselines3 (>=2.4,<3.0)
|
|
19
25
|
Requires-Dist: torch (>=2.4,<3.0)
|
|
@@ -24,9 +30,11 @@ Description-Content-Type: text/markdown
|
|
|
24
30
|
**Compliance testing for AI-controlled robot policies — pytest for robot
|
|
25
31
|
policies.**
|
|
26
32
|
|
|
27
|
-
Cotter loads a trained robot policy as a black box (observation → action)
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
Cotter loads a trained robot policy as a black box (observation → action)
|
|
34
|
+
— a Stable-Baselines3 `.zip`, a raw PyTorch `.pt`/`.pth` module, or an
|
|
35
|
+
ONNX `.onnx` model — runs it through a battery of standardized tests in
|
|
36
|
+
MuJoCo simulation, and produces structured pass/fail results with
|
|
37
|
+
statistical guarantees. It is
|
|
30
38
|
aimed at the emerging regulatory need (EU Machinery Regulation, ISO 10218)
|
|
31
39
|
for evidence that a learned controller actually behaves — but the core is
|
|
32
40
|
just honest, reproducible testing:
|
|
@@ -34,9 +42,10 @@ just honest, reproducible testing:
|
|
|
34
42
|
| Category | Question | Method |
|
|
35
43
|
|---|---|---|
|
|
36
44
|
| **Performance** | Does it succeed at the task? | Wald's sequential probability ratio test (SPRT) — stops sampling as soon as the evidence is decisive |
|
|
37
|
-
| **Safety** | Does it ever exceed a hard physical limit? | Per-timestep checks on joint velocities / actuator forces / contacts; a single violation anywhere fails, no averaging |
|
|
45
|
+
| **Safety** | Does it ever exceed a hard physical limit? | Per-timestep checks on joint velocities / actuator forces / contacts; a single violation anywhere fails, no averaging. Includes an **ISO/TS 15066** power-and-force-limiting check (per-body-region contact-speed limits) |
|
|
38
46
|
| **Regression** | Did the new version break behavior? | Matched pairs on a shared seed sequence, exact McNemar (binary) and Wilcoxon signed-rank (continuous) |
|
|
39
|
-
| **Adversarial** | How bad is the worst case? | A PPO adversary trained to perturb the policy's *observations* within an L∞ budget, plus a guaranteed random-noise baseline |
|
|
47
|
+
| **Adversarial** | How bad is the worst case? | A PPO adversary trained to perturb the policy's *observations* (sensor attack) or *actions* (actuator attack) within an L∞ budget, plus a guaranteed random-noise baseline |
|
|
48
|
+
| **Specification** | Does it meet a declared behavioral requirement? | Signal Temporal Logic (STL) specs scored by *quantitative robustness* per rollout — e.g. `always (speed <= 1.5)`, `eventually (dist_to_goal <= 0.05)` |
|
|
40
49
|
|
|
41
50
|
Everything runs on CPU (developed on Apple Silicon; no CUDA anywhere in
|
|
42
51
|
the stack).
|
|
@@ -50,13 +59,27 @@ Requires Python 3.11. From PyPI (the distribution is published as
|
|
|
50
59
|
pip install cotterbot
|
|
51
60
|
```
|
|
52
61
|
|
|
62
|
+
To load **ONNX** policies, install the optional extra (pulls in `onnxruntime`):
|
|
63
|
+
|
|
64
|
+
```sh
|
|
65
|
+
pip install "cotterbot[onnx]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> **Note:** on some systems the `cotter` binary lands outside your PATH (pip
|
|
69
|
+
> will warn you with the exact directory). Either add that directory to PATH,
|
|
70
|
+
> or invoke via `python -m cotter` instead:
|
|
71
|
+
>
|
|
72
|
+
> ```sh
|
|
73
|
+
> python -m cotter run --policy policy.zip --config run.yaml
|
|
74
|
+
> ```
|
|
75
|
+
|
|
53
76
|
Or from source with [Poetry](https://python-poetry.org/):
|
|
54
77
|
|
|
55
78
|
```sh
|
|
56
79
|
git clone https://github.com/yih0nk/cotter.git
|
|
57
80
|
cd cotter
|
|
58
81
|
poetry install
|
|
59
|
-
poetry run pytest #
|
|
82
|
+
poetry run pytest # 435 tests, unit + real-MuJoCo integration
|
|
60
83
|
```
|
|
61
84
|
|
|
62
85
|
## Quickstart (CLI)
|
|
@@ -103,6 +126,224 @@ confidence interval (SPRT and adversarial results), and regression
|
|
|
103
126
|
results carry an effect size — the discordant-pair odds ratio for
|
|
104
127
|
McNemar, the rank-biserial correlation for Wilcoxon.
|
|
105
128
|
|
|
129
|
+
#### Adversarial attack surface
|
|
130
|
+
|
|
131
|
+
The adversarial category attacks either surface, set by
|
|
132
|
+
`adversarial.attack`:
|
|
133
|
+
|
|
134
|
+
- `observation` (default) — perturb what the victim *sees* (sensor noise,
|
|
135
|
+
calibration drift).
|
|
136
|
+
- `action` — perturb what the victim *does* (actuator noise, control-channel
|
|
137
|
+
corruption), clipped back into the action space.
|
|
138
|
+
- `both` — run both; the report carries distinct `action_*` entries.
|
|
139
|
+
|
|
140
|
+
Each surface runs a guaranteed random baseline and, when `train: true`, a
|
|
141
|
+
PPO adversary trained against the frozen victim.
|
|
142
|
+
|
|
143
|
+
### Specifications (Signal Temporal Logic)
|
|
144
|
+
|
|
145
|
+
Beyond the fixed categories, declare behavioral requirements in **Signal
|
|
146
|
+
Temporal Logic** and Cotter scores each rollout by *quantitative
|
|
147
|
+
robustness* — a signed margin (positive = satisfied, negative = violated,
|
|
148
|
+
and by how much), not just pass/fail. Requires the `stl` extra
|
|
149
|
+
(`pip install cotterbot[stl]`):
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
stl:
|
|
153
|
+
n_episodes: 20
|
|
154
|
+
variables:
|
|
155
|
+
speed: cotter/tcp_speed # scalar info key
|
|
156
|
+
jv0: {key: cotter/joint_velocities, index: 0} # a vector component
|
|
157
|
+
threshold: 0.0 # min robustness required to pass (0 = the STL boundary)
|
|
158
|
+
specs:
|
|
159
|
+
- name: speed_cap
|
|
160
|
+
formula: "always (speed <= 1.5)"
|
|
161
|
+
- name: settles
|
|
162
|
+
formula: "eventually (speed <= 0.1)"
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Each spec becomes a `specification`-category result reporting the worst
|
|
166
|
+
(minimum) episode robustness. Signals are pulled from the per-step `info`
|
|
167
|
+
dict by the `variables` map. STL operators (`always`, `eventually`,
|
|
168
|
+
`until`, bounded `[a,b]`) are evaluated offline with
|
|
169
|
+
[`rtamt`](https://github.com/nickovic/rtamt).
|
|
170
|
+
|
|
171
|
+
### Falsification (active worst-case search)
|
|
172
|
+
|
|
173
|
+
The categories above *sample* scenarios; falsification *searches* for the
|
|
174
|
+
worst one. Point CMA-ES at an STL-robustness objective and it hunts the
|
|
175
|
+
initial condition / disturbance / parameter vector that most violates a
|
|
176
|
+
spec — returning the concrete counterexample. Requires the `falsify`
|
|
177
|
+
extra (`pip install cotterbot[falsify]`); a Python-API feature, since the
|
|
178
|
+
scenario→env mapping is code:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
import cotter
|
|
182
|
+
|
|
183
|
+
# how a scenario vector configures the env (masses, frictions, disturbance...)
|
|
184
|
+
def apply_scenario(env, params):
|
|
185
|
+
env.unwrapped.model.body_mass[1] = params[0] # e.g. sweep a link mass
|
|
186
|
+
|
|
187
|
+
objective = cotter.stl_scenario_objective(
|
|
188
|
+
policy, env_factory, apply_scenario,
|
|
189
|
+
formula="always (tcp_speed <= 1.5)",
|
|
190
|
+
variables={"tcp_speed": {"key": "cotter/tcp_speed"}},
|
|
191
|
+
)
|
|
192
|
+
result = cotter.falsify(objective, lower=[0.5], upper=[5.0], max_evaluations=200)
|
|
193
|
+
if result.falsified:
|
|
194
|
+
print("counterexample scenario:", result.best_params)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`falsify` minimizes the objective and reports whether it dropped below the
|
|
198
|
+
threshold (0 = a spec violation). Derivative-free and CPU-only — each
|
|
199
|
+
evaluation is one rollout; 1-D spaces use a deterministic sweep, higher
|
|
200
|
+
dimensions use CMA-ES.
|
|
201
|
+
|
|
202
|
+
### Coverage sweeps (operating-envelope coverage)
|
|
203
|
+
|
|
204
|
+
A green report shouldn't be able to hide untested regions. Coverage
|
|
205
|
+
sweeps the scenario space with a low-discrepancy **Sobol** sequence and
|
|
206
|
+
reports the metric *as a function of the scenario* — the failure fraction
|
|
207
|
+
and worst region, not one aggregate number. Same `objective` as
|
|
208
|
+
falsification (sweep it here, search it there), CPU-only, no extra:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
import cotter
|
|
212
|
+
|
|
213
|
+
result = cotter.sweep(
|
|
214
|
+
objective, # e.g. cotter.stl_scenario_objective(...)
|
|
215
|
+
lower=[0.5, 0.0], upper=[5.0, 1.0], # the operating envelope
|
|
216
|
+
n_scenarios=64,
|
|
217
|
+
)
|
|
218
|
+
print(result.summary()) # "covered 64 scenarios, 7 below threshold (11% failure region) ..."
|
|
219
|
+
for o in result.outcomes: # every (scenario, value, pass/fail) — a coverage map
|
|
220
|
+
...
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Coverage-of-operating-envelope reporting is something regulators ask for
|
|
224
|
+
(UNECE-style ODD coverage) but no open policy-eval tool provides.
|
|
225
|
+
|
|
226
|
+
For the common case — coverage across the **observation-disturbance**
|
|
227
|
+
envelope — a `coverage` config section runs it as part of `cotter run`
|
|
228
|
+
(no code), sweeping a constant observation offset in the L∞ box and
|
|
229
|
+
reporting a `coverage`-category result:
|
|
230
|
+
|
|
231
|
+
```yaml
|
|
232
|
+
coverage:
|
|
233
|
+
epsilon: 0.1 # L∞ budget on the observation offset
|
|
234
|
+
n_scenarios: 32
|
|
235
|
+
n_episodes: 10
|
|
236
|
+
min_success_rate: 0.8 # a scenario fails below this
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### ISO/TS 15066 power-and-force-limiting (PFL)
|
|
240
|
+
|
|
241
|
+
Collaborative-robot safety comes down to a concrete numeric question: at
|
|
242
|
+
its speed, would a contact exceed the human injury threshold for the body
|
|
243
|
+
region it might hit? Cotter checks it directly. Declare an
|
|
244
|
+
`iso_ts_15066` section:
|
|
245
|
+
|
|
246
|
+
```yaml
|
|
247
|
+
iso_ts_15066:
|
|
248
|
+
m_robot: 5.0 # effective robot mass at the TCP (kg), incl. payload
|
|
249
|
+
speed_key: cotter/tcp_speed # per-step info key holding TCP linear velocity
|
|
250
|
+
contact_type: transient # transient (dynamic) or quasi_static (clamping)
|
|
251
|
+
regions: [chest, hand_finger, skull_forehead] # omit for the full library
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For each region, Cotter back-solves the maximum permissible contact speed
|
|
255
|
+
from the ISO/TS 15066 collision model — `F = v·√(k·μ)`, reduced mass
|
|
256
|
+
`1/μ = 1/m_robot + 1/m_human` — and flags any timestep whose TCP speed
|
|
257
|
+
would produce a force over that region's limit, reporting the binding
|
|
258
|
+
(most restrictive) region and the implied contact force.
|
|
259
|
+
|
|
260
|
+
> The bundled per-region limits (`cotter.ISO_TS_15066_LIMITS`) are the
|
|
261
|
+
> widely-published approximations and are **user-editable** — confirm them
|
|
262
|
+
> against the purchased ISO/TS 15066 before relying on them for
|
|
263
|
+
> certification.
|
|
264
|
+
|
|
265
|
+
### Clause traceability
|
|
266
|
+
|
|
267
|
+
Conformity assessment is clause-by-clause. Declare which checks evidence
|
|
268
|
+
each regulatory clause and Cotter reports every clause as verified,
|
|
269
|
+
failed, or **unverified** — the coverage-gap view an auditor asks for:
|
|
270
|
+
|
|
271
|
+
```yaml
|
|
272
|
+
traceability:
|
|
273
|
+
EHSR-1.3.7:
|
|
274
|
+
description: "moving parts of machinery"
|
|
275
|
+
checks: [hard_limits, iso_ts_15066_pfl]
|
|
276
|
+
AI-Act-15:
|
|
277
|
+
description: "robustness & accuracy"
|
|
278
|
+
checks: [learned_ppo, speed_cap]
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
A clause is *failed* if any of its checks failed, *verified* if it has a
|
|
282
|
+
passing check and none failed, else *unverified* (no evidence). The result
|
|
283
|
+
is a `traceability`-category entry that fails the report if any clause is
|
|
284
|
+
failed or unverified. (The certified clause *thresholds* are the paid
|
|
285
|
+
layer; this open mapping ties your checks to the clauses you declare.)
|
|
286
|
+
|
|
287
|
+
### Reports (JSON + HTML + JUnit + Markdown)
|
|
288
|
+
|
|
289
|
+
Every run prints a console summary and can write four artifacts:
|
|
290
|
+
|
|
291
|
+
- **JSON** (`report:` in the config, or `--report`) — the machine-readable
|
|
292
|
+
evidence, one object per category with the full statistics.
|
|
293
|
+
- **HTML** (`report_html:` in the config, or `--report-html`) — a single
|
|
294
|
+
self-contained page you can open in a browser, drop into a pull request,
|
|
295
|
+
or attach to a demo. No external assets, no JavaScript, adapts to the
|
|
296
|
+
viewer's light/dark theme.
|
|
297
|
+
- **JUnit XML** (`report_junit:` in the config, or `--report-junit`) — each
|
|
298
|
+
category as a `<testcase>`, so a Cotter run shows up as native test
|
|
299
|
+
results in GitHub Actions, GitLab, Jenkins, CircleCI, etc.
|
|
300
|
+
- **Markdown** (`report_md:` in the config, or `--report-md`) — a shareable
|
|
301
|
+
plain-text report to paste into a PR, an issue, or a wiki.
|
|
302
|
+
|
|
303
|
+
```sh
|
|
304
|
+
poetry run cotter run \
|
|
305
|
+
--policy artifacts/victim_ppo_inverted_pendulum.zip \
|
|
306
|
+
--config examples/inverted_pendulum.yaml \
|
|
307
|
+
--report report.json --report-html report.html
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The HTML page is the *free* counterpart to the paid compliance layer: the
|
|
311
|
+
open engine renders the pass/fail evidence into something shareable; the
|
|
312
|
+
licensed layer renders it into a signed, regulator-ready technical file.
|
|
313
|
+
|
|
314
|
+
### Reproducibility manifest
|
|
315
|
+
|
|
316
|
+
Every report (schema v2+) carries a `manifest` recording the full
|
|
317
|
+
provenance of the run — so a result can be reproduced and audited, not
|
|
318
|
+
just read:
|
|
319
|
+
|
|
320
|
+
```json
|
|
321
|
+
"manifest": {
|
|
322
|
+
"cotter_version": "0.2.0",
|
|
323
|
+
"python_version": "3.11.9",
|
|
324
|
+
"platform": "Linux-6.5.0-x86_64",
|
|
325
|
+
"dependencies": { "gymnasium": "1.0.0", "mujoco": "3.2.3", "torch": "2.4.1", ... },
|
|
326
|
+
"env_id": "InvertedPendulum-v5",
|
|
327
|
+
"base_seed": 0,
|
|
328
|
+
"policy_path": "artifacts/victim.zip",
|
|
329
|
+
"policy_sha256": "sha256:1f3a…"
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The report also includes `content_sha256`, a digest over the report body
|
|
334
|
+
(excluding the timestamp and the digest itself). It is a stable id and a
|
|
335
|
+
tamper-evidence check.
|
|
336
|
+
|
|
337
|
+
Verify a report's integrity with `cotter verify`:
|
|
338
|
+
|
|
339
|
+
```sh
|
|
340
|
+
cotter verify report.json # recompute + check content_sha256
|
|
341
|
+
cotter verify report.json --policy p.zip # also re-hash the policy vs the manifest
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Exit 0 means verified, 1 means a check failed (the report or policy was
|
|
345
|
+
modified), 2 means the report is missing or invalid.
|
|
346
|
+
|
|
106
347
|
### Comparing two policy versions
|
|
107
348
|
|
|
108
349
|
`cotter compare` runs only the regression test between a baseline and a
|
|
@@ -115,6 +356,24 @@ poetry run cotter compare \
|
|
|
115
356
|
--config examples/inverted_pendulum.yaml
|
|
116
357
|
```
|
|
117
358
|
|
|
359
|
+
Like `run`, it accepts `--report` (JSON) and `--report-html` to write the
|
|
360
|
+
comparison result to disk — handy for archiving the artifact or feeding a
|
|
361
|
+
PR comment.
|
|
362
|
+
|
|
363
|
+
### Diffing reports (safety budget)
|
|
364
|
+
|
|
365
|
+
Where `compare` re-runs the regression test, `cotter diff` compares two
|
|
366
|
+
existing **reports** and fails if any previously-passing check now fails —
|
|
367
|
+
a safety-budget gate across the *whole* battery, not just regression:
|
|
368
|
+
|
|
369
|
+
```sh
|
|
370
|
+
cotter diff baseline_report.json candidate_report.json
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
Each check is classified regressed / improved / added / removed; exit 1
|
|
374
|
+
means a check that used to pass now fails (0 otherwise, 2 on a bad
|
|
375
|
+
report). Store a baseline report and diff every retrain against it.
|
|
376
|
+
|
|
118
377
|
### Parallel rollouts
|
|
119
378
|
|
|
120
379
|
Safety, regression, and fixed-N adversarial evaluation parallelize across
|
|
@@ -162,7 +421,7 @@ from cotter import (
|
|
|
162
421
|
# magnitudes in every step's info dict.
|
|
163
422
|
env = CotterWrapper(gym.make("InvertedPendulum-v5"))
|
|
164
423
|
|
|
165
|
-
# 2. Load the policy under test (SB3 .zip
|
|
424
|
+
# 2. Load the policy under test (SB3 .zip, raw torch .pt, or .onnx).
|
|
166
425
|
# Observation/action spaces are validated and mismatches fail loudly.
|
|
167
426
|
policy = load_policy("artifacts/victim_ppo_inverted_pendulum.zip", env, algo=PPO)
|
|
168
427
|
|
|
@@ -184,13 +443,14 @@ safe = evaluate_safety(rollouts.episode_infos, [
|
|
|
184
443
|
|
|
185
444
|
adv = run_adversarial_test(policy, env, success, epsilon=0.07, n_episodes=20)
|
|
186
445
|
|
|
187
|
-
# 4. Aggregate into a report (console summary + JSON
|
|
446
|
+
# 4. Aggregate into a report (console summary + JSON + HTML artifacts).
|
|
188
447
|
report = TestReport(policy_name="my_policy", env_id="InvertedPendulum-v5")
|
|
189
448
|
report.add_sprt(perf)
|
|
190
449
|
report.add_safety(safe)
|
|
191
450
|
report.add_adversarial(adv)
|
|
192
451
|
print(report.summary())
|
|
193
452
|
report.to_json("report.json")
|
|
453
|
+
report.to_html("report.html") # self-contained, shareable page
|
|
194
454
|
```
|
|
195
455
|
|
|
196
456
|
## Demo
|
|
@@ -347,8 +607,28 @@ Trained adversaries are expensive and specific to what they attack, so
|
|
|
347
607
|
adversarial section and the first run trains and stores the attacker;
|
|
348
608
|
later runs on the same victim reuse it instead of retraining. The victim
|
|
349
609
|
hash is taken over the policy artifact (or its parameter tensors), so a
|
|
350
|
-
retrained victim never silently reuses a stale adversary.
|
|
351
|
-
|
|
610
|
+
retrained victim never silently reuses a stale adversary.
|
|
611
|
+
|
|
612
|
+
### Pretrained adversaries (transfer attacks)
|
|
613
|
+
|
|
614
|
+
`cotter.zoo.PretrainedZoo` is a different registry: keyed by **robot
|
|
615
|
+
class** — `(robot_class, env_id, epsilon, attack)` — not by victim. An
|
|
616
|
+
adversary trained per class transfers to *any* compatible victim on that
|
|
617
|
+
env (adversarial policies exploit the environment, not victim-specific
|
|
618
|
+
weights), so you can attack your own policy without training one:
|
|
619
|
+
|
|
620
|
+
```yaml
|
|
621
|
+
adversarial:
|
|
622
|
+
epsilon: 0.05
|
|
623
|
+
pretrained: franka-panda # transfer-attack from the pretrained zoo
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
The run loads the class adversary, checks it matches the env, and reports
|
|
627
|
+
a `pretrained_<class>_<surface>` category (a missing class or incompatible
|
|
628
|
+
adversary is skipped, never fatal). Inspect the registry with
|
|
629
|
+
`cotter pretrained list`. The open engine ships the mechanism
|
|
630
|
+
(`register` / `transfer_attack`); a hosted library of GPU-trained experts
|
|
631
|
+
per robot class is the paid tier.
|
|
352
632
|
|
|
353
633
|
## Compliance layer (paid tier)
|
|
354
634
|
|
|
@@ -380,7 +660,8 @@ cotter/
|
|
|
380
660
|
│ ├── registry.py # resolves gymnasium-robotics (and other) extension envs
|
|
381
661
|
│ └── factory.py # picklable env factory for parallel rollouts
|
|
382
662
|
├── report.py # TestReport: console summary + JSON (results container only)
|
|
383
|
-
├──
|
|
663
|
+
├── render.py # self-contained HTML rendering of a TestReport (free tier)
|
|
664
|
+
├── zoo/ # adversary registries: victim-keyed cache + class-keyed pretrained
|
|
384
665
|
├── compliance/ # paid-tier regulatory document stub (license required)
|
|
385
666
|
└── tests/
|
|
386
667
|
├── sprt.py # Wald's sequential probability ratio test (+ CI)
|