tinybaselines 0.1.0a1__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.
@@ -0,0 +1,5 @@
1
+ from .ppo import PPO
2
+ from .policies import GaussianActorCritic
3
+ from .rollout import RolloutBatch, RolloutState, rollout
4
+ from .training import TrainingStep, learn
5
+ from .evaluation import evaluate
@@ -0,0 +1,6 @@
1
+ from .rollout import rollout
2
+
3
+
4
+ def evaluate(env, policy, num_episodes, deterministic=False):
5
+ """Return total rewards from num_episodes fresh episodes."""
6
+ return rollout(env, policy, num_episodes=num_episodes, deterministic=deterministic)
@@ -0,0 +1,75 @@
1
+ import json
2
+
3
+ import numpy as np
4
+ from tinygrad import Tensor, TinyJit, nn
5
+
6
+
7
+ class MLP:
8
+ def __init__(self, input_dim, output_dim, hidden_dim, layer_count):
9
+ self.layers = [nn.Linear(input_dim, hidden_dim)]
10
+ self.layers.extend(nn.Linear(hidden_dim, hidden_dim) for _ in range(layer_count - 1))
11
+ self.layers.append(nn.Linear(hidden_dim, output_dim))
12
+
13
+ def __call__(self, x):
14
+ for layer in self.layers[:-1]:
15
+ x = layer(x).relu()
16
+ return self.layers[-1](x)
17
+
18
+
19
+ class GaussianActorCritic:
20
+ def __init__(self, state_dim, action_dim, hidden_dim, actor_layers, critic_layers, observation_scale=None):
21
+ self.config = dict(state_dim=state_dim, action_dim=action_dim, hidden_dim=hidden_dim,
22
+ actor_layers=actor_layers, critic_layers=critic_layers)
23
+ self.actor = MLP(state_dim, action_dim, hidden_dim, actor_layers)
24
+ self.critic = MLP(state_dim, 1, hidden_dim, critic_layers)
25
+ self.actor.layers[-1].weight.assign(self.actor.layers[-1].weight * 0.01).realize()
26
+ self.actor.layers[-1].bias.assign(Tensor.zeros_like(self.actor.layers[-1].bias)).realize()
27
+ self.log_std = Tensor.full((action_dim,), -0.7).realize()
28
+ scale = np.ones(state_dim, np.float32) if observation_scale is None else np.asarray(observation_scale, np.float32)
29
+ self.observation_scale = Tensor(scale.copy()).is_param_(False).realize()
30
+ self.jit_shape = None
31
+ self.actor_jit = TinyJit(lambda state: self.actor(state).realize())
32
+ self.refresh_inference()
33
+
34
+ def __call__(self, state):
35
+ action_mean = self.actor(state)
36
+ log_std = self.log_std.clip(-5, 2)
37
+ value = self.critic(state)
38
+ return action_mean, log_std, value
39
+
40
+ def preprocess(self, observations):
41
+ return Tensor(observations.astype(np.float32), device=self.observation_scale.device) * self.observation_scale
42
+
43
+ def act(self, state, deterministic=False):
44
+ state_tensor = self.preprocess(state)
45
+ if self.jit_shape != state_tensor.shape:
46
+ self.actor_jit.reset()
47
+ self.jit_shape = state_tensor.shape
48
+ mean = self.actor_jit(state_tensor).numpy()
49
+ if deterministic:
50
+ action = np.clip(mean, -1, 1)
51
+ return action, mean
52
+ sample = np.random.normal(mean, self.action_std).astype(np.float32)
53
+ action = np.clip(sample, -1, 1)
54
+ return action, sample
55
+
56
+ def state_dict(self):
57
+ return nn.state.get_state_dict(dict(actor=self.actor, critic=self.critic, log_std=self.log_std,
58
+ observation_scale=self.observation_scale))
59
+
60
+ def save(self, path):
61
+ nn.state.safe_save(self.state_dict(), path, metadata={'config': json.dumps(self.config)})
62
+
63
+ @classmethod
64
+ def load(cls, path):
65
+ _, _, header = nn.state.safe_load_metadata(path)
66
+ model = cls(**json.loads(header['__metadata__']['config']))
67
+ nn.state.load_state_dict(model.state_dict(), nn.state.safe_load(path), verbose=False)
68
+ model.refresh_inference()
69
+ return model
70
+
71
+ def refresh_inference(self):
72
+ self.actor_jit.reset()
73
+ self.action_std = np.exp(self.log_std.clip(-5, 2).numpy())
74
+ if not np.isfinite(self.action_std).all():
75
+ raise FloatingPointError('non-finite policy standard deviation')
tinybaselines/ppo.py ADDED
@@ -0,0 +1,155 @@
1
+ import json
2
+ from pathlib import Path
3
+ from tempfile import TemporaryDirectory
4
+
5
+ import numpy as np
6
+ from tinygrad import Context, Tensor, TinyJit, nn
7
+
8
+ from .policies import GaussianActorCritic
9
+
10
+ LOG_2PI = float(np.log(2.0 * np.pi))
11
+ NORMAL_ENTROPY_CONSTANT = float(0.5 * np.log(2.0 * np.pi * np.e))
12
+
13
+
14
+ def normal_log_prob(action_mean, log_std, actions):
15
+ normalized = (actions - action_mean) / log_std.exp()
16
+ return (-0.5 * (normalized.square() + 2.0 * log_std + LOG_2PI)).sum(axis=-1)
17
+
18
+
19
+ def clip_grad_norm(parameters, max_norm):
20
+ gradients = [parameter.grad for parameter in parameters if parameter.grad is not None]
21
+ if not gradients:
22
+ return
23
+ total_norm = sum((gradient.square().sum() for gradient in gradients)).sqrt()
24
+ scale = (max_norm / (total_norm + 1e-6)).clip(max_=1.0)
25
+ for parameter in parameters:
26
+ if parameter.grad is not None:
27
+ parameter.grad = parameter.grad * scale
28
+
29
+
30
+ class PPO:
31
+ def __init__(self, actor_critic, pi_lr, vf_lr, gamma, lamda, K_epochs, eps_clip, batch_size, vf_coef, entropy_coef):
32
+ self.actor_critic = actor_critic
33
+ self.device = actor_critic.log_std.device
34
+ self.actor_parameters = nn.state.get_parameters(actor_critic.actor) + [actor_critic.log_std]
35
+ self.critic_parameters = nn.state.get_parameters(actor_critic.critic)
36
+ self.pi_optimizer = nn.optim.Adam(self.actor_parameters, lr=pi_lr)
37
+ self.vf_optimizer = nn.optim.Adam(self.critic_parameters, lr=vf_lr)
38
+ self.optimizer = nn.optim.OptimizerGroup(self.pi_optimizer, self.vf_optimizer)
39
+ self.gamma, self.lamda, self.K_epochs = gamma, lamda, K_epochs
40
+ self.eps_clip, self.batch_size = eps_clip, batch_size
41
+ self.vf_coef, self.entropy_coef = vf_coef, entropy_coef
42
+ self._train_steps = {}
43
+
44
+ @property
45
+ def config(self):
46
+ return dict(pi_lr=self.pi_optimizer.lr.item(), vf_lr=self.vf_optimizer.lr.item(),
47
+ gamma=self.gamma, lamda=self.lamda, K_epochs=self.K_epochs, eps_clip=self.eps_clip,
48
+ batch_size=self.batch_size, vf_coef=self.vf_coef, entropy_coef=self.entropy_coef)
49
+
50
+ def state_dict(self):
51
+ state = dict(policy=self.actor_critic.state_dict())
52
+ for name in ('pi_optimizer', 'vf_optimizer'):
53
+ optimizer = getattr(self, name)
54
+ state[name] = {key: getattr(optimizer, key) for key in ('lr', 'b1_t', 'b2_t', 'm', 'v')}
55
+ return nn.state.get_state_dict(state)
56
+
57
+ def save(self, path):
58
+ path = Path(path)
59
+ metadata = dict(format='tinybaselines.ppo.v1', policy_config=json.dumps(self.actor_critic.config),
60
+ ppo_config=json.dumps(self.config))
61
+ with TemporaryDirectory(dir=path.parent) as directory:
62
+ temporary = Path(directory) / path.name
63
+ nn.state.safe_save(self.state_dict(), temporary, metadata=metadata)
64
+ temporary.replace(path)
65
+
66
+ @classmethod
67
+ def load(cls, path):
68
+ _, _, header = nn.state.safe_load_metadata(path)
69
+ metadata = header.get('__metadata__', {})
70
+ if metadata.get('format') != 'tinybaselines.ppo.v1':
71
+ raise ValueError('unsupported PPO checkpoint format')
72
+ policy = GaussianActorCritic(**json.loads(metadata['policy_config']))
73
+ ppo = cls(policy, **json.loads(metadata['ppo_config']))
74
+ nn.state.load_state_dict(ppo.state_dict(), nn.state.safe_load(path), verbose=False)
75
+ policy.refresh_inference()
76
+ return ppo
77
+
78
+ def compute_advantages(self, rewards, state_values, terminated, truncated, next_values):
79
+ T, N = rewards.shape
80
+ advantages = np.zeros_like(rewards, dtype=np.float32)
81
+ gae = np.zeros(N, dtype=np.float32)
82
+ episode_ends = terminated | truncated
83
+ for t in reversed(range(T)):
84
+ delta = rewards[t] + self.gamma * next_values[t] * ~terminated[t] - state_values[t]
85
+ gae = delta + self.gamma * self.lamda * ~episode_ends[t] * gae
86
+ advantages[t] = gae
87
+ returns = advantages + state_values
88
+ advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
89
+ return advantages.reshape(-1), returns.reshape(-1)
90
+
91
+ def compute_loss(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
92
+ action_means, log_std, state_values = self.actor_critic(batch_states)
93
+ action_logprobs = normal_log_prob(action_means, log_std, batch_actions)
94
+ ratios = (action_logprobs - batch_logprobs).exp()
95
+ unclipped = ratios * batch_advantages
96
+ clipped = ratios.clip(1 - self.eps_clip, 1 + self.eps_clip) * batch_advantages
97
+ actor_loss = -unclipped.minimum(clipped).mean()
98
+ critic_loss = (state_values.squeeze(-1) - batch_returns).square().mean()
99
+ entropy = (log_std + NORMAL_ENTROPY_CONSTANT).sum()
100
+ return actor_loss + self.vf_coef * critic_loss - self.entropy_coef * entropy
101
+
102
+ def train_step(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
103
+ size = batch_states.shape[0]
104
+ if size not in self._train_steps:
105
+ self._train_steps[size] = TinyJit(self._train_step)
106
+ loss = self._train_steps[size](batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns)
107
+ self.actor_critic.refresh_inference()
108
+ return loss
109
+
110
+ @Context(TRAINING=1)
111
+ def _train_step(self, batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns):
112
+ self.optimizer.zero_grad()
113
+ loss = self.compute_loss(
114
+ batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns
115
+ )
116
+ loss.backward()
117
+ clip_grad_norm(self.actor_parameters, 0.5)
118
+ clip_grad_norm(self.critic_parameters, 0.5)
119
+ return loss.realize(*self.optimizer.schedule_step())
120
+
121
+ def prepare(self, batch):
122
+ T, N = batch.rewards.shape
123
+ obs_flat = self.actor_critic.preprocess(batch.observations.reshape(T * N, -1))
124
+ raw_act_np = batch.raw_actions.reshape(T * N, -1).astype(np.float32)
125
+ raw_act_flat = Tensor(raw_act_np, device=self.device)
126
+ old_mean, old_log_std, old_value = self.actor_critic(obs_flat)
127
+ old_logprobs_np = normal_log_prob(old_mean, old_log_std, raw_act_flat).numpy()
128
+ old_values_np = old_value.numpy().reshape(T, N)
129
+ next_obs_t = self.actor_critic.preprocess(batch.next_observations.reshape(T * N, -1))
130
+ next_values_np = self.actor_critic.critic(next_obs_t).numpy().reshape(T, N)
131
+ advantages_np, returns_np = self.compute_advantages(
132
+ batch.rewards, old_values_np, batch.terminated, batch.truncated, next_values_np
133
+ )
134
+
135
+ old_logprobs = Tensor(old_logprobs_np, device=self.device)
136
+ advantages = Tensor(advantages_np, device=self.device)
137
+ returns = Tensor(returns_np, device=self.device)
138
+ return obs_flat, raw_act_flat, old_logprobs, advantages, returns
139
+
140
+ def update(self, batch):
141
+ obs_flat, raw_act_flat, old_logprobs, advantages, returns = self.prepare(batch)
142
+ num_samples = obs_flat.shape[0]
143
+
144
+ for _ in range(self.K_epochs):
145
+ permutation = np.random.permutation(num_samples).astype(np.int32)
146
+ for start in range(0, num_samples, self.batch_size):
147
+ idx = Tensor(permutation[start:start + self.batch_size], device=self.device)
148
+ batch_states = obs_flat[idx].contiguous().realize()
149
+ batch_actions = raw_act_flat[idx].contiguous().realize()
150
+ batch_logprobs = old_logprobs[idx].contiguous().realize()
151
+ batch_advantages = advantages[idx].contiguous().realize()
152
+ batch_returns = returns[idx].contiguous().realize()
153
+ self.train_step(
154
+ batch_states, batch_actions, batch_logprobs, batch_advantages, batch_returns
155
+ )
@@ -0,0 +1,80 @@
1
+ from dataclasses import dataclass
2
+ from typing import NamedTuple
3
+
4
+ import numpy as np
5
+
6
+
7
+ @dataclass
8
+ class RolloutState:
9
+ observations: np.ndarray
10
+ episode_returns: np.ndarray
11
+
12
+
13
+ class RolloutBatch(NamedTuple):
14
+ observations: np.ndarray
15
+ raw_actions: np.ndarray
16
+ rewards: np.ndarray
17
+ terminated: np.ndarray
18
+ truncated: np.ndarray
19
+ next_observations: np.ndarray
20
+
21
+
22
+ def track_episode_returns(done_mask, ep_returns, ep_rets, remaining=None):
23
+ for idx in np.where(done_mask)[0]:
24
+ if remaining is None or remaining[idx] > 0:
25
+ ep_returns.append(ep_rets[idx])
26
+ if remaining is not None:
27
+ remaining[idx] -= 1
28
+ ep_rets[idx] = 0.0
29
+
30
+
31
+ def rollout(env, actor_critic, num_steps=None, num_episodes=None, deterministic=False, state=None):
32
+ assert (num_steps is None) != (num_episodes is None), 'Specify exactly one: num_steps or num_episodes'
33
+ if num_episodes is not None and num_episodes <= 0:
34
+ raise ValueError('num_episodes must be positive')
35
+
36
+ N = env.num_envs
37
+ if state is None:
38
+ state = RolloutState(env.reset()[0], np.zeros(N))
39
+ states = state.observations
40
+ ep_returns, ep_rets = [], state.episode_returns
41
+
42
+ collect = num_steps is not None
43
+ remaining = None if collect else (num_episodes + np.arange(N)) // N
44
+ if collect:
45
+ T = num_steps // N
46
+ obs = np.empty((T, *states.shape), dtype=np.float32)
47
+ next_obs = np.empty_like(obs)
48
+ rew = np.empty((T, N), dtype=np.float32)
49
+ terminations = np.empty((T, N), dtype=bool)
50
+ truncations = np.empty((T, N), dtype=bool)
51
+
52
+ t = 0
53
+ while True:
54
+ actions, raw_actions = actor_critic.act(states, deterministic=deterministic)
55
+ if collect:
56
+ if t == 0:
57
+ raw_act = np.empty((T, *raw_actions.shape), dtype=np.float32)
58
+ obs[t], raw_act[t] = states, raw_actions
59
+
60
+ states, rewards, terminated, truncated, infos = env.step(actions)
61
+ d = np.logical_or(terminated, truncated)
62
+ if collect:
63
+ rew[t], terminations[t], truncations[t] = rewards, terminated, truncated
64
+ next_obs[t] = states
65
+ final_obs = infos.get('final_obs')
66
+ for idx in np.flatnonzero(d):
67
+ if final_obs is not None and final_obs[idx] is not None:
68
+ next_obs[t, idx] = final_obs[idx]
69
+ elif truncated[idx] and not terminated[idx]:
70
+ raise ValueError('truncated transitions require final_obs')
71
+
72
+ ep_rets += rewards
73
+ track_episode_returns(d, ep_returns, ep_rets, remaining)
74
+ t += 1
75
+
76
+ if (collect and t >= T) or (not collect and not remaining.any()):
77
+ break
78
+
79
+ state.observations = states
80
+ return (RolloutBatch(obs, raw_act, rew, terminations, truncations, next_obs), ep_returns) if collect else ep_returns
@@ -0,0 +1,32 @@
1
+ from time import perf_counter
2
+ from typing import NamedTuple
3
+
4
+ import numpy as np
5
+
6
+ from .rollout import RolloutState, rollout
7
+
8
+
9
+ class TrainingStep(NamedTuple):
10
+ timesteps: int
11
+ episode_returns: list[float]
12
+ rollout_seconds: float
13
+ update_seconds: float
14
+
15
+
16
+ def learn(env, ppo, total_timesteps, rollout_steps):
17
+ """Train in full rollouts, yielding progress after each update. The timestep budget is a minimum."""
18
+ if total_timesteps < 0:
19
+ raise ValueError('total_timesteps must be nonnegative')
20
+ if rollout_steps <= 0 or rollout_steps % env.num_envs:
21
+ raise ValueError('rollout_steps must be a positive multiple of env.num_envs')
22
+
23
+ state = None
24
+ for timesteps in range(0, total_timesteps, rollout_steps):
25
+ if state is None:
26
+ state = RolloutState(env.reset()[0], np.zeros(env.num_envs))
27
+ started = perf_counter()
28
+ batch, returns = rollout(env, ppo.actor_critic, num_steps=rollout_steps, state=state)
29
+ collected = perf_counter()
30
+ ppo.update(batch)
31
+ updated = perf_counter()
32
+ yield TrainingStep(timesteps + batch.rewards.size, returns, collected - started, updated - collected)
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: tinybaselines
3
+ Version: 0.1.0a1
4
+ Summary: PPO in tinygrad.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: tinygrad==0.14.0
10
+ Requires-Dist: numpy<3,>=1.26
11
+ Dynamic: license-file
12
+
13
+ # tinybaselines
14
+
15
+ PPO in tinygrad. Continuous actions clipped to `[-1, 1]`.
16
+
17
+ Python 3.11 and clang.
18
+
19
+ ```sh
20
+ python3.11 -m venv .venv
21
+ source .venv/bin/activate
22
+ pip install -e . -r examples/lunar_lander/requirements.txt
23
+ ```
24
+
25
+ Train, save, and load.
26
+
27
+ ```python
28
+ from statistics import mean
29
+ from tinybaselines import GaussianActorCritic, PPO, evaluate, learn
30
+ from examples.lunar_lander.config import OBS_SCALE
31
+ from examples.lunar_lander.train import make_env
32
+
33
+ env = make_env(4, asynchronous=False)
34
+ policy = GaussianActorCritic(state_dim=8, action_dim=2, hidden_dim=128,
35
+ actor_layers=4, critic_layers=4, observation_scale=OBS_SCALE)
36
+ ppo = PPO(policy, pi_lr=1e-3, vf_lr=1e-3, gamma=0.99, lamda=0.95,
37
+ K_epochs=20, eps_clip=0.2, batch_size=10_000, vf_coef=1.0, entropy_coef=0.001)
38
+
39
+ for step in learn(env, ppo, total_timesteps=2_000_000, rollout_steps=20_000):
40
+ print(step.timesteps, step.rollout_seconds, step.update_seconds)
41
+
42
+ ppo.save('ppo.safetensors')
43
+ ppo = PPO.load('ppo.safetensors')
44
+ policy = ppo.actor_critic
45
+ observations, _ = env.reset()
46
+ actions, raw_actions = policy.act(observations, deterministic=True)
47
+ env.close()
48
+
49
+ eval_env = make_env(4, asynchronous=False)
50
+ returns = evaluate(eval_env, policy, num_episodes=100)
51
+ eval_env.close()
52
+ print(mean(returns))
53
+ ```
54
+
55
+ `total_timesteps` and `rollout_steps` count transitions across all environments.
56
+ Use a positive multiple of `env.num_envs` for `rollout_steps`. Training stops
57
+ after the first full rollout that reaches the budget. Each step contains completed
58
+ `episode_returns` and collection/update times. The caller owns the environment.
59
+ Use a separate environment for evaluation during training.
60
+ `evaluate` resets its environment and returns complete episode returns. It samples
61
+ actions by default; `deterministic=True` uses clipped policy means.
62
+ `actions` go to the environment; `raw_actions` go into PPO.
63
+ The collector uses Gymnasium vector environments with same-step autoreset.
64
+ For time-limit bootstrapping, the collector reads pre-reset observations from `infos['final_obs']`.
65
+
66
+ PPO checkpoints contain the policy, observation scale, Adam state, and PPO settings.
67
+ Continue training by passing the loaded PPO to `learn` with a new transition budget.
68
+ Each `learn` call starts fresh episodes and a new transition counter.
69
+ `policy.save(path)` exports a policy for inference.
70
+
71
+ The full LunarLander trainer, evaluation, and PyTorch reference live in
72
+ `examples/lunar_lander`.
73
+
74
+ ```sh
75
+ python -m examples.lunar_lander.train
76
+ ```
77
+
78
+ Pendulum uses three observations and one action. Its example rescales policy actions
79
+ from `[-1, 1]` to torque in `[-2, 2]`.
80
+
81
+ ```sh
82
+ pip install -e . -r examples/pendulum/requirements.txt
83
+ DEV=CPU BEAM=0 python -m examples.pendulum.train --output runs/pendulum
84
+ ```
85
+
86
+ The output directory contains the configuration, episode returns, update times,
87
+ and a PPO checkpoint. Initial and final evaluations each use 100 fresh stochastic episodes.
88
+
89
+ Three independent four-environment CPU runs reached mean returns of -141.4, -162.2,
90
+ and -150.8 after 200,704 transitions each. Each final evaluation used the reloaded checkpoint.
91
+ Our acceptance cutoff is -200 over 100 fresh stochastic episodes per run; all three passed.
92
+ Gymnasium defines no solved threshold for Pendulum. We chose this cutoff after the first run.
93
+ Run records: [1](examples/pendulum/results/cpu.jsonl),
94
+ [2](examples/pendulum/results/cpu-2.jsonl), [3](examples/pendulum/results/cpu-3.jsonl).
@@ -0,0 +1,11 @@
1
+ tinybaselines/__init__.py,sha256=YF5iiW_X6vwTmNRdiaQHMNgE7VMa2idpulh6IfqRzds,195
2
+ tinybaselines/evaluation.py,sha256=3ti_T2gtJgpulgX1C0HEVxXITXfxPGkACsJnKfUOE_o,246
3
+ tinybaselines/policies.py,sha256=u1fOphgpKufGYBquVdpE3cWhotBUUVcq79-XfQcL3DM,3358
4
+ tinybaselines/ppo.py,sha256=PmChTu-iHEHIhCr1NxL60qK3AXs2jItQOPMj0zhcGY8,7877
5
+ tinybaselines/rollout.py,sha256=9niFh9epX-W4P5rlzPc3b-UYrmHHqM7MOK-CnE4L9O8,2864
6
+ tinybaselines/training.py,sha256=5VfZq4S7xlBs6PtdIgDP51Ei1uNGdBqi6f-SNESLYls,1171
7
+ tinybaselines-0.1.0a1.dist-info/licenses/LICENSE,sha256=voIoq73Yv9HUCAMenLnIuZ-t4TJa_CfIO7TKUlwhWlc,1069
8
+ tinybaselines-0.1.0a1.dist-info/METADATA,sha256=O_qFoN-6Jeja_wmcxoslMIdglQAK3bq1ZqLwQZeq9q0,3728
9
+ tinybaselines-0.1.0a1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
10
+ tinybaselines-0.1.0a1.dist-info/top_level.txt,sha256=P-g_X6Qa5L4-vsMCPSgUkolHGQMiDfxhra0_KSeEhpg,14
11
+ tinybaselines-0.1.0a1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Utkarsh Gill
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tinybaselines