samsara-rl 0__py3-none-any.whl

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.
samsara_rl/__init__.py ADDED
File without changes
File without changes
@@ -0,0 +1,103 @@
1
+ from enum import Enum
2
+
3
+ import numpy as np
4
+
5
+
6
+ class GridWorldActions(Enum):
7
+ UP = 0
8
+ DOWN = 1
9
+ LEFT = 2
10
+ RIGHT = 3
11
+
12
+
13
+ class GridWorldMDP:
14
+ """
15
+ Sample 4x4 Grid world as described in David Silvers 3rd RL Lecture.
16
+ Consists of 4 actions, UP,DOWN,LEFT,RIGHT. Fully deterministic, transition probability is 1.0 for an action (i.e. UP will always
17
+ move up.). Default initial policy is uniform random.
18
+ @author ashish juneja
19
+ """
20
+
21
+ ACTION_COUNT = 4
22
+ STATE_COUNT = 16
23
+
24
+ def __init__(self) -> None:
25
+ self.terminal_states: list[tuple[int, int] | int] = [(0, 0), (3, 3), 0, 15]
26
+ self.state_action_transition_matrix = np.zeros((16, 4, 16))
27
+ self.reward_matrix = np.full((16, 4, 16), -1)
28
+
29
+ self.actions = {
30
+ GridWorldActions.UP.value: self.up,
31
+ GridWorldActions.DOWN.value: self.down,
32
+ GridWorldActions.LEFT.value: self.left,
33
+ GridWorldActions.RIGHT.value: self.right,
34
+ }
35
+ self.init_transition_probabilities()
36
+ self.init_reward_matrix()
37
+
38
+ def step(self, state: int, action: int) -> tuple[int, int]:
39
+ curr_state_transitions = self.state_action_transition_matrix[state][action]
40
+ next_state = np.random.choice(len(curr_state_transitions), p=curr_state_transitions)
41
+ reward = self.reward_matrix[state][action][next_state]
42
+ return reward, next_state
43
+
44
+ def init_reward_matrix(self) -> None:
45
+ self.reward_matrix[0, :, 0] = 0
46
+ self.reward_matrix[15, :, 15] = 0
47
+
48
+ def init_transition_probabilities(self) -> None:
49
+ """
50
+ Initialize a 16x4x16 numpy matrix that captures the GridWorld
51
+ Deterministic Dynamics. 16 possible starting states, 4 actions from each of those states
52
+ and 16 possible states the agent can end up in after invoking one of the 4 UP,DOWN,LEFT,RIGHT
53
+ actions. All actions are deterministic, ex: UP has 100% probability of upward movement and 0% into any
54
+ of the other 15 states. At boarder collisions agent ends up in current state before action
55
+ """
56
+ for curr_state_next_action in np.ndindex(self.state_action_transition_matrix.shape[:-1]):
57
+ curr_state, action = curr_state_next_action
58
+ curr_row, curr_col = self._unflatten(curr_state)
59
+ next_state = self.actions[action]((curr_row, curr_col))[0]
60
+ flattened_next_state = self._flatten(next_state[0], next_state[1])
61
+ self.state_action_transition_matrix[curr_state, action, flattened_next_state] = 1
62
+
63
+ def move(self, state: tuple[int, int], x_direction: int, y_direction: int) -> list[tuple[int, int]]:
64
+ next_step = (state[0] + x_direction, state[1] + y_direction)
65
+ if state in self.terminal_states or not self.in_bounds(next_step[0], next_step[1]):
66
+ return [state]
67
+ else:
68
+ return [next_step]
69
+
70
+ def up(self, state: tuple[int, int]) -> list[tuple[int, int]]:
71
+ return self.move(state, x_direction=-1, y_direction=0)
72
+
73
+ def down(self, state: tuple[int, int]) -> list[tuple[int, int]]:
74
+ return self.move(state, x_direction=1, y_direction=0)
75
+
76
+ def left(self, state: tuple[int, int]) -> list[tuple[int, int]]:
77
+ return self.move(state, x_direction=0, y_direction=-1)
78
+
79
+ def right(self, state: tuple[int, int]) -> list[tuple[int, int]]:
80
+ return self.move(state, x_direction=0, y_direction=1)
81
+
82
+ def in_bounds(self, x: int, y: int) -> bool:
83
+ return x in range(0, 4) and y in range(0, 4)
84
+
85
+ def _unflatten(self, state: int) -> tuple[int, int]:
86
+ """
87
+ Folds the tensor addressable 0-15 state into a 4x4 human readable coordinate
88
+ """
89
+ row = state // 4
90
+ col = state % 4
91
+ return row, col
92
+
93
+ def _flatten(self, row: int, col: int) -> int:
94
+ """
95
+ Folds the human readable row,col coordinate into a machine addressable 0..15
96
+ """
97
+ return 4 * row + col
98
+
99
+ def initial_state(self) -> int:
100
+ return self._flatten(2, 2)
101
+
102
+ def is_terminal_state(self, state: int) -> bool:
103
+ return state in self.terminal_states
File without changes
@@ -0,0 +1,41 @@
1
+ import math
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+
7
+
8
+ class Planning(ABC):
9
+ def __init__(self, mdp: Any, bellman_tolerance: float = 0.01) -> None:
10
+ self.mdp = mdp
11
+ self.bellman_tolerance = bellman_tolerance
12
+ self.policy: np.ndarray = np.zeros((self.mdp.STATE_COUNT, self.mdp.ACTION_COUNT))
13
+ self.values: np.ndarray = np.zeros(self.mdp.STATE_COUNT)
14
+
15
+ @abstractmethod
16
+ def find_optimal_policy(self) -> np.ndarray:
17
+ pass
18
+
19
+ def _bellman_error(self, value_history: list[np.ndarray]) -> float:
20
+ """Return the sum of absolute value changes between the last two iterations."""
21
+ if len(value_history) > 1:
22
+ return float(np.absolute(value_history[-1] - value_history[-2]).sum())
23
+ return math.inf
24
+
25
+ def improve_policy(self) -> None:
26
+ """
27
+ Update the policy greedily with respect to the current state values.
28
+
29
+ For each state, assigns probability 1.0 to the action with the highest
30
+ expected next-state value (ties are broken by np.argmax, i.e. first index).
31
+
32
+ Updates self.policy in place.
33
+ """
34
+ # Expected next-state value for each (s, a): shape (S, A)
35
+ expected_next_value = np.dot(self.mdp.state_action_transition_matrix, self.values)
36
+
37
+ best_actions = np.argmax(expected_next_value, axis=1)
38
+ state_indices = np.arange(self.mdp.STATE_COUNT)
39
+
40
+ self.policy = np.zeros((self.mdp.STATE_COUNT, self.mdp.ACTION_COUNT))
41
+ self.policy[state_indices, best_actions] = 1.0
@@ -0,0 +1,84 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+
5
+ from samsara_rl.planning.planning import Planning
6
+
7
+
8
+ class PolicyIteration(Planning):
9
+ """
10
+ Policy Iteration for finite, discrete MDPs.
11
+
12
+ Alternates between two steps until the policy stops changing:
13
+ 1. Policy Evaluation — compute V(s) for the current policy via iterative
14
+ application of the Bellman expectation equation.
15
+ 2. Policy Improvement — update the policy greedily with respect to V(s).
16
+
17
+ Follows the formulation in David Silver's RL Lecture 3 (Planning by DP).
18
+
19
+ Attributes:
20
+ mdp: The MDP to solve. Must expose STATE_COUNT, ACTION_COUNT,
21
+ state_action_transition_matrix, and reward_matrix.
22
+ bellman_tolerance: Convergence threshold for policy evaluation.
23
+ policy: Current policy as a (STATE_COUNT, ACTION_COUNT) probability
24
+ matrix. Initialised to the uniform random policy.
25
+ values: Current state-value estimate, shape (STATE_COUNT,).
26
+ """
27
+
28
+ def __init__(self, mdp: "Any", bellman_tolerance: float = 0.01) -> None:
29
+ super().__init__(mdp, bellman_tolerance)
30
+ self.policy = self._init_policy()
31
+
32
+ def find_optimal_policy(self, max_iter: int = 99) -> np.ndarray:
33
+ """
34
+ Run policy iteration until the policy converges or max_iter is reached.
35
+
36
+ Returns:
37
+ The converged (optimal) policy array of shape (STATE_COUNT, ACTION_COUNT).
38
+ """
39
+ previous_policy: np.ndarray | None = None
40
+ iteration = 0
41
+
42
+ while not self._is_converged(previous_policy) and iteration < max_iter:
43
+ self.evaluate_policy()
44
+ previous_policy = np.copy(self.policy)
45
+ self.improve_policy()
46
+ iteration += 1
47
+
48
+ return self.policy
49
+
50
+ def evaluate_policy(self) -> None:
51
+ """
52
+ Estimate state values for the current policy via iterative Bellman updates.
53
+
54
+ Applies the Bellman expectation equation repeatedly:
55
+ V(s) = Σ_a π(a|s) Σ_s' T(s,a,s') [R(s,a,s') + V(s')]
56
+ until the change in values falls below bellman_tolerance.
57
+
58
+ Updates self.values in place.
59
+ """
60
+ value_history: list[np.ndarray] = []
61
+
62
+ while self._bellman_error(value_history) > self.bellman_tolerance:
63
+ # Expected next-state value for each (s, a): shape (S, A)
64
+ expected_next_value = np.dot(self.mdp.state_action_transition_matrix, self.values)
65
+
66
+ # Expected immediate reward for each (s, a): shape (S, A)
67
+ expected_reward = (self.mdp.reward_matrix * self.mdp.state_action_transition_matrix).sum(axis=-1)
68
+
69
+ # Weight by policy probabilities and collapse over actions: shape (S,)
70
+ new_values = (self.policy * (expected_next_value + expected_reward)).sum(axis=-1)
71
+
72
+ value_history.append(new_values)
73
+ self.values = new_values
74
+
75
+ def _init_policy(self) -> np.ndarray:
76
+ """Return a uniform random policy of shape (STATE_COUNT, ACTION_COUNT)."""
77
+ return np.full(
78
+ (self.mdp.STATE_COUNT, self.mdp.ACTION_COUNT),
79
+ 1.0 / self.mdp.ACTION_COUNT,
80
+ )
81
+
82
+ def _is_converged(self, previous_policy: np.ndarray | None) -> bool:
83
+ """Return True if the policy is unchanged since the last improvement step."""
84
+ return previous_policy is not None and np.array_equal(previous_policy, self.policy)
@@ -0,0 +1,55 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+
5
+ from samsara_rl.planning.planning import Planning
6
+
7
+
8
+ class ValueIteration(Planning):
9
+ """
10
+ Value Iteration for finite, discrete MDPs.
11
+
12
+ Iteratively applies the Bellman optimality equation until state values
13
+ converge, then extracts a greedy policy from the converged values.
14
+
15
+ Follows the formulation in David Silver's RL Lecture 3 (Planning by DP).
16
+ """
17
+
18
+ def __init__(self, mdp: "Any", bellman_tolerance: float = 0.01) -> None:
19
+ super().__init__(mdp, bellman_tolerance)
20
+
21
+ def find_optimal_policy(self) -> np.ndarray:
22
+ """
23
+ Run value iteration to convergence, then extract a greedy policy.
24
+
25
+ Returns:
26
+ The optimal policy as a (STATE_COUNT, ACTION_COUNT) array.
27
+ """
28
+ self._evaluate_value_function()
29
+ self.improve_policy()
30
+ return self.policy
31
+
32
+ def _evaluate_value_function(self) -> None:
33
+ """
34
+ Estimate optimal state values via iterative Bellman optimality updates.
35
+
36
+ For each iteration, applies:
37
+ V(s) = max_a Σ_s' T(s,a,s') [R(s,a,s') + V(s')]
38
+ until the change in values falls below bellman_tolerance.
39
+
40
+ Updates self.values in place.
41
+ """
42
+ value_history: list[np.ndarray] = []
43
+
44
+ while self._bellman_error(value_history) > self.bellman_tolerance:
45
+ # Expected next-state value for each (s, a): shape (S, A)
46
+ expected_next_value = np.dot(self.mdp.state_action_transition_matrix, self.values)
47
+
48
+ # Expected immediate reward for each (s, a): shape (S, A)
49
+ expected_reward = (self.mdp.reward_matrix * self.mdp.state_action_transition_matrix).sum(axis=2)
50
+
51
+ # Bellman optimality: take max over actions, shape (S,)
52
+ new_values = np.max(expected_reward + expected_next_value, axis=1)
53
+
54
+ self.values = new_values
55
+ value_history.append(new_values.copy())
File without changes
@@ -0,0 +1,74 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+
5
+ from samsara_rl.prediction.policy_evaluation import PolicyEvaluation
6
+
7
+
8
+ class MonteCarloPrediction(PolicyEvaluation):
9
+ """Every-visit Monte Carlo prediction for estimating Q(s, a).
10
+
11
+ Generates episodes under a given policy and updates action-value
12
+ estimates using constant-alpha MC learning. Returns are computed
13
+ in a fully vectorized manner without explicit loops over time steps.
14
+
15
+ Args:
16
+ mdp: Environment with ``STATE_COUNT``, ``ACTION_COUNT``,
17
+ ``initial_state()``, ``is_terminal_state()``, and ``step()``
18
+ methods.
19
+ policy: A stochastic policy array of shape
20
+ ``(STATE_COUNT, ACTION_COUNT)`` used to sample actions.
21
+ alpha: Learning rate for incremental Q updates.
22
+ gamma: Discount factor.
23
+ """
24
+
25
+ def __init__(self, mdp: Any, policy: np.ndarray, alpha: float = 0.01, gamma: float = 1) -> None:
26
+ super().__init__(mdp, policy, alpha, gamma)
27
+
28
+ def post_visit(self, trajectory: np.ndarray) -> None:
29
+ return
30
+
31
+ def post_episode(self, trajectory: np.ndarray) -> None:
32
+ """Update Q-table from a single episode trajectory.
33
+
34
+ Uses advanced indexing to apply the constant-alpha MC update
35
+ Q(s,a) <- Q(s,a) + alpha * (G - Q(s,a)) for every visited
36
+ (state, action) pair in the trajectory.
37
+
38
+ Args:
39
+ trajectory: Array of shape ``(T, 3)`` where each row is
40
+ ``[state, action, reward]``.
41
+ """
42
+ discounted_trajectory = self._discounted_cum_trajectory(trajectory)
43
+ trajectory = np.column_stack((trajectory, discounted_trajectory))
44
+ s_a_pairs = trajectory[:, 0:2].astype(np.int16)
45
+ s = s_a_pairs[:, 0]
46
+ a = s_a_pairs[:, 1]
47
+ bellman_error = self.alpha * (discounted_trajectory - self.q_table[s, a])
48
+ np.add.at(self.q_table, (s, a), bellman_error)
49
+
50
+ def _discounted_cum_trajectory(self, trajectory: np.ndarray) -> np.ndarray:
51
+ """Compute discounted returns for every time step, vectorized.
52
+
53
+ Avoids the standard O(T) reverse loop by factoring out discount
54
+ weights from a cumulative sum:
55
+
56
+ 1. Divide each reward by its positional gamma power to normalize.
57
+ 2. Reverse and cumsum so earlier states accumulate future rewards.
58
+ 3. Multiply back by gamma powers to restore correct discounting.
59
+
60
+ Args:
61
+ trajectory: Array of shape ``(T, 3)`` where column 2 contains
62
+ rewards.
63
+
64
+ Returns:
65
+ Array of shape ``(T,)`` with the discounted return G_t for
66
+ each time step.
67
+ """
68
+ discount_ratio = self.gamma ** np.arange(0, len(trajectory[:, 2]))[::-1]
69
+ reversed_reward = trajectory[:, 2]
70
+ reversed_reward = reversed_reward / discount_ratio
71
+ reversed_reward_cum = reversed_reward[::-1].cumsum()
72
+ reversed_reward_cum = reversed_reward_cum * discount_ratio[::-1]
73
+ result: np.ndarray = reversed_reward_cum[::-1]
74
+ return result
@@ -0,0 +1,100 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ import numpy as np
5
+
6
+ from samsara_rl.utils.policy.policy_utils import sample
7
+
8
+
9
+ class PolicyEvaluation(ABC):
10
+ """Base class for model-free policy evaluation methods.
11
+
12
+ Provides the episode-generation loop (template method pattern) and
13
+ defines hooks that subclasses override to implement algorithm-specific
14
+ update logic.
15
+
16
+ Args:
17
+ mdp: Environment with ``STATE_COUNT``, ``ACTION_COUNT``,
18
+ ``initial_state()``, ``is_terminal_state()``, and ``step()``
19
+ methods.
20
+ policy: Stochastic policy array of shape ``(S, A)`` where each
21
+ row sums to 1.
22
+ alpha: Learning rate for incremental Q updates.
23
+ gamma: Discount factor applied to future rewards.
24
+ """
25
+
26
+ def __init__(self, mdp: Any, policy: np.ndarray, alpha: float = 0.01, gamma: float = 0.9) -> None:
27
+ self.q_table: np.ndarray = np.zeros((mdp.STATE_COUNT, mdp.ACTION_COUNT))
28
+ self.mdp = mdp
29
+ self.policy: np.ndarray = policy
30
+ self.gamma: float = gamma
31
+ self.alpha: float = alpha
32
+
33
+ @abstractmethod
34
+ def post_visit(self, trajectory: np.ndarray) -> None:
35
+ """Called after each step within an episode.
36
+
37
+ Subclasses use this to perform per-step updates (e.g. TD
38
+ updates with eligibility traces). No-op for methods that
39
+ only update at episode end (e.g. Monte Carlo).
40
+
41
+ Args:
42
+ trajectory: Array of shape ``(t, 3)`` containing all steps
43
+ recorded so far in the current episode, where each row
44
+ is ``[state, action, reward]``.
45
+ """
46
+ pass
47
+
48
+ @abstractmethod
49
+ def post_episode(self, trajectory: np.ndarray) -> None:
50
+ """Called after a complete episode has been generated.
51
+
52
+ Subclasses use this to perform end-of-episode updates
53
+ (e.g. Monte Carlo return-based Q updates) or to reset
54
+ per-episode state (e.g. eligibility traces).
55
+
56
+ Args:
57
+ trajectory: Array of shape ``(T, 3)`` containing the full
58
+ episode, where each row is ``[state, action, reward]``.
59
+ """
60
+ pass
61
+
62
+ def run_episode(self) -> np.ndarray:
63
+ """Generate a complete episode under the current policy.
64
+
65
+ Samples actions from the policy and steps through the MDP until
66
+ a terminal state is reached. Calls ``post_visit`` after each
67
+ step so subclasses can perform online updates.
68
+
69
+ Returns:
70
+ np.ndarray: Array of shape ``(T, 3)`` where each row is
71
+ ``[state, action, reward]``.
72
+ """
73
+ curr_state, curr_step = self.mdp.initial_state(), 0
74
+
75
+ trajectory = np.zeros((1000, 3))
76
+
77
+ while not self.mdp.is_terminal_state(curr_state):
78
+ sampled_action = sample(self.policy, curr_state)
79
+ reward, next_state = self.mdp.step(curr_state, sampled_action)
80
+ trajectory[curr_step][0] = curr_state
81
+ trajectory[curr_step][1] = sampled_action
82
+ trajectory[curr_step][2] = reward
83
+ curr_step += 1
84
+ curr_state = next_state
85
+ self.post_visit(trajectory[0:curr_step])
86
+
87
+ return trajectory[0:curr_step]
88
+
89
+ def evaluate(self, max_iter: int = 1000) -> None:
90
+ """Run prediction for a fixed number of episodes.
91
+
92
+ Generates episodes and calls ``post_episode`` after each one,
93
+ allowing subclasses to perform end-of-episode updates.
94
+
95
+ Args:
96
+ max_iter: Number of episodes to sample and learn from.
97
+ """
98
+ for _ in range(0, max_iter):
99
+ trajectory = self.run_episode()
100
+ self.post_episode(trajectory)
@@ -0,0 +1,36 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+
5
+ from samsara_rl.prediction.policy_evaluation import PolicyEvaluation
6
+
7
+
8
+ class TemporalDifference(PolicyEvaluation):
9
+ def __init__(
10
+ self, mdp: Any, policy: np.ndarray, alpha: float = 0.01, gamma: float = 0.9, _lambda: float = 0.4
11
+ ) -> None:
12
+ super().__init__(mdp, policy, alpha, gamma)
13
+ self.elibility = np.zeros((mdp.STATE_COUNT, self.mdp.ACTION_COUNT))
14
+ self._lambda = _lambda
15
+
16
+ def post_episode(self, trajectory: np.ndarray) -> None:
17
+ self.elibility = np.zeros((self.mdp.STATE_COUNT, self.mdp.ACTION_COUNT))
18
+
19
+ def post_visit(self, trajectory: np.ndarray) -> None:
20
+ if trajectory.shape[0] < 2:
21
+ return
22
+ self.elibility = self.elibility * self._lambda
23
+ visited_state = trajectory[-2][0].astype(np.int8)
24
+ el_action = trajectory[-2][1]
25
+ self.elibility[visited_state][el_action.astype(np.int8)] = 1
26
+
27
+ R_prime = trajectory[-2][2]
28
+ S_prime = trajectory[-1][0].astype(np.int8) # state we ended up in after A
29
+ Q_expectation = self.q_table[S_prime].dot(self.policy[S_prime]) # optimal known action from next step
30
+
31
+ S = trajectory[-2][0].astype(np.int8) # state before taking A
32
+ A = trajectory[-2][1].astype(np.int8) # action we took to end up in S'
33
+ Q = self.q_table[S][A] # Estimate of how good S,A is before taking
34
+
35
+ td_error = (R_prime + self.gamma * Q_expectation) - Q
36
+ self.q_table = self.q_table + self.alpha * self.elibility * td_error
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ from typing import Any
2
+
3
+ import numpy as np
4
+
5
+
6
+ def init_uniform_random(mdp: Any) -> np.ndarray:
7
+ """Return uniform random policy given an MDP
8
+ of all actions being uniform"""
9
+ return np.full(
10
+ (mdp.STATE_COUNT, mdp.ACTION_COUNT),
11
+ 1.0 / mdp.ACTION_COUNT,
12
+ )
13
+
14
+
15
+ def sample(policy: np.ndarray, s: int) -> int:
16
+ """Returns a sample action based on an input policy"""
17
+ action_distribution = policy[s]
18
+ selected_action = np.random.choice(len(action_distribution), p=action_distribution)
19
+ return selected_action
@@ -0,0 +1,218 @@
1
+ Metadata-Version: 2.4
2
+ Name: samsara-rl
3
+ Version: 0
4
+ Summary: Vectorized Reinforcement Learning Library
5
+ Project-URL: Homepage, https://swordofzeus.github.io/samsara-rl/
6
+ Project-URL: Repository, https://github.com/swordofzeus/samsara-rl
7
+ Project-URL: Documentation, https://swordofzeus.github.io/samsara-rl/
8
+ Author-email: Ashish Juneja <ashishjuneja99@gmail.com>
9
+ License-File: LICENSE
10
+ Keywords: python
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Requires-Python: <4.0,>=3.10
21
+ Requires-Dist: numpy>=2.2.6
22
+ Description-Content-Type: text/markdown
23
+
24
+ # Samsara RL
25
+
26
+ <p align="center">
27
+ <img src="img/samsara-rl.png" alt="Samsara RL" width="200">
28
+ </p>
29
+
30
+ [![CI](https://github.com/swordofzeus/samsara-rl/actions/workflows/main.yml/badge.svg)](https://github.com/swordofzeus/samsara-rl/actions/workflows/main.yml)
31
+
32
+ A vectorized NumPy implementation of foundational reinforcement learning algorithms, following David Silver's RL lecture series, Sutton and Barto book and other papers cited when referenced. Built for clarity, learning as well as speed.
33
+
34
+ Applications of RL include robotic manipulation, LLM fine-tuning, financial portfolio management, and control systems.
35
+
36
+ ---
37
+
38
+ ## Table of Contents
39
+
40
+ 1. [Installation](#installation)
41
+ 2. [Quick Start](#quick-start)
42
+ 3. [Planning](#planning)
43
+ - [MDP Structure](#mdp-structure)
44
+ - [Policy Iteration](#policy-iteration)
45
+ - [Value Iteration](#value-iteration)
46
+ 4. [Model-Free Prediction](#model-free-prediction)
47
+ - [Monte Carlo](#monte-carlo)
48
+ - [TD Learning](#td-learning)
49
+ 5. [Model-Free Control](#model-free-control)
50
+ - [SARSA](#sarsa)
51
+ - [Q-Learning](#q-learning)
52
+ 6. [Function Approximation](#function-approximation)
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ ```bash
59
+ pip install samsara-rl
60
+ ```
61
+
62
+ ---
63
+
64
+ ## Quick Start
65
+
66
+ ```python
67
+ from samsara_rl.mdp.grid_world.grid_world_mdp import GridWorldMDP
68
+ from samsara_rl.planning.policy_iteration import PolicyIteration
69
+
70
+ mdp = GridWorldMDP()
71
+ pi = PolicyIteration(mdp)
72
+ policy = pi.find_optimal_policy()
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Planning
78
+
79
+ Planning algorithms assume full knowledge of environment dynamics (transition probabilities and reward function). While not "true RL" — agents never have access to dynamics in practice — planning provides the theoretical foundation all RL algorithms build on.
80
+
81
+ ### MDP Structure
82
+
83
+ MDPs are represented as NumPy arrays. The included `GridWorldMDP` implements the 4x4 grid world from David Silver's Lecture 3.
84
+
85
+ | Attribute | Shape | Description |
86
+ |----------------------------------|--------------|--------------------------------------------------|
87
+ | `state_action_transition_matrix` | `(S, A, S')` | T(s, a, s') — transition probabilities |
88
+ | `reward_matrix` | `(S, A, S')` | R(s, a, s') — reward for each transition |
89
+
90
+ ### Policy Iteration
91
+
92
+ Alternates between evaluating the current policy using the Bellman expectation equation and improving it greedily until the policy stops changing.
93
+
94
+ **`PolicyIteration(mdp, bellman_tolerance)`**
95
+
96
+ | Argument | Type | Default | Description |
97
+ |---------------------|---------|---------|---------------------------------------------|
98
+ | `mdp` | MDP | | MDP instance to solve |
99
+ | `bellman_tolerance` | `float` | `0.01` | Convergence threshold for policy evaluation |
100
+
101
+ **`find_optimal_policy(max_iter)`**
102
+
103
+ | Argument | Type | Default | Description |
104
+ |------------|-------|---------|------------------------------------------|
105
+ | `max_iter` | `int` | `99` | Maximum number of policy iteration steps |
106
+
107
+ ### Value Iteration
108
+
109
+ Applies the Bellman optimality equation directly each iteration. Equivalent to policy iteration with k=1 evaluation steps per improvement. Policy is extracted once at convergence.
110
+
111
+ **`ValueIteration(mdp, bellman_tolerance)`**
112
+
113
+ | Argument | Type | Default | Description |
114
+ |---------------------|---------|---------|-------------------------------------------|
115
+ | `mdp` | MDP | | MDP instance to solve |
116
+ | `bellman_tolerance` | `float` | `0.01` | Convergence threshold for value iteration |
117
+
118
+ **Examples**
119
+
120
+ ```python
121
+ from samsara_rl.mdp.grid_world.grid_world_mdp import GridWorldMDP
122
+ from samsara_rl.planning.policy_iteration import PolicyIteration
123
+ from samsara_rl.planning.value_iteration import ValueIteration
124
+
125
+ mdp = GridWorldMDP()
126
+
127
+ policy = PolicyIteration(mdp, bellman_tolerance=0.001).find_optimal_policy(max_iter=50)
128
+ policy = ValueIteration(mdp, bellman_tolerance=0.001).find_optimal_policy()
129
+ ```
130
+
131
+ ---
132
+
133
+ ## Model-Free Prediction
134
+
135
+ Model-free methods learn value functions directly from experience (sampled episodes) without access to the MDP's transition or reward dynamics.
136
+
137
+ ### Monte Carlo
138
+
139
+ Every-visit Monte Carlo prediction estimates Q(s, a) from sampled returns. After each episode, the return G_t (discounted cumulative reward from time step t onward) is computed for every visited state-action pair, and the Q-table is updated using constant-alpha learning:
140
+
141
+ Q(s, a) ← Q(s, a) + α (G_t − Q(s, a))
142
+
143
+ If the same (s, a) pair appears multiple times in an episode, each occurrence triggers an update. Returns are computed in a fully vectorized pass using a cumulative-sum trick that avoids the standard reverse loop over time steps.
144
+
145
+ **`MonteCarloPrediction(mdp, policy, alpha, gamma)`**
146
+
147
+ | Argument | Type | Default | Description |
148
+ |----------|---------|---------|------------------------------------------|
149
+ | `mdp` | MDP | | MDP instance to sample episodes from |
150
+ | `policy` | `array` | | Stochastic policy of shape `(S, A)` |
151
+ | `alpha` | `float` | `0.01` | Learning rate for incremental Q updates |
152
+ | `gamma` | `float` | `1` | Discount factor |
153
+
154
+ **`evaluate(max_iter)`**
155
+
156
+ | Argument | Type | Default | Description |
157
+ |------------|-------|---------|--------------------------------------|
158
+ | `max_iter` | `int` | `10000` | Number of episodes to sample from |
159
+
160
+ ### TD(λ)
161
+
162
+ TD(λ) learns Q(s, a) online using one-step bootstrapping with eligibility traces. After each step, the TD error is computed against the expected Q-value of the next state under the current policy, and all previously visited state-action pairs are updated proportionally to their eligibility:
163
+
164
+ δ = R + γ E_π[Q(S', ·)] − Q(S, A)
165
+
166
+ Q(s, a) ← Q(s, a) + α δ e(s, a)
167
+
168
+ Eligibility traces use the replacing variant — on each visit to (s, a), the trace is set to 1 rather than incremented. All traces decay by γλ at each time step. Traces are reset to zero between episodes.
169
+
170
+ **`TemporalDifference(mdp, policy, alpha, gamma, _lambda)`**
171
+
172
+ | Argument | Type | Default | Description |
173
+ |-----------|---------|---------|------------------------------------------------|
174
+ | `mdp` | MDP | | MDP instance to sample episodes from |
175
+ | `policy` | `array` | | Stochastic policy of shape `(S, A)` |
176
+ | `alpha` | `float` | `0.01` | Learning rate for incremental Q updates |
177
+ | `gamma` | `float` | `0.9` | Discount factor |
178
+ | `_lambda` | `float` | `0.4` | Trace decay parameter (0 = TD(0), 1 = TD(1)) |
179
+
180
+ **`evaluate(max_iter)`**
181
+
182
+ | Argument | Type | Default | Description |
183
+ |------------|-------|---------|--------------------------------------|
184
+ | `max_iter` | `int` | `1000` | Number of episodes to sample from |
185
+
186
+ **Examples**
187
+
188
+ ```python
189
+ from samsara_rl.mdp.grid_world.grid_world_mdp import GridWorldMDP
190
+ from samsara_rl.prediction.monte_carlo import MonteCarloPrediction
191
+ from samsara_rl.prediction.td import TemporalDifference
192
+ from samsara_rl.utils.policy.policy_utils import init_uniform_random
193
+
194
+ mdp = GridWorldMDP()
195
+ policy = init_uniform_random(mdp)
196
+
197
+ mc = MonteCarloPrediction(mdp, policy, alpha=0.01, gamma=0.9)
198
+ mc.evaluate(max_iter=10000)
199
+
200
+ td = TemporalDifference(mdp, policy, alpha=0.01, gamma=0.9, _lambda=0.4)
201
+ td.evaluate(max_iter=10000)
202
+
203
+ # V(s) for the random policy (expected value over actions)
204
+ v_mc = mc.q_table.mean(axis=1).reshape(4, 4)
205
+ v_td = td.q_table.mean(axis=1).reshape(4, 4)
206
+ ```
207
+
208
+ ---
209
+
210
+ ## Model-Free Control
211
+
212
+ TODO
213
+
214
+ ---
215
+
216
+ ## Function Approximation
217
+
218
+ TODO
@@ -0,0 +1,18 @@
1
+ samsara_rl/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ samsara_rl/mdp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ samsara_rl/mdp/grid_world/grid_world_mdp.py,sha256=c9UzPNZaNY451gjWSTios39WZDL7rKXQ1CPxAFXhRkk,4106
4
+ samsara_rl/planning/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ samsara_rl/planning/planning.py,sha256=GZ3TDxz0n_hdXtHsJ0-zMM-2ZGRAiQHRaxm57IDWy0w,1557
6
+ samsara_rl/planning/policy_iteration.py,sha256=7vAZq2v9-WHpSTV4mEkfwVdKyY_GnGhR6CaUCdCqDfU,3412
7
+ samsara_rl/planning/value_iteration.py,sha256=qV-L_40DGu0y65YsXbO9QrLB4vSl00RLnjlLvV0mFYI,1938
8
+ samsara_rl/prediction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ samsara_rl/prediction/monte_carlo.py,sha256=HUAct1tKEdTyTVjiedAB8ZqWtk7twnJ2hDegksxpvqg,3020
10
+ samsara_rl/prediction/policy_evaluation.py,sha256=y9MMHfqH_bG4j5dIBWp66ldzInc81H85IoI7rHhAJeo,3665
11
+ samsara_rl/prediction/td.py,sha256=HbpIgoJeVabxcAAth1r7WRm-fRYujKxwmQ8b-6ThNHI,1533
12
+ samsara_rl/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ samsara_rl/utils/policy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ samsara_rl/utils/policy/policy_utils.py,sha256=9uFu_1tpn08Nnue4aZ7zXzTusjqw_w7NKrTEdz6lu1A,541
15
+ samsara_rl-0.dist-info/METADATA,sha256=goJw0yMZxxq7CPOVDM5sDEE3gF05z0ye8tyzXb5FXSc,8822
16
+ samsara_rl-0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
17
+ samsara_rl-0.dist-info/licenses/LICENSE,sha256=WsVkxEenhuQ7iKmUhnb9fyyCESIuJTMFwAibGMxyXL0,1070
18
+ samsara_rl-0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ashish Juneja
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.