rlshiftlab 0.2.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.
Files changed (50) hide show
  1. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/PKG-INFO +1 -1
  2. rlshiftlab-0.3.0/examples/lab8_qlearning_vs_sarsa.py +150 -0
  3. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/pyproject.toml +1 -1
  4. rlshiftlab-0.3.0/src/rlshiftlab/__init__.py +59 -0
  5. rlshiftlab-0.3.0/src/rlshiftlab/envs/__init__.py +25 -0
  6. rlshiftlab-0.3.0/src/rlshiftlab/envs/gridworld.py +233 -0
  7. rlshiftlab-0.3.0/src/rlshiftlab/tabular/__init__.py +17 -0
  8. rlshiftlab-0.3.0/src/rlshiftlab/tabular/algorithms.py +247 -0
  9. rlshiftlab-0.3.0/src/rlshiftlab/tabular/history.py +121 -0
  10. rlshiftlab-0.3.0/src/rlshiftlab/viz/__init__.py +15 -0
  11. rlshiftlab-0.3.0/src/rlshiftlab/viz/grid.py +243 -0
  12. rlshiftlab-0.3.0/tests/test_gridworld.py +136 -0
  13. rlshiftlab-0.3.0/tests/test_gridworld_suite_integration.py +66 -0
  14. rlshiftlab-0.3.0/tests/test_tabular.py +112 -0
  15. rlshiftlab-0.3.0/tests/test_viz.py +46 -0
  16. rlshiftlab-0.2.0/src/rlshiftlab/__init__.py +0 -23
  17. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/.github/workflows/ci.yml +0 -0
  18. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/.gitignore +0 -0
  19. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/CHANGELOG.md +0 -0
  20. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/LICENSE +0 -0
  21. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/PLAN.md +0 -0
  22. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/PLAN_V0.2.md +0 -0
  23. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/README.md +0 -0
  24. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/docs/.gitkeep +0 -0
  25. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/docs/cliffwalking_degradation.png +0 -0
  26. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/examples/.gitkeep +0 -0
  27. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/examples/cartpole_heuristic.py +0 -0
  28. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/examples/cliffwalking_multiseed.py +0 -0
  29. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/examples/cliffwalking_q_vs_sarsa.py +0 -0
  30. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/examples/custom_metrics.py +0 -0
  31. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/comparisons.py +0 -0
  32. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/exports.py +0 -0
  33. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/groups.py +0 -0
  34. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/py.typed +0 -0
  35. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/records.py +0 -0
  36. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/report.py +0 -0
  37. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/results.py +0 -0
  38. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/runner.py +0 -0
  39. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/shifts.py +0 -0
  40. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/src/rlshiftlab/stats.py +0 -0
  41. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/.gitkeep +0 -0
  42. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_comparisons.py +0 -0
  43. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_crossed_stats.py +0 -0
  44. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_groups.py +0 -0
  45. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_metrics.py +0 -0
  46. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_results.py +0 -0
  47. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_runner.py +0 -0
  48. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_shifts.py +0 -0
  49. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_stats.py +0 -0
  50. {rlshiftlab-0.2.0 → rlshiftlab-0.3.0}/tests/test_v02_outputs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: rlshiftlab
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Stress-test frozen RL policies under controlled shifts and get degradation curves with confidence intervals.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -0,0 +1,150 @@
1
+ """Complete Lab 8 Experiment: Q-Learning vs SARSA in 5x5 GridWorld with RLShiftLab."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from time import perf_counter
7
+
8
+ from rlshiftlab import (
9
+ ActionSlip,
10
+ GridWorldEnv,
11
+ Suite,
12
+ Transition,
13
+ frozen_policy,
14
+ plot_grid_policy,
15
+ plot_q_values,
16
+ plot_training_curves,
17
+ train_q_learning,
18
+ train_sarsa,
19
+ )
20
+
21
+ ROOT = Path(__file__).resolve().parents[1]
22
+ OUTPUT_DIR = ROOT / "artifacts" / "lab8"
23
+
24
+
25
+ def main() -> None:
26
+ started = perf_counter()
27
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
28
+ print("=== RL Lab 8: Q-Learning vs SARSA in 5x5 GridWorld ===")
29
+
30
+ # 1. Initialize 5x5 GridWorld matching Lab 8 specifications
31
+ env = GridWorldEnv(
32
+ rows=5,
33
+ cols=5,
34
+ start_state=20, # S20 (bottom-left)
35
+ goal_state=4, # S4 (top-right)
36
+ obstacles=[6, 11, 13, 17],
37
+ step_reward=-1.0,
38
+ obstacle_reward=-10.0,
39
+ wall_reward=-10.0,
40
+ goal_reward=10.0,
41
+ max_episode_steps=100,
42
+ )
43
+ print("\nInitial Grid World Layout:")
44
+ print(env.render_ascii())
45
+
46
+ # 2. Train Q-Learning (off-policy TD control)
47
+ print("\nTraining Q-Learning agent (500 episodes)...")
48
+ q_table, q_hist = train_q_learning(
49
+ env=env,
50
+ episodes=500,
51
+ alpha=0.5,
52
+ gamma=0.9,
53
+ epsilon=0.1,
54
+ seed=42,
55
+ )
56
+ print(q_hist.summary())
57
+
58
+ # 3. Train SARSA (on-policy TD control)
59
+ print("\nTraining SARSA agent (500 episodes)...")
60
+ s_table, s_hist = train_sarsa(
61
+ env=env,
62
+ episodes=500,
63
+ alpha=0.5,
64
+ gamma=0.9,
65
+ epsilon=0.1,
66
+ seed=42,
67
+ )
68
+ print(s_hist.summary())
69
+
70
+ # 4. Generate Lab 8 Visualizations for Submission
71
+ print("\nGenerating Lab 8 Visualizations...")
72
+ plot_training_curves(
73
+ {"Q-learning": q_hist, "SARSA": s_hist},
74
+ window=15,
75
+ title="Lab 8: Training Convergence Comparison",
76
+ filepath=OUTPUT_DIR / "training_curves.png",
77
+ )
78
+
79
+ plot_grid_policy(
80
+ env=env,
81
+ q_table=q_table,
82
+ title="Q-Learning Optimal Policy (Hugs Obstacles)",
83
+ filepath=OUTPUT_DIR / "q_policy.png",
84
+ )
85
+
86
+ plot_grid_policy(
87
+ env=env,
88
+ q_table=s_table,
89
+ title="SARSA Optimal Policy (Safer Detour)",
90
+ filepath=OUTPUT_DIR / "sarsa_policy.png",
91
+ )
92
+
93
+ plot_q_values(
94
+ env=env,
95
+ q_table=q_table,
96
+ title="Q-Learning State Values V(s)",
97
+ filepath=OUTPUT_DIR / "q_values.png",
98
+ )
99
+
100
+ plot_q_values(
101
+ env=env,
102
+ q_table=s_table,
103
+ title="SARSA State Values V(s)",
104
+ filepath=OUTPUT_DIR / "sarsa_values.png",
105
+ )
106
+
107
+ # 5. RLShiftLab Robustness Evaluation
108
+ print("\nRunning RLShiftLab Robustness Evaluation under Action Slips...")
109
+ q_policy = frozen_policy(q_table)
110
+ sarsa_policy = frozen_policy(s_table)
111
+
112
+ def count_obstacles(transition: Transition) -> float:
113
+ return float(transition.info.get("hit_obstacle", False))
114
+
115
+ suite = Suite(
116
+ env_fn=lambda: GridWorldEnv(
117
+ rows=5,
118
+ cols=5,
119
+ start_state=20,
120
+ goal_state=4,
121
+ obstacles=[6, 11, 13, 17],
122
+ max_episode_steps=100,
123
+ ),
124
+ shifts=[
125
+ ActionSlip(levels=[0.05, 0.1, 0.2, 0.3]),
126
+ ],
127
+ n_episodes=100,
128
+ seed=123,
129
+ success_fn=lambda ep: ep.terminated,
130
+ step_metrics={"obstacle_hits": count_obstacles},
131
+ )
132
+
133
+ results = suite.evaluate({"Q-learning": q_policy, "SARSA": sarsa_policy})
134
+ print("\nRobustness Evaluation Summary:")
135
+ print(results.summary())
136
+
137
+ results.to_csv(OUTPUT_DIR / "robustness_results.csv")
138
+ results.plot(OUTPUT_DIR / "robustness_degradation.png")
139
+ results.to_html(OUTPUT_DIR / "lab8_report.html")
140
+
141
+ contrast = results.compare("Q-learning", "SARSA", metric="return")
142
+ contrast.to_csv(OUTPUT_DIR / "comparison_return.csv")
143
+ contrast.plot(OUTPUT_DIR / "comparison_return.png")
144
+
145
+ print(f"\nAll artifacts generated in: {OUTPUT_DIR}")
146
+ print(f"Total execution time: {perf_counter() - started:.2f}s")
147
+
148
+
149
+ if __name__ == "__main__":
150
+ main()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rlshiftlab"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "Stress-test frozen RL policies under controlled shifts and get degradation curves with confidence intervals."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -0,0 +1,59 @@
1
+ """Stress-test frozen reinforcement learning policies under controlled shifts."""
2
+
3
+ __version__ = "0.3.0"
4
+
5
+ from rlshiftlab.comparisons import ComparisonResults
6
+ from rlshiftlab.envs.gridworld import (
7
+ ACTION_DOWN,
8
+ ACTION_LEFT,
9
+ ACTION_NAMES,
10
+ ACTION_RIGHT,
11
+ ACTION_SYMBOLS,
12
+ ACTION_UP,
13
+ DEFAULT_OBSTACLES,
14
+ GridWorldEnv,
15
+ )
16
+ from rlshiftlab.groups import GroupResults
17
+ from rlshiftlab.results import Results
18
+ from rlshiftlab.runner import EpisodeRecord, EpisodeResult, Suite, Transition
19
+ from rlshiftlab.shifts import ActionSlip, ObservationNoise, StickyActions
20
+ from rlshiftlab.tabular.algorithms import (
21
+ frozen_policy,
22
+ train_q_learning,
23
+ train_sarsa,
24
+ )
25
+ from rlshiftlab.tabular.history import TrainingHistory
26
+ from rlshiftlab.viz.grid import (
27
+ plot_grid_policy,
28
+ plot_q_values,
29
+ plot_training_curves,
30
+ )
31
+
32
+ __all__ = [
33
+ "ACTION_DOWN",
34
+ "ACTION_LEFT",
35
+ "ACTION_NAMES",
36
+ "ACTION_RIGHT",
37
+ "ACTION_SYMBOLS",
38
+ "ACTION_UP",
39
+ "DEFAULT_OBSTACLES",
40
+ "ActionSlip",
41
+ "ComparisonResults",
42
+ "EpisodeRecord",
43
+ "EpisodeResult",
44
+ "GridWorldEnv",
45
+ "GroupResults",
46
+ "ObservationNoise",
47
+ "Results",
48
+ "StickyActions",
49
+ "Suite",
50
+ "TrainingHistory",
51
+ "Transition",
52
+ "__version__",
53
+ "frozen_policy",
54
+ "plot_grid_policy",
55
+ "plot_q_values",
56
+ "plot_training_curves",
57
+ "train_q_learning",
58
+ "train_sarsa",
59
+ ]
@@ -0,0 +1,25 @@
1
+ """Custom environments for RLShiftLab experiments."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rlshiftlab.envs.gridworld import (
6
+ ACTION_DOWN,
7
+ ACTION_LEFT,
8
+ ACTION_NAMES,
9
+ ACTION_RIGHT,
10
+ ACTION_SYMBOLS,
11
+ ACTION_UP,
12
+ DEFAULT_OBSTACLES,
13
+ GridWorldEnv,
14
+ )
15
+
16
+ __all__ = [
17
+ "ACTION_DOWN",
18
+ "ACTION_LEFT",
19
+ "ACTION_NAMES",
20
+ "ACTION_RIGHT",
21
+ "ACTION_SYMBOLS",
22
+ "ACTION_UP",
23
+ "DEFAULT_OBSTACLES",
24
+ "GridWorldEnv",
25
+ ]
@@ -0,0 +1,233 @@
1
+ """Gymnasium-compliant 5x5 GridWorld environment for RL Lab 8."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from typing import Any, ClassVar
7
+
8
+ import gymnasium as gym
9
+ import numpy as np
10
+
11
+ ACTION_UP = 0
12
+ ACTION_DOWN = 1
13
+ ACTION_LEFT = 2
14
+ ACTION_RIGHT = 3
15
+
16
+ ACTION_NAMES: dict[int, str] = {
17
+ ACTION_UP: "UP",
18
+ ACTION_DOWN: "DOWN",
19
+ ACTION_LEFT: "LEFT",
20
+ ACTION_RIGHT: "RIGHT",
21
+ }
22
+
23
+ ACTION_SYMBOLS: dict[int, str] = {
24
+ ACTION_UP: "^",
25
+ ACTION_DOWN: "v",
26
+ ACTION_LEFT: "<",
27
+ ACTION_RIGHT: ">",
28
+ }
29
+
30
+ # Default obstacles matching Lab 8:
31
+ # Row 1 Col 1 -> S6
32
+ # Row 2 Col 1 -> S11
33
+ # Row 2 Col 3 -> S13
34
+ # Row 3 Col 2 -> S17
35
+ DEFAULT_OBSTACLES: frozenset[int] = frozenset({6, 11, 13, 17})
36
+
37
+
38
+ class GridWorldEnv(gym.Env):
39
+ """A 5x5 (or N x M) navigation grid with obstacles, boundary walls, and goals.
40
+
41
+ Conforms to Gymnasium 0.29+ Env interface with Discrete observation and
42
+ action spaces.
43
+ """
44
+
45
+ metadata: ClassVar[dict[str, Any]] = {"render_modes": ["ansi", "human"]}
46
+
47
+ def __init__(
48
+ self,
49
+ rows: int = 5,
50
+ cols: int = 5,
51
+ start_state: int = 20,
52
+ goal_state: int = 4,
53
+ obstacles: Sequence[int] | set[int] | frozenset[int] | None = None,
54
+ step_reward: float = -1.0,
55
+ obstacle_reward: float = -10.0,
56
+ wall_reward: float = -10.0,
57
+ goal_reward: float = 10.0,
58
+ obstacle_bounce: bool = True,
59
+ max_episode_steps: int = 100,
60
+ render_mode: str | None = None,
61
+ ) -> None:
62
+ super().__init__()
63
+ if rows < 2 or cols < 2:
64
+ raise ValueError(f"rows and cols must be >= 2, got {rows}x{cols}")
65
+ n_states = rows * cols
66
+ if not (0 <= start_state < n_states):
67
+ raise ValueError(f"start_state {start_state} must be in [0, {n_states})")
68
+ if not (0 <= goal_state < n_states):
69
+ raise ValueError(f"goal_state {goal_state} must be in [0, {n_states})")
70
+ if start_state == goal_state:
71
+ raise ValueError("start_state and goal_state must be distinct")
72
+
73
+ self.rows = rows
74
+ self.cols = cols
75
+ self.n_states = n_states
76
+ self.start_state = int(start_state)
77
+ self.goal_state = int(goal_state)
78
+
79
+ if obstacles is None:
80
+ self.obstacles = frozenset(DEFAULT_OBSTACLES)
81
+ else:
82
+ self.obstacles = frozenset(int(s) for s in obstacles)
83
+
84
+ if self.start_state in self.obstacles:
85
+ raise ValueError(f"start_state {self.start_state} cannot be an obstacle")
86
+ if self.goal_state in self.obstacles:
87
+ raise ValueError(f"goal_state {self.goal_state} cannot be an obstacle")
88
+
89
+ self.step_reward = float(step_reward)
90
+ self.obstacle_reward = float(obstacle_reward)
91
+ self.wall_reward = float(wall_reward)
92
+ self.goal_reward = float(goal_reward)
93
+ self.obstacle_bounce = bool(obstacle_bounce)
94
+ self.max_episode_steps = int(max_episode_steps)
95
+ self.render_mode = render_mode
96
+
97
+ self.observation_space = gym.spaces.Discrete(self.n_states)
98
+ self.action_space = gym.spaces.Discrete(4)
99
+
100
+ self._state = self.start_state
101
+ self._step_count = 0
102
+ self._np_random: np.random.Generator = np.random.default_rng()
103
+
104
+ def to_state(self, row: int, col: int) -> int:
105
+ """Convert 2D (row, col) coordinates to 1D state index."""
106
+ return row * self.cols + col
107
+
108
+ def to_coord(self, state: int) -> tuple[int, int]:
109
+ """Convert 1D state index to 2D (row, col) coordinates."""
110
+ return divmod(state, self.cols)
111
+
112
+ def reset(
113
+ self,
114
+ *,
115
+ seed: int | None = None,
116
+ options: dict[str, Any] | None = None,
117
+ ) -> tuple[int, dict[str, Any]]:
118
+ """Reset the environment to the start state."""
119
+ super().reset(seed=seed)
120
+ if seed is not None:
121
+ self._np_random = np.random.default_rng(seed)
122
+
123
+ self._state = self.start_state
124
+ self._step_count = 0
125
+
126
+ info = {
127
+ "row": self.to_coord(self._state)[0],
128
+ "col": self.to_coord(self._state)[1],
129
+ "hit_wall": False,
130
+ "hit_obstacle": False,
131
+ "reached_goal": False,
132
+ }
133
+ return self._state, info
134
+
135
+ def step(self, action: int) -> tuple[int, float, bool, bool, dict[str, Any]]:
136
+ """Execute one action transition."""
137
+ action = int(action)
138
+ if not self.action_space.contains(action):
139
+ raise ValueError(f"Invalid action {action!r}; must be in [0, 3]")
140
+
141
+ self._step_count += 1
142
+ r, c = self.to_coord(self._state)
143
+
144
+ # Compute candidate target coordinate
145
+ if action == ACTION_UP:
146
+ nr, nc = r - 1, c
147
+ elif action == ACTION_DOWN:
148
+ nr, nc = r + 1, c
149
+ elif action == ACTION_LEFT:
150
+ nr, nc = r, c - 1
151
+ elif action == ACTION_RIGHT:
152
+ nr, nc = r, c + 1
153
+ else:
154
+ nr, nc = r, c
155
+
156
+ hit_wall = False
157
+ hit_obstacle = False
158
+ reached_goal = False
159
+ terminated = False
160
+
161
+ # 1. Boundary wall check: outside bounds
162
+ if nr < 0 or nr >= self.rows or nc < 0 or nc >= self.cols:
163
+ hit_wall = True
164
+ next_state = self._state # remains in the same state
165
+ reward = self.wall_reward
166
+ else:
167
+ candidate_state = self.to_state(nr, nc)
168
+ # 2. Obstacle check
169
+ if candidate_state in self.obstacles:
170
+ hit_obstacle = True
171
+ reward = self.obstacle_reward
172
+ next_state = self._state if self.obstacle_bounce else candidate_state
173
+ # 3. Goal check
174
+ elif candidate_state == self.goal_state:
175
+ reached_goal = True
176
+ next_state = candidate_state
177
+ reward = self.goal_reward
178
+ terminated = True
179
+ # 4. Standard move
180
+ else:
181
+ next_state = candidate_state
182
+ reward = self.step_reward
183
+
184
+ self._state = next_state
185
+ truncated = self._step_count >= self.max_episode_steps and not terminated
186
+
187
+ info = {
188
+ "row": self.to_coord(self._state)[0],
189
+ "col": self.to_coord(self._state)[1],
190
+ "hit_wall": hit_wall,
191
+ "hit_obstacle": hit_obstacle,
192
+ "reached_goal": reached_goal,
193
+ "step_count": self._step_count,
194
+ }
195
+
196
+ if self.render_mode == "human":
197
+ print(self.render_ascii())
198
+
199
+ return self._state, reward, terminated, truncated, info
200
+
201
+ def render_ascii(self) -> str:
202
+ """Render grid as a formatted ASCII string."""
203
+ lines = []
204
+ for r in range(self.rows):
205
+ row_chars = []
206
+ for c in range(self.cols):
207
+ s = self.to_state(r, c)
208
+ if s == self._state:
209
+ if s == self.start_state:
210
+ row_chars.append("A(S)")
211
+ elif s == self.goal_state:
212
+ row_chars.append("A(G)")
213
+ else:
214
+ row_chars.append(" A ")
215
+ elif s == self.goal_state:
216
+ row_chars.append(" G ")
217
+ elif s == self.start_state:
218
+ row_chars.append(" S ")
219
+ elif s in self.obstacles:
220
+ row_chars.append(" X ")
221
+ else:
222
+ row_chars.append(f"{s:2d} ")
223
+ lines.append(" ".join(row_chars))
224
+ return "\n".join(lines)
225
+
226
+ def render(self) -> str | None:
227
+ """Gymnasium render entry point."""
228
+ if self.render_mode == "ansi":
229
+ return self.render_ascii()
230
+ if self.render_mode == "human":
231
+ print(self.render_ascii())
232
+ return None
233
+ return None
@@ -0,0 +1,17 @@
1
+ """Tabular reinforcement learning algorithms and history utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from rlshiftlab.tabular.algorithms import (
6
+ frozen_policy,
7
+ train_q_learning,
8
+ train_sarsa,
9
+ )
10
+ from rlshiftlab.tabular.history import TrainingHistory
11
+
12
+ __all__ = [
13
+ "TrainingHistory",
14
+ "frozen_policy",
15
+ "train_q_learning",
16
+ "train_sarsa",
17
+ ]