simonbb 0.0.6__tar.gz → 0.0.7__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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: simonbb
3
- Version: 0.0.6
3
+ Version: 0.0.7
4
4
  Summary: A small package to facilitate teaching AI courses at business schools
5
5
  Project-URL: Homepage, https://simonbb.com
6
6
  Author-email: Huaxia Rui <huaxia.rui@simon.rochester.edu>
@@ -18,6 +18,7 @@ Requires-Dist: langchain~=1.2.13
18
18
  Requires-Dist: langgraph~=1.1.3
19
19
  Requires-Dist: matplotlib~=3.10.8
20
20
  Requires-Dist: numpy~=2.4.0
21
+ Requires-Dist: opencv-python~=5.0.0.93
21
22
  Requires-Dist: pytorch-ignite~=0.5.4
22
23
  Requires-Dist: pyyaml~=6.0.3
23
24
  Requires-Dist: ray~=2.56.0
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simonbb"
7
- version = "0.0.6"
7
+ version = "0.0.7"
8
8
  authors = [
9
9
  { name="Huaxia Rui", email="huaxia.rui@simon.rochester.edu" },
10
10
  ]
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "pytorch-ignite~=0.5.4",
35
35
  "stable_baselines3~=2.9.0",
36
36
  "ray~=2.56.0",
37
+ "opencv-python~=5.0.0.93",
37
38
  "pyyaml~=6.0.3"
38
39
  ]
39
40
  [project.urls]
@@ -25,9 +25,9 @@ class Experience:
25
25
 
26
26
  class ExperienceSource:
27
27
  """
28
- Simple n-step experience source using single or multiple environments
28
+ n-step experience source using single or multiple environments
29
29
 
30
- Every experience contains n list of Experience entries
30
+ Every experience contains a list of n Experience entries
31
31
  """
32
32
  Item = tt.Tuple[Experience, ...]
33
33
 
@@ -39,7 +39,7 @@ class ExperienceSource:
39
39
  :param env: environment or list of environments to be used
40
40
  :param agent: callable to convert batch of states into actions to take
41
41
  :param steps_count: count of steps to track for every experience chain
42
- :param steps_delta: how many steps to do between experience items
42
+ :param steps_delta: how many steps to do between subjectory emissions
43
43
  :param env_seed: seed to be used in Env.reset() call
44
44
  """
45
45
  assert steps_count >= 1
@@ -54,7 +54,7 @@ class ExperienceSource:
54
54
  self.agent = agent
55
55
  self.steps_count = steps_count
56
56
  self.steps_delta = steps_delta
57
- self.total_rewards = []
57
+ self.total_rewards = [] # simple sum of immediate rewards, without discounting
58
58
  self.total_steps = []
59
59
  self.agent_states = [agent.initial_state() for _ in self.pool]
60
60
  self.env_seed = env_seed
@@ -67,7 +67,7 @@ class ExperienceSource:
67
67
  else:
68
68
  obs, _ = env.reset()
69
69
  states.append(obs)
70
- histories.append(deque(maxlen=self.steps_count))
70
+ histories.append( deque(maxlen=self.steps_count) ) # Note the max length of the double-ended queue
71
71
  cur_rewards.append(0.0)
72
72
  cur_steps.append(0)
73
73
 
@@ -81,17 +81,16 @@ class ExperienceSource:
81
81
  next_state, r, is_done, is_tr, _ = env.step(action)
82
82
  cur_rewards[idx] += r
83
83
  cur_steps[idx] += 1
84
- history.append(Experience(state=state, action=action, reward=r, done_trunc=is_done or is_tr))
84
+ history.append(Experience(state=state, action=action, reward=r, done_trunc=is_done or is_tr)) # push out the oldest step once deque is full (i.e., steps_count reached)
85
85
  if len(history) == self.steps_count and iter_idx % self.steps_delta == 0:
86
86
  yield tuple(history)
87
87
  states[idx] = next_state
88
88
  if is_done or is_tr:
89
- # generate tail of history
90
89
  if 0 < len(history) < self.steps_count:
91
90
  yield tuple(history)
92
91
  while len(history) > 1:
93
92
  history.popleft()
94
- yield tuple(history)
93
+ yield tuple(history) # a sub-trajectory of the history
95
94
  self.total_rewards.append(cur_rewards[idx])
96
95
  self.total_steps.append(cur_steps[idx])
97
96
  cur_rewards[idx] = 0.0
@@ -104,13 +103,15 @@ class ExperienceSource:
104
103
  history.clear()
105
104
  iter_idx += 1
106
105
 
106
+
107
+ # pop_total_rewards() returns a list of total rewards (without discounting), one per env, after an episode ends
107
108
  def pop_total_rewards(self) -> tt.List[float]:
108
109
  r = self.total_rewards
109
110
  if r:
110
111
  self.total_rewards = []
111
112
  self.total_steps = []
112
113
  return r
113
-
114
+ # pop_total_rewards() returns a list of total rewards (without discounting) and number of steps, one per env, after an episode ends
114
115
  def pop_rewards_steps(self) -> tt.List[tt.Tuple[float, int]]:
115
116
  res = list(zip(self.total_rewards, self.total_steps))
116
117
  if res:
@@ -148,12 +149,54 @@ class ExperienceSourceFirstLast(ExperienceSource):
148
149
  else:
149
150
  last_state = exp[-1].state
150
151
  elems = exp[:-1]
151
- total_reward = 0.0
152
+ discounted_total_reward = 0.0
152
153
  for e in reversed(elems):
153
- total_reward *= self.gamma
154
- total_reward += e.reward
155
- yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action,
156
- reward=total_reward, last_state=last_state)
154
+ discounted_total_reward *= self.gamma
155
+ discounted_total_reward += e.reward
156
+ yield ExperienceFirstLast(state=exp[0].state, action=exp[0].action, reward=discounted_total_reward, last_state=last_state)
157
+
158
+
159
+
160
+ class ExperienceReplayBuffer:
161
+ def __init__(self, experience_source: tt.Optional[ExperienceSource], buffer_size: int):
162
+ self.experience_source_iter = None if experience_source is None else iter(experience_source)
163
+ self.buffer: tt.List[ExperienceSource.Item] = []
164
+ self.capacity = buffer_size
165
+ self.pos = 0
166
+
167
+ def __len__(self):
168
+ return len(self.buffer)
169
+
170
+ def __iter__(self) -> tt.Iterator[ExperienceSource.Item]:
171
+ return iter(self.buffer)
172
+
173
+ def sample(self, batch_size: int) -> tt.List[ExperienceSource.Item]:
174
+ """
175
+ Get one random batch from experience replay
176
+ :param batch_size: size of the batch to sample
177
+ :return: list of experience entries
178
+ """
179
+ if len(self.buffer) <= batch_size:
180
+ return self.buffer
181
+ # Warning: replace=False makes random.choice O(n)
182
+ keys = np.random.choice(len(self.buffer), batch_size, replace=True)
183
+ return [self.buffer[key] for key in keys]
184
+
185
+ def _add(self, sample: ExperienceSource.Item):
186
+ if len(self.buffer) < self.capacity:
187
+ self.buffer.append(sample)
188
+ else:
189
+ self.buffer[self.pos] = sample
190
+ self.pos = (self.pos + 1) % self.capacity
191
+
192
+ def populate(self, samples: int):
193
+ """
194
+ Populates samples into the buffer
195
+ :param samples: how many samples to populate
196
+ """
197
+ for _ in range(samples):
198
+ entry = next(self.experience_source_iter)
199
+ self._add(entry)
157
200
 
158
201
 
159
202
  def vector_rewards(rewards: tt.Deque[np.ndarray], dones: tt.Deque[np.ndarray], gamma: float) -> np.ndarray:
@@ -406,47 +449,6 @@ class ExperienceSourceBuffer:
406
449
  yield self.buffer[episode][ofs:ofs+self.steps_count]
407
450
 
408
451
 
409
- class ExperienceReplayBuffer:
410
- def __init__(self, experience_source: tt.Optional[ExperienceSource], buffer_size: int):
411
- self.experience_source_iter = None if experience_source is None else iter(experience_source)
412
- self.buffer: tt.List[ExperienceSource.Item] = []
413
- self.capacity = buffer_size
414
- self.pos = 0
415
-
416
- def __len__(self):
417
- return len(self.buffer)
418
-
419
- def __iter__(self) -> tt.Iterator[ExperienceSource.Item]:
420
- return iter(self.buffer)
421
-
422
- def sample(self, batch_size: int) -> tt.List[ExperienceSource.Item]:
423
- """
424
- Get one random batch from experience replay
425
- :param batch_size: size of the batch to sample
426
- :return: list of experience entries
427
- """
428
- if len(self.buffer) <= batch_size:
429
- return self.buffer
430
- # Warning: replace=False makes random.choice O(n)
431
- keys = np.random.choice(len(self.buffer), batch_size, replace=True)
432
- return [self.buffer[key] for key in keys]
433
-
434
- def _add(self, sample: ExperienceSource.Item):
435
- if len(self.buffer) < self.capacity:
436
- self.buffer.append(sample)
437
- else:
438
- self.buffer[self.pos] = sample
439
- self.pos = (self.pos + 1) % self.capacity
440
-
441
- def populate(self, samples: int):
442
- """
443
- Populates samples into the buffer
444
- :param samples: how many samples to populate
445
- """
446
- for _ in range(samples):
447
- entry = next(self.experience_source_iter)
448
- self._add(entry)
449
-
450
452
 
451
453
  class PrioReplayBufferNaive:
452
454
  def __init__(self, exp_source, buf_size, prob_alpha=0.6):
@@ -13,39 +13,38 @@ class EpisodeEvents(EventEnum):
13
13
 
14
14
 
15
15
  class EndOfEpisodeHandler:
16
- def __init__(self, exp_source: ExperienceSource, alpha: float = 0.98,
17
- bound_avg_reward: Optional[float] = None,
18
- subsample_end_of_episode: Optional[int] = None):
16
+ def __init__(self, exp_source: ExperienceSource, alpha: float = 0.98, bound_avg_reward: Optional[float] = None, period: Optional[int] = None):
19
17
  """
20
- Construct end-of-episode event handler
18
+ This end-of-episode event handler, triggered by Events.ITERATION_COMPLETED, fires an EPISODE_COMPLETED event for every episode, or every {period} number of episodes
21
19
  :param exp_source: experience source to use
22
20
  :param alpha: smoothing alpha param
23
21
  :param bound_avg_reward: optional boundary for average reward
24
- :param subsample_end_of_episode: if given, end of episode event will be subsampled by this amount
22
+ :param period: if given, end of episode event fire once every {period} episodes
25
23
  """
26
24
  self._exp_source = exp_source
27
25
  self._alpha = alpha
28
26
  self._bound_avg_reward = bound_avg_reward
29
27
  self._best_avg_reward = None
30
- self._subsample_end_of_episode = subsample_end_of_episode
28
+ self._period = period
31
29
 
32
30
  def attach(self, engine: Engine):
33
- engine.add_event_handler(Events.ITERATION_COMPLETED, self)
34
- engine.register_events(*EpisodeEvents)
31
+ engine.add_event_handler( Events.ITERATION_COMPLETED, self ) # invoke EndOfEpisodeHandler() instance whenever an iteration (consisting of multiple episodes) is completed
32
+ engine.register_events( *EpisodeEvents )
35
33
  State.event_to_attr[EpisodeEvents.EPISODE_COMPLETED] = "episode"
36
34
  State.event_to_attr[EpisodeEvents.BOUND_REWARD_REACHED] = "episode"
37
35
  State.event_to_attr[EpisodeEvents.BEST_REWARD_REACHED] = "episode"
38
36
 
39
37
  def __call__(self, engine: Engine):
38
+ # pop_rewards_steps() returns a list of (total rewards, steps) of newly completed episodes since the last call of pop_rewards_steps().
40
39
  for reward, steps in self._exp_source.pop_rewards_steps():
41
40
  engine.state.episode = getattr(engine.state, "episode", 0) + 1
42
- engine.state.episode_reward = reward
41
+ engine.state.episode_reward = reward # sum of immediate rewards of all steps during the episode, without discounting
43
42
  engine.state.episode_steps = steps
44
43
  engine.state.metrics['reward'] = reward
45
44
  engine.state.metrics['steps'] = steps
46
45
  self._update_smoothed_metrics(engine, reward, steps)
47
- if self._subsample_end_of_episode is None or engine.state.episode % self._subsample_end_of_episode == 0:
48
- engine.fire_event(EpisodeEvents.EPISODE_COMPLETED)
46
+ if self._period is None or engine.state.episode % self._period == 0:
47
+ engine.fire_event( EpisodeEvents.EPISODE_COMPLETED )
49
48
  if self._bound_avg_reward is not None and engine.state.metrics['avg_reward'] >= self._bound_avg_reward:
50
49
  engine.fire_event(EpisodeEvents.BOUND_REWARD_REACHED)
51
50
  if self._best_avg_reward is None:
@@ -110,13 +109,10 @@ class PeriodEvents(EventEnum):
110
109
  ITERS_10000_COMPLETED = "iterations_10000_completed"
111
110
  ITERS_100000_COMPLETED = "iterations_100000_completed"
112
111
 
113
-
114
112
  class PeriodicEvents:
115
113
  """
116
- The same as CustomPeriodicEvent from ignite.contrib, but use true amount of iterations,
117
- which is good for TensorBoard
114
+ The same as CustomPeriodicEvent from ignite.contrib, but use true amount of iterations, which is good for TensorBoard
118
115
  """
119
-
120
116
  INTERVAL_TO_EVENT = {
121
117
  10: PeriodEvents.ITERS_10_COMPLETED,
122
118
  100: PeriodEvents.ITERS_100_COMPLETED,
File without changes
File without changes
File without changes
File without changes