rlshiftlab 0.2.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.
Files changed (37) hide show
  1. rlshiftlab-0.2.0/.github/workflows/ci.yml +22 -0
  2. rlshiftlab-0.2.0/.gitignore +8 -0
  3. rlshiftlab-0.2.0/CHANGELOG.md +41 -0
  4. rlshiftlab-0.2.0/LICENSE +21 -0
  5. rlshiftlab-0.2.0/PKG-INFO +341 -0
  6. rlshiftlab-0.2.0/PLAN.md +254 -0
  7. rlshiftlab-0.2.0/PLAN_V0.2.md +313 -0
  8. rlshiftlab-0.2.0/README.md +324 -0
  9. rlshiftlab-0.2.0/docs/.gitkeep +0 -0
  10. rlshiftlab-0.2.0/docs/cliffwalking_degradation.png +0 -0
  11. rlshiftlab-0.2.0/examples/.gitkeep +0 -0
  12. rlshiftlab-0.2.0/examples/cartpole_heuristic.py +70 -0
  13. rlshiftlab-0.2.0/examples/cliffwalking_multiseed.py +236 -0
  14. rlshiftlab-0.2.0/examples/cliffwalking_q_vs_sarsa.py +132 -0
  15. rlshiftlab-0.2.0/examples/custom_metrics.py +90 -0
  16. rlshiftlab-0.2.0/pyproject.toml +40 -0
  17. rlshiftlab-0.2.0/src/rlshiftlab/__init__.py +23 -0
  18. rlshiftlab-0.2.0/src/rlshiftlab/comparisons.py +301 -0
  19. rlshiftlab-0.2.0/src/rlshiftlab/exports.py +98 -0
  20. rlshiftlab-0.2.0/src/rlshiftlab/groups.py +440 -0
  21. rlshiftlab-0.2.0/src/rlshiftlab/py.typed +0 -0
  22. rlshiftlab-0.2.0/src/rlshiftlab/records.py +50 -0
  23. rlshiftlab-0.2.0/src/rlshiftlab/report.py +273 -0
  24. rlshiftlab-0.2.0/src/rlshiftlab/results.py +200 -0
  25. rlshiftlab-0.2.0/src/rlshiftlab/runner.py +458 -0
  26. rlshiftlab-0.2.0/src/rlshiftlab/shifts.py +170 -0
  27. rlshiftlab-0.2.0/src/rlshiftlab/stats.py +208 -0
  28. rlshiftlab-0.2.0/tests/.gitkeep +0 -0
  29. rlshiftlab-0.2.0/tests/test_comparisons.py +214 -0
  30. rlshiftlab-0.2.0/tests/test_crossed_stats.py +122 -0
  31. rlshiftlab-0.2.0/tests/test_groups.py +175 -0
  32. rlshiftlab-0.2.0/tests/test_metrics.py +287 -0
  33. rlshiftlab-0.2.0/tests/test_results.py +178 -0
  34. rlshiftlab-0.2.0/tests/test_runner.py +276 -0
  35. rlshiftlab-0.2.0/tests/test_shifts.py +245 -0
  36. rlshiftlab-0.2.0/tests/test_stats.py +104 -0
  37. rlshiftlab-0.2.0/tests/test_v02_outputs.py +214 -0
@@ -0,0 +1,22 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ checks:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ python-version: ["3.9", "3.12"]
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - name: Install development dependencies
18
+ run: python -m pip install -e ".[dev]"
19
+ - name: Run tests
20
+ run: python -m pytest -q
21
+ - name: Run Ruff
22
+ run: python -m ruff check .
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ *.egg-info/
7
+ build/
8
+ dist/
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 (unreleased)
4
+
5
+ - Added evaluation of caller-supplied groups of frozen policies, with equally
6
+ weighted member means, descriptive observed member spread, and pointwise
7
+ crossed member/episode bootstrap confidence intervals.
8
+ - Added direct left-minus-right comparisons for single policies and groups
9
+ on complete, paired evaluation episode keys. Group comparisons resample
10
+ training members independently on each side.
11
+ - Added step-contribution and episode-summary callbacks for custom scalar
12
+ per-episode metrics, with raw episode records for audit.
13
+ - Added separate group, member, custom-metric, episode, and comparison CSVs;
14
+ custom metric and group plots; and self-contained HTML reports that can
15
+ include explicitly selected comparisons.
16
+ - Added a multi-seed CliffWalking example and a small custom-metrics API
17
+ example. The package continues to evaluate already frozen policies; the
18
+ examples keep any training code outside the package.
19
+ - Preserved the v0.1 single-policy API and its 14-column CSV, with a v0.1
20
+ baseline archive and reproducibility manifest in `artifacts/v0.1/`.
21
+
22
+ Intervals are pointwise and approximate, and observed member spread also
23
+ contains finite evaluation noise. Local packaging and install validation is
24
+ recorded in `WORK_LOG.md`; publication remains a separate human action.
25
+
26
+ ## 0.1.0 (unreleased)
27
+
28
+ - Added an installable, typed Python package with three controlled shifts:
29
+ observation noise, action slip, and sticky actions.
30
+ - Added paired, seeded evaluation of frozen callable policies and confidence
31
+ intervals for mean return, nominal-minus-shifted drop, and optional success
32
+ rate.
33
+ - Added deterministic tidy CSV export, a text summary, headless degradation
34
+ plots, and a self-contained HTML report.
35
+ - Added runnable CliffWalking Q-learning/SARSA and CartPole heuristic examples,
36
+ including generated CSV, HTML, and PNG artifacts.
37
+ - Added a pytest suite and GitHub Actions pytest and Ruff checks on Python
38
+ 3.9 and 3.12.
39
+ - Added the project specification, usage documentation, and MIT license.
40
+
41
+ Its packaging check was deferred to the v0.2 final validation gate.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RLShiftLab contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,341 @@
1
+ Metadata-Version: 2.5
2
+ Name: rlshiftlab
3
+ Version: 0.2.0
4
+ Summary: Stress-test frozen RL policies under controlled shifts and get degradation curves with confidence intervals.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: gymnasium<2,>=0.29
9
+ Requires-Dist: matplotlib>=3.6
10
+ Requires-Dist: numpy>=1.23
11
+ Provides-Extra: dev
12
+ Requires-Dist: build; extra == 'dev'
13
+ Requires-Dist: pytest>=7; extra == 'dev'
14
+ Requires-Dist: ruff; extra == 'dev'
15
+ Requires-Dist: twine; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # RLShiftLab
19
+
20
+ RLShiftLab 0.2.0 evaluates frozen policies under controlled observation and
21
+ action shifts. A single-policy run produces a degradation curve: shift
22
+ strength on the x axis, mean episode return with a pointwise 95% confidence
23
+ interval on the y axis. Version 0.2 also evaluates groups of already trained
24
+ policies, reports the observed spread across their members, compares policies
25
+ directly on paired evaluation episodes, and records custom per-episode
26
+ metrics. Each shift is varied separately. Reports describe measured
27
+ differences and do not establish their causes.
28
+
29
+ ## Install locally
30
+
31
+ Requires Python 3.9 or newer. From a checkout of this repository:
32
+
33
+ ```sh
34
+ python -m pip install -e .
35
+ ```
36
+
37
+ For tests and lint checks, use `python -m pip install -e ".[dev]"`, then
38
+ `python -m pytest -q` and `python -m ruff check .`.
39
+
40
+ ## Quickstart
41
+
42
+ Save this as `quickstart.py` and run `python quickstart.py` from a writable
43
+ directory after installing the package:
44
+
45
+ ```python
46
+ import gymnasium as gym
47
+ from rlshiftlab import ActionSlip, ObservationNoise, Suite
48
+
49
+ def policy(obs):
50
+ return int(obs[2] + 0.5 * obs[3] > 0)
51
+
52
+ suite = Suite(
53
+ env_fn=lambda: gym.make("CartPole-v1"),
54
+ shifts=[ObservationNoise(levels=[0.01, 0.05]),
55
+ ActionSlip(levels=[0.05, 0.1])],
56
+ n_episodes=20,
57
+ seed=0,
58
+ success_fn=lambda ep: ep.truncated,
59
+ )
60
+ results = suite.evaluate({"angle+velocity": policy})
61
+
62
+ print(results.summary())
63
+ results.to_csv("results.csv")
64
+ results.plot("degradation.png")
65
+ results.to_html("report.html")
66
+ ```
67
+
68
+ `policy` is a callable from observation to action. RLShiftLab does not train
69
+ or alter it. Use a deterministic policy for paired comparisons; if a policy
70
+ has a `reset()` method, RLShiftLab calls it at the start of each episode.
71
+ The suite gives every policy and condition the same sequence of environment
72
+ reset seeds. Shift randomness is also seeded per episode, and reused across
73
+ levels and policies for the same shift. Repeated runs with the same inputs and
74
+ seed produce identical CSV rows.
75
+
76
+ The nominal row has no shift. For shifted rows, **drop** is the nominal episode
77
+ return minus the shifted return, paired by episode seed; a positive value means
78
+ lower return under the shift. Mean-return and drop intervals use percentile
79
+ bootstrap over episodes. Optional success-rate intervals use the Wilson
80
+ method. The CSV includes mean episode length and, when `success_fn` is given,
81
+ success rate. A policy's behavior can still be stateful or stochastic if its
82
+ callable is written that way; the suite cannot guarantee reproducibility for
83
+ randomness that the policy manages independently. This original
84
+ `Suite.evaluate()` interface and its 14-column `to_csv()` export remain
85
+ compatible with v0.1.
86
+
87
+ ## Shifts and outputs
88
+
89
+ - `ObservationNoise`: Gaussian noise for Box observations; random replacement
90
+ for Discrete observations.
91
+ - `ActionSlip`: random replacement of an action before it reaches the environment.
92
+ - `StickyActions`: repeat the previously executed action with a given probability.
93
+
94
+ Discrete random replacement may select the original value. At replacement
95
+ probability `p` with `n` equally likely values, the observed mismatch rate is
96
+ `p * (1 - 1/n)`. The level is the replacement probability, not a guaranteed
97
+ mismatch rate.
98
+
99
+ `Results.to_csv()` writes tidy rows; `Results.plot()` returns a Matplotlib
100
+ figure and optionally writes a PNG; `Results.to_html()` writes a self-contained
101
+ report with the plot, setup, summary, and full results table. Plotting uses the
102
+ headless Agg canvas.
103
+
104
+ ## v0.2: groups of frozen policies
105
+
106
+ Supply a mapping from group name to member ID to an **already trained**
107
+ callable policy. Each member ID should identify a separately trained frozen
108
+ policy, such as a run with its own training seed. Multiple checkpoints from
109
+ one training run are not independent training members. RLShiftLab evaluates
110
+ the supplied members; it does not train them or verify their independence.
111
+ The runnable multi-seed CliffWalking example in `examples/` shows how to
112
+ train the members outside the package and pass their frozen policies to
113
+ `evaluate_groups()`.
114
+
115
+ For example, after creating four frozen CliffWalking callables in your own
116
+ training code, configure `suite` with that CliffWalking environment and its
117
+ evaluation shifts. The earlier CartPole quickstart suite is for CartPole
118
+ policies and cannot evaluate these CliffWalking tables:
119
+
120
+ ```python
121
+ groups = {
122
+ "Q-learning": {"train-0": q_policy_0, "train-1": q_policy_1},
123
+ "SARSA": {"train-0": sarsa_policy_0, "train-1": sarsa_policy_1},
124
+ }
125
+ group_results = suite.evaluate_groups(groups)
126
+ group_results.to_csv("groups.csv")
127
+ group_results.to_member_csv("members.csv")
128
+ group_results.to_episode_csv("episodes.csv")
129
+ group_results.plot("group_degradation.png", band="ci")
130
+ group_results.plot("member_spread.png", band="std")
131
+
132
+ contrast = group_results.compare("Q-learning", "SARSA", metric="return")
133
+ contrast.to_csv("comparison.csv")
134
+ contrast.plot("comparison.png")
135
+ group_results.to_html("group_report.html", comparisons=[contrast])
136
+ ```
137
+
138
+ The same `Suite` configuration works for both single policies and groups.
139
+ At least two members are required per group, but groups may have different
140
+ member counts. Each member receives the same ordered evaluation reset seeds
141
+ at every condition. The group mean first averages episodes within a member,
142
+ then gives each member equal weight. The reported `member_std`, minimum, and
143
+ maximum describe the observed spread of member means. That spread includes
144
+ finite evaluation noise and is neither a confidence interval nor a pure
145
+ estimate of training variance.
146
+
147
+ Group confidence intervals use a crossed bootstrap that resamples members
148
+ and evaluation episode keys. They are approximate **pointwise** 95%
149
+ intervals, not simultaneous bands or multiple-comparison-adjusted claims.
150
+ Intervals from only a few members may be unstable. Single-policy success
151
+ rates retain Wilson intervals; group success rates use the crossed bootstrap.
152
+ The [pigeonhole bootstrap paper](https://arxiv.org/abs/0712.1111) informs the
153
+ two-axis resampling design, and [Agarwal et al.'s RL evaluation
154
+ paper](https://arxiv.org/abs/2108.13264) motivates careful reporting across
155
+ trained runs. Neither source guarantees exact coverage for a particular
156
+ RLShiftLab experiment.
157
+
158
+ ## Direct comparisons and custom metrics
159
+
160
+ Call `Results.compare(left, right, metric="return")` for two individual
161
+ policies, or `GroupResults.compare(...)` for two groups. Every nominal and
162
+ shifted comparison uses the same evaluation episode keys on both sides;
163
+ missing or duplicate keys are errors. The estimate is **left minus right**.
164
+ A positive value means the left side has a larger measured metric, which is
165
+ not always better (for example, more falls or longer episodes). Single-policy
166
+ comparisons bootstrap the paired episode differences. Group comparisons
167
+ resample members independently on each side while sharing evaluation episode
168
+ keys; equal member ID strings do not assert matched training runs. Each
169
+ contrast has its own pointwise 95% interval and can be saved as CSV, a
170
+ zero-reference plot, or standalone HTML. Pass a list to `to_html(...,
171
+ comparisons=[contrast])` to include chosen comparisons in an evaluation
172
+ report.
173
+
174
+ Pass `step_metrics` and `episode_metrics` to `Suite` to collect scalar metrics
175
+ in both single and group runs. Each step callback contributes a finite
176
+ number; RLShiftLab sums it over the episode. For the configured CliffWalking
177
+ environment, a reward of -100 marks a fall, so this metric counts falls:
178
+
179
+ ```python
180
+ suite = Suite(
181
+ env_fn=make_cliffwalking_env,
182
+ shifts=[ActionSlip([0.1, 0.2])],
183
+ n_episodes=100,
184
+ seed=1000,
185
+ step_metrics={"cliff_falls": lambda t: float(t.reward == -100.0)},
186
+ episode_metrics={"return_per_step": lambda ep: ep.episode_return / ep.length},
187
+ )
188
+ ```
189
+
190
+ Here `make_cliffwalking_env` is your environment factory. The -100 rule is
191
+ specific to that environment, not a generic fall detector. Step callbacks
192
+ receive a `Transition` with policy-visible observations, requested action,
193
+ next observation, reward, termination/truncation flags, step index, episode
194
+ seed, and step `info`. Its requested action may differ from the action
195
+ actually executed after action slip or sticky actions. Episode callbacks
196
+ receive a copy of the final `EpisodeResult`, including step metric totals,
197
+ length, return, flags, and final step `info`. Each callback must return one
198
+ finite numeric scalar; no full trajectory is retained. The resulting
199
+ `metric_rows` and `to_metrics_csv()` summarize each metric, while
200
+ `to_episode_csv()` includes scalar values under `metric:<name>` columns.
201
+
202
+ The raw episode CSV records policy/group, member, shift, level, episode index,
203
+ reset seed, return, length, flags, optional success, and custom scalars. It
204
+ does not serialize arbitrary observations or `info` objects. These records
205
+ make pairing and reported aggregates auditable.
206
+
207
+ Repeated runs with the same inputs, software versions, suite seed, and pure
208
+ deterministic or consistently resettable policy and metric callbacks produce
209
+ the same CSV bytes. Suite seeds control Gymnasium resets and shift randomness;
210
+ they do not control randomness inside arbitrary user policies or callbacks.
211
+
212
+ | Result object | Main export | Additional exports |
213
+ | --- | --- | --- |
214
+ | `Results` | `to_csv(path)` keeps the v0.1 14 columns | `to_metrics_csv(path)`, `to_episode_csv(path)`, `plot(metric=...)`, `to_html(path, comparisons=...)` |
215
+ | `GroupResults` | `to_csv(path)` writes aggregate rows | `to_member_csv(path)`, `to_metrics_csv(path)`, `to_episode_csv(path)`, `plot(metric=..., band="ci" or "std")`, `to_html(path, comparisons=...)` |
216
+ | `ComparisonResults` | `to_csv(path)` writes left-minus-right rows | `plot(path)`, `to_html(path)`, `summary()` |
217
+
218
+ ## v0.2 examples
219
+
220
+ Run either example from the repository root:
221
+
222
+ ```sh
223
+ python examples/cliffwalking_multiseed.py
224
+ python examples/custom_metrics.py
225
+ ```
226
+
227
+ The [multi-seed CliffWalking example](examples/cliffwalking_multiseed.py)
228
+ trains its Q-learning and SARSA tables in the example code, then evaluates
229
+ them as frozen members. It writes separate group, member, metric, episode,
230
+ and paired-comparison files, plus plots and HTML, under
231
+ [`artifacts/v0.2/cliffwalking/`](artifacts/v0.2/cliffwalking/).
232
+ The [small custom-metrics example](examples/custom_metrics.py) uses fixed toy
233
+ policies to demonstrate step and episode metric callbacks and comparison
234
+ exports under [`artifacts/v0.2/custom_metrics/`](artifacts/v0.2/custom_metrics/).
235
+ Its outputs illustrate the API, not a research benchmark. The package does
236
+ not perform the training in either case.
237
+
238
+ ### Measured multi-seed CliffWalking run
239
+
240
+ The generated [v0.2 group results](artifacts/v0.2/cliffwalking/group.csv)
241
+ used deterministic CliffWalking-v1, ten Q-learning training seeds (0–9) and
242
+ ten SARSA training seeds (100–109), with 500 training episodes per member.
243
+ Each frozen member was evaluated on the same 100 reset seeds per condition
244
+ for one nominal condition and eight shift levels (18,000 raw episodes).
245
+ [run.json](artifacts/v0.2/cliffwalking/run.json) records the configuration,
246
+ software versions, and source/output hashes.
247
+
248
+ Nominal mean return across members was -13.000 for Q-learning (observed
249
+ member SD 0.000) and -35.300 for SARSA (SD 57.870). In the
250
+ [member results](artifacts/v0.2/cliffwalking/member.csv), SARSA training seed
251
+ 100 had nominal return -200, length 200, and no success; each of the other
252
+ nine SARSA seeds had return -17, length 17, and success. These member scores
253
+ underlie the reported nominal spread; they do not identify why that one
254
+ training outcome occurred.
255
+
256
+ At action slip 0.3, group mean returns were -149.928 for Q-learning and
257
+ -41.324 for SARSA. The direct Q-learning-minus-SARSA return difference was
258
+ -108.604, with a pointwise 95% CI of [-138.084, -79.740]. The corresponding
259
+ [cliff-fall difference](artifacts/v0.2/cliffwalking/falls_comparison.csv) was
260
+ +1.066 falls per episode [0.808, 1.329], so Q-learning had more recorded
261
+ falls under that shift in this configured run. These are measured differences
262
+ for these trained tables, environment settings, and evaluation seeds; they
263
+ are not causal explanations or a general ranking of the algorithms.
264
+
265
+ ![v0.2 multi-seed CliffWalking group return curves](artifacts/v0.2/cliffwalking/mean_return_ci.png)
266
+
267
+ The [return comparison CSV](artifacts/v0.2/cliffwalking/return_comparison.csv),
268
+ [episode audit CSV](artifacts/v0.2/cliffwalking/episodes.csv), and
269
+ [standalone report](artifacts/v0.2/cliffwalking/report.html) provide the
270
+ condition-level and per-episode evidence. A repeated execution with the
271
+ same configuration produced byte-identical hashes for all 13 generated
272
+ outputs. That repeatability applies to this deterministic example and
273
+ software setup, not to arbitrary user-supplied stochastic callbacks.
274
+
275
+ The [toy custom-metrics outputs](artifacts/v0.2/custom_metrics/) use two
276
+ fixed policies and 20 paired evaluation episodes per condition. At action
277
+ slip 0.5, the measured always-zero-minus-always-one return difference was
278
+ 0.85 [0.5, 1.2], while its unsafe-event difference was -0.5 [-0.7, -0.3].
279
+ This demonstrates the metric and comparison APIs; it is not a research
280
+ benchmark.
281
+
282
+ ## Historical v0.1 examples and measured results
283
+
284
+ The original v0.1 repository contains a CliffWalking Q-learning versus SARSA example and a
285
+ CartPole comparison of two hand-coded policies in `examples/`. Training code
286
+ for CliffWalking stays in the example; the RLShiftLab package evaluates only
287
+ the resulting frozen policies. The CliffWalking example uses the registered
288
+ `CliffWalking-v0` when available, or `CliffWalking-v1` with
289
+ `is_slippery=False` on newer Gymnasium versions. See the [Gymnasium
290
+ CliffWalking documentation](https://gymnasium.farama.org/v1.3.0/environments/toy_text/cliff_walking/)
291
+ for the v1 environment and its optional slippery mode.
292
+
293
+ Run the examples from the repository root:
294
+
295
+ ```sh
296
+ python examples/cliffwalking_q_vs_sarsa.py
297
+ python examples/cartpole_heuristic.py
298
+ ```
299
+
300
+ The CliffWalking run writes [results.csv](results.csv),
301
+ [report.html](report.html), and the figure below. The CartPole run writes
302
+ [results.csv](artifacts/cartpole/results.csv),
303
+ [report.html](artifacts/cartpole/report.html), and
304
+ [degradation.png](artifacts/cartpole/degradation.png) under
305
+ `artifacts/cartpole/`.
306
+
307
+ ![Measured CliffWalking degradation curves](docs/cliffwalking_degradation.png)
308
+
309
+ In the generated CliffWalking results (200 evaluation episodes per condition),
310
+ nominal mean return was -13.000 for Q-learning and -19.000 for SARSA. At action
311
+ slip 0.3, mean return was -198.250 for Q-learning and -48.815 for SARSA. The
312
+ paired drops from nominal were 185.250 (95% CI [154.945, 220.342]) and 29.815
313
+ (95% CI [23.120, 37.440]), respectively. This run measured a larger drop for
314
+ Q-learning at that shift level. The example trained one table for each
315
+ algorithm with 500 episodes before evaluation; it does not measure variation
316
+ across training seeds.
317
+
318
+ In the generated CartPole results (100 evaluation episodes per condition),
319
+ nominal mean return was 500.000 for the angle+velocity heuristic and 41.040
320
+ for the angle-only heuristic. At observation noise 0.2, their mean returns were
321
+ 267.400 and 38.310, respectively. These values describe the two particular
322
+ frozen policies and the configured evaluation seeds.
323
+
324
+ ## What v0.2 does not do
325
+
326
+ Version 0.2 does not train or adapt policies inside the package, measure
327
+ recovery, apply reward shifts or delays, change environment dynamics, support
328
+ vectorized environments or a CLI, or accept Dict, MultiDiscrete, or Tuple
329
+ observation spaces. A group member ID is caller-provided provenance, not
330
+ proof that training runs were independent. Reports do not provide causal
331
+ explanations or multiplicity-adjusted confidence intervals.
332
+
333
+ ## Roadmap after v0.2
334
+
335
+ Future work may add learners and adaptation with recovery metrics, reward
336
+ shifts and delays, environment-specific dynamics hooks, and Dict and
337
+ MultiDiscrete spaces.
338
+
339
+ ## License
340
+
341
+ MIT; see [LICENSE](LICENSE).