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 +0 -0
- samsara_rl/mdp/__init__.py +0 -0
- samsara_rl/mdp/grid_world/grid_world_mdp.py +103 -0
- samsara_rl/planning/__init__.py +0 -0
- samsara_rl/planning/planning.py +41 -0
- samsara_rl/planning/policy_iteration.py +84 -0
- samsara_rl/planning/value_iteration.py +55 -0
- samsara_rl/prediction/__init__.py +0 -0
- samsara_rl/prediction/monte_carlo.py +74 -0
- samsara_rl/prediction/policy_evaluation.py +100 -0
- samsara_rl/prediction/td.py +36 -0
- samsara_rl/utils/__init__.py +0 -0
- samsara_rl/utils/policy/__init__.py +0 -0
- samsara_rl/utils/policy/policy_utils.py +19 -0
- samsara_rl-0.dist-info/METADATA +218 -0
- samsara_rl-0.dist-info/RECORD +18 -0
- samsara_rl-0.dist-info/WHEEL +4 -0
- samsara_rl-0.dist-info/licenses/LICENSE +21 -0
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
|
+
[](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,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.
|