From 9e27f42f2701c4767ce38feecc5356c02dbce9e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sat, 4 Jul 2026 22:30:16 +0900 Subject: [PATCH] Add JAX PPO static-opponent trainer --- README.md | 41 ++ configs/jax_ppo/balanced.yaml | 40 ++ configs/jax_ppo/cautious.yaml | 40 ++ configs/jax_ppo/discard-only.yaml | 40 ++ configs/jax_ppo/smoke.yaml | 40 ++ pyproject.toml | 4 + src/lost_cities_jax/__init__.py | 12 + src/lost_cities_jax/opponents.py | 146 +++++ src/lost_cities_jax/ppo.py | 812 ++++++++++++++++++++++++ src/lost_cities_jax/ppo_cli.py | 13 + tests/lost_cities_jax/test_ppo_stack.py | 86 +++ uv.lock | 408 ++++++++++++ 12 files changed, 1682 insertions(+) create mode 100644 configs/jax_ppo/balanced.yaml create mode 100644 configs/jax_ppo/cautious.yaml create mode 100644 configs/jax_ppo/discard-only.yaml create mode 100644 configs/jax_ppo/smoke.yaml create mode 100644 src/lost_cities_jax/opponents.py create mode 100644 src/lost_cities_jax/ppo.py create mode 100644 src/lost_cities_jax/ppo_cli.py create mode 100644 tests/lost_cities_jax/test_ppo_stack.py diff --git a/README.md b/README.md index 2166ade..3c6f0e9 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,47 @@ steps_per_sec=3956598.38 - Observation scalar normalization is implementation-defined as documented above and locked by the exported `OBS_DIM`. +## JAX PPO Static-Opponent Ladder + +The first training stack above `lost_cities_jax` is exposed as: + +```bash +uv run lost-cities-jax-ppo --help +``` + +CPU smoke: + +```bash +uv run lost-cities-jax-ppo rollout-smoke --config configs/jax_ppo/smoke.yaml +uv run lost-cities-jax-ppo train --config configs/jax_ppo/smoke.yaml +``` + +GPU training uses optional CUDA JAX, keeping CUDA wheels out of the default +project dependency set: + +```bash +tmux new-session -s coolrl-jax-ppo-discard \ + -c /home/coolguy/dev/coolrl-lost-cities \ + "flock -n .compute.lock uv run --with 'jax[cuda12]' lost-cities-jax-ppo train \ + --config configs/jax_ppo/discard-only.yaml" +``` + +Random-policy baseline vs `discard_only`, measured on 2026-07-04 with +8192 games x 400 plies on GPU: + +```text +return_mean=-55.582763671875 +game_length_mean=69.6085205078125 +max_steps_rate=0.0 +play_action_rate=0.28854578733444214 +opened_colors_mean=4.942626953125 +positive_expeditions_mean=0.41796875 +``` + +Large PPO artifacts are written under +`/mnt/2tbhdd/coolrl-lost-cities-artifacts/jax-ppo-static-opponents/`. +Generated checkpoints and evaluation JSON are not committed to git. + ## Basic Usage ```python diff --git a/configs/jax_ppo/balanced.yaml b/configs/jax_ppo/balanced.yaml new file mode 100644 index 0000000..7155264 --- /dev/null +++ b/configs/jax_ppo/balanced.yaml @@ -0,0 +1,40 @@ +run: + experiment_name: jax-ppo-balanced + seed: 20260704 + learner_seat: 0 + total_updates: 250 + log_every: 1 + checkpoint_every: 10 + artifact_root: /mnt/2tbhdd/coolrl-lost-cities-artifacts/jax-ppo-static-opponents + +opponent: + name: heuristic_balanced + +network: + hidden_size: 512 + num_layers: 3 + +ppo: + batch_games: 8192 + rollout_steps: 400 + gamma: 1.0 + gae_lambda: 0.95 + clip_epsilon: 0.2 + entropy_coef: 0.01 + value_coef: 0.5 + max_grad_norm: 0.5 + learning_rate: 0.0003 + epochs: 4 + minibatches: 128 + +reward: + terminal_scale: 50.0 + potential_shaping_initial: 1.0 + potential_shaping_final: 0.0 + potential_shaping_anneal_steps: 5000000 + +evaluation: + games: 10000 + duplicate: true + shuffle_bank_seed: 20260704 + batch_games: 8192 diff --git a/configs/jax_ppo/cautious.yaml b/configs/jax_ppo/cautious.yaml new file mode 100644 index 0000000..8f1986f --- /dev/null +++ b/configs/jax_ppo/cautious.yaml @@ -0,0 +1,40 @@ +run: + experiment_name: jax-ppo-cautious + seed: 20260704 + learner_seat: 0 + total_updates: 250 + log_every: 1 + checkpoint_every: 10 + artifact_root: /mnt/2tbhdd/coolrl-lost-cities-artifacts/jax-ppo-static-opponents + +opponent: + name: heuristic_cautious + +network: + hidden_size: 512 + num_layers: 3 + +ppo: + batch_games: 8192 + rollout_steps: 400 + gamma: 1.0 + gae_lambda: 0.95 + clip_epsilon: 0.2 + entropy_coef: 0.01 + value_coef: 0.5 + max_grad_norm: 0.5 + learning_rate: 0.0003 + epochs: 4 + minibatches: 128 + +reward: + terminal_scale: 50.0 + potential_shaping_initial: 1.0 + potential_shaping_final: 0.0 + potential_shaping_anneal_steps: 5000000 + +evaluation: + games: 10000 + duplicate: true + shuffle_bank_seed: 20260704 + batch_games: 8192 diff --git a/configs/jax_ppo/discard-only.yaml b/configs/jax_ppo/discard-only.yaml new file mode 100644 index 0000000..53b70be --- /dev/null +++ b/configs/jax_ppo/discard-only.yaml @@ -0,0 +1,40 @@ +run: + experiment_name: jax-ppo-discard-only + seed: 20260704 + learner_seat: 0 + total_updates: 250 + log_every: 1 + checkpoint_every: 10 + artifact_root: /mnt/2tbhdd/coolrl-lost-cities-artifacts/jax-ppo-static-opponents + +opponent: + name: discard_only + +network: + hidden_size: 512 + num_layers: 3 + +ppo: + batch_games: 8192 + rollout_steps: 400 + gamma: 1.0 + gae_lambda: 0.95 + clip_epsilon: 0.2 + entropy_coef: 0.01 + value_coef: 0.5 + max_grad_norm: 0.5 + learning_rate: 0.0003 + epochs: 4 + minibatches: 128 + +reward: + terminal_scale: 50.0 + potential_shaping_initial: 1.0 + potential_shaping_final: 0.0 + potential_shaping_anneal_steps: 5000000 + +evaluation: + games: 10000 + duplicate: true + shuffle_bank_seed: 20260704 + batch_games: 8192 diff --git a/configs/jax_ppo/smoke.yaml b/configs/jax_ppo/smoke.yaml new file mode 100644 index 0000000..558bf7e --- /dev/null +++ b/configs/jax_ppo/smoke.yaml @@ -0,0 +1,40 @@ +run: + experiment_name: jax-ppo-smoke + seed: 20260704 + learner_seat: 0 + total_updates: 1 + log_every: 1 + checkpoint_every: 1 + artifact_root: runs/tmp/jax-ppo-artifacts + +opponent: + name: discard_only + +network: + hidden_size: 64 + num_layers: 2 + +ppo: + batch_games: 64 + rollout_steps: 64 + gamma: 1.0 + gae_lambda: 0.95 + clip_epsilon: 0.2 + entropy_coef: 0.01 + value_coef: 0.5 + max_grad_norm: 0.5 + learning_rate: 0.0003 + epochs: 1 + minibatches: 4 + +reward: + terminal_scale: 50.0 + potential_shaping_initial: 1.0 + potential_shaping_final: 0.0 + potential_shaping_anneal_steps: 100000 + +evaluation: + games: 128 + duplicate: true + shuffle_bank_seed: 20260704 + batch_games: 64 diff --git a/pyproject.toml b/pyproject.toml index 95a849a..13ba2fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,9 @@ dependencies = [ "torch>=2.3.0", "jax>=0.10.2", "chex>=0.1.92", + "flax>=0.12.7", + "optax>=0.2.8", + "orbax-checkpoint>=0.12.1", ] [project.optional-dependencies] @@ -32,6 +35,7 @@ lost-cities-eval = "coolrl_lost_cities.games.classic.evaluation:main" lost-cities-classic-gui = "coolrl_lost_cities.games.classic.pygame_pvp:main" lost-cities-deep-cfr = "coolrl_lost_cities.games.classic.deep_cfr.cli:main" lost-cities-ismcts = "coolrl_lost_cities.games.classic.ismcts.cli:main" +lost-cities-jax-ppo = "lost_cities_jax.ppo_cli:main" [dependency-groups] dev = [ diff --git a/src/lost_cities_jax/__init__.py b/src/lost_cities_jax/__init__.py index 614d380..5e51d02 100644 --- a/src/lost_cities_jax/__init__.py +++ b/src/lost_cities_jax/__init__.py @@ -18,6 +18,13 @@ from lost_cities_jax.engine import ( step, ) from lost_cities_jax.obs import observation +from lost_cities_jax.opponents import ( + discard_only_action, + heuristic_balanced_action, + heuristic_cautious_action, + policy_by_name, + random_legal_action, +) from lost_cities_jax.types import ( MAX_STEPS, N_ACTIONS, @@ -38,8 +45,13 @@ __all__ = [ "board_score", "current_hand_sorted", "decode_action", + "discard_only_action", + "heuristic_balanced_action", + "heuristic_cautious_action", "legal_action_mask", "observation", + "policy_by_name", + "random_legal_action", "reset", "reset_from_order", "score", diff --git a/src/lost_cities_jax/opponents.py b/src/lost_cities_jax/opponents.py new file mode 100644 index 0000000..b3d71b9 --- /dev/null +++ b/src/lost_cities_jax/opponents.py @@ -0,0 +1,146 @@ +"""Pure-JAX static opponent policies for PPO diagnostics.""" + +from __future__ import annotations + +from collections.abc import Callable + +import jax +import jax.numpy as jnp + +from lost_cities_jax.engine import current_hand_sorted, legal_action_mask +from lost_cities_jax.types import ( + CARDS_PER_COLOR, + DISCARD, + DRAW_DECK, + N_ACTIONS, + N_CARDS, + N_COLORS, + PLAY, + State, +) + +ActionPolicy = Callable[[State, jax.Array, jax.Array], jax.Array] + +ACTION_IDS = jnp.arange(N_ACTIONS, dtype=jnp.int32) +ACTION_HAND_SLOT = ACTION_IDS // 12 +ACTION_PLACE_TYPE = (ACTION_IDS % 12) // 6 +ACTION_DRAW_SOURCE = ACTION_IDS % 6 + + +def policy_by_name(name: str) -> ActionPolicy: + normalized = name.replace("-", "_") + if normalized == "discard_only": + return discard_only_action + if normalized == "heuristic_balanced": + return heuristic_balanced_action + if normalized == "heuristic_cautious": + return heuristic_cautious_action + msg = f"unknown opponent policy: {name}" + raise ValueError(msg) + + +def discard_only_action(state: State, player: jax.Array, rng: jax.Array) -> jax.Array: + del player, rng + mask = legal_action_mask(state) + scores = ( + (ACTION_PLACE_TYPE == DISCARD).astype(jnp.float32) * 100.0 + + (ACTION_DRAW_SOURCE == DRAW_DECK).astype(jnp.float32) * 10.0 + - ACTION_IDS.astype(jnp.float32) * 1.0e-4 + ) + return _argmax_legal(mask, scores) + + +def heuristic_balanced_action(state: State, player: jax.Array, rng: jax.Array) -> jax.Array: + del rng + return _heuristic_action(state, player, cautious=False) + + +def heuristic_cautious_action(state: State, player: jax.Array, rng: jax.Array) -> jax.Array: + del rng + return _heuristic_action(state, player, cautious=True) + + +def random_legal_action(state: State, player: jax.Array, rng: jax.Array) -> jax.Array: + del player + mask = legal_action_mask(state) + logits = jnp.where(mask, 0.0, -1.0e9) + return jax.random.categorical(rng, logits).astype(jnp.int32) + + +def _heuristic_action(state: State, player: jax.Array, *, cautious: bool) -> jax.Array: + player = player.astype(jnp.int32) + mask = legal_action_mask(state) + cards = current_hand_sorted(state, player).astype(jnp.int32) + action_cards = cards[ACTION_HAND_SLOT] + valid_card = action_cards < N_CARDS + color = jnp.clip(action_cards // CARDS_PER_COLOR, 0, N_COLORS - 1) + slot = action_cards % CARDS_PER_COLOR + is_handshake = slot < 3 + rank = jnp.where(is_handshake, 0, slot - 1).astype(jnp.float32) + + col_top = state.col_top[player, color].astype(jnp.float32) + col_len = state.col_len[player, color].astype(jnp.float32) + col_hs = state.col_hs[player, color].astype(jnp.float32) + is_open = col_len > 0 + + if cautious: + play_number_score = jnp.where( + is_open, + 16.0 + rank * 2.5 - (rank - col_top - 1.0) * 0.25, + rank * 2.0 - 8.0, + ) + play_hs_score = 4.0 - col_hs * 0.75 + discard_score = 13.0 - rank * 1.2 - is_open.astype(jnp.float32) * 5.0 + else: + play_number_score = jnp.where( + is_open, + 14.0 + rank * 2.3 - (rank - col_top - 1.0) * 0.2, + rank * 2.1 - 5.0, + ) + play_hs_score = 8.0 - col_hs * 0.5 + discard_score = 12.0 - rank - is_open.astype(jnp.float32) * 4.0 + + place_score = jnp.where( + ACTION_PLACE_TYPE == PLAY, + jnp.where(is_handshake, play_hs_score, play_number_score), + discard_score, + ) + draw_score = _draw_source_score(state, player, ACTION_DRAW_SOURCE) + scores = jnp.where(valid_card, place_score + draw_score, -1.0e9) + scores = scores - ACTION_IDS.astype(jnp.float32) * 1.0e-4 + return _argmax_legal(mask, scores) + + +def _draw_source_score(state: State, player: jax.Array, draw_source: jax.Array) -> jax.Array: + deck_score = jnp.where(draw_source == DRAW_DECK, 1.0, 0.0) + pile_color = jnp.clip(draw_source - 1, 0, N_COLORS - 1) + pile_len = state.pile_len[pile_color].astype(jnp.int32) + top_idx = jnp.maximum(pile_len - 1, 0) + top_card = jnp.clip(state.pile[pile_color, top_idx].astype(jnp.int32), 0, N_CARDS - 1) + slot = top_card % CARDS_PER_COLOR + is_handshake = slot < 3 + rank = jnp.where(is_handshake, 0, slot - 1) + color = top_card // CARDS_PER_COLOR + col_top = state.col_top[player.astype(jnp.int32), color] + can_play = jnp.where(is_handshake, col_top == 0, rank > col_top) + pile_score = jnp.where( + draw_source > 0, + jnp.where(can_play & (pile_len > 0), 9.0 + rank.astype(jnp.float32), 0.5), + 0.0, + ) + return deck_score + pile_score + + +def _argmax_legal(mask: jax.Array, scores: jax.Array) -> jax.Array: + legal_scores = jnp.where(mask, scores, -1.0e9) + return jnp.argmax(legal_scores).astype(jnp.int32) + + +__all__ = [ + "ActionPolicy", + "discard_only_action", + "heuristic_balanced_action", + "heuristic_cautious_action", + "policy_by_name", + "random_legal_action", +] diff --git a/src/lost_cities_jax/ppo.py b/src/lost_cities_jax/ppo.py new file mode 100644 index 0000000..6fb216d --- /dev/null +++ b/src/lost_cities_jax/ppo.py @@ -0,0 +1,812 @@ +"""Compact PPO stack for the JAX Lost Cities engine.""" + +from __future__ import annotations + +import argparse +import json +import math +import shutil +import time +from dataclasses import asdict, dataclass, field +from pathlib import Path +from typing import NamedTuple + +import flax.linen as nn +import jax +import jax.numpy as jnp +import optax +import orbax.checkpoint as ocp +import yaml +from flax.training import train_state + +from lost_cities_jax.engine import board_score, legal_action_mask, reset, reset_from_order, step +from lost_cities_jax.obs import observation +from lost_cities_jax.opponents import policy_by_name, random_legal_action +from lost_cities_jax.types import ( + CARDS_PER_COLOR, + LOC_P0_BOARD, + MAX_STEPS, + N_ACTIONS, + N_CARDS, + N_COLORS, + OBS_DIM, + PLAY, + State, +) + +ARTIFACT_ROOT = "/mnt/2tbhdd/coolrl-lost-cities-artifacts/jax-ppo-static-opponents" +NEG_INF = -1.0e9 + + +@dataclass +class RunConfig: + experiment_name: str = "jax-ppo-discard-only" + seed: int = 20260704 + learner_seat: int = 0 + total_updates: int = 200 + log_every: int = 1 + checkpoint_every: int = 10 + artifact_root: str = ARTIFACT_ROOT + + +@dataclass +class OpponentConfig: + name: str = "discard_only" + + +@dataclass +class NetworkConfig: + hidden_size: int = 512 + num_layers: int = 3 + + +@dataclass +class PPOHyperConfig: + batch_games: int = 8192 + rollout_steps: int = MAX_STEPS + gamma: float = 1.0 + gae_lambda: float = 0.95 + clip_epsilon: float = 0.2 + entropy_coef: float = 0.01 + value_coef: float = 0.5 + max_grad_norm: float = 0.5 + learning_rate: float = 3.0e-4 + epochs: int = 4 + minibatches: int = 8 + + +@dataclass +class RewardConfig: + terminal_scale: float = 50.0 + potential_shaping_initial: float = 1.0 + potential_shaping_final: float = 0.0 + potential_shaping_anneal_steps: int = 5_000_000 + + +@dataclass +class EvalConfig: + games: int = 10_000 + duplicate: bool = True + shuffle_bank_seed: int = 20260704 + batch_games: int = 8192 + + +@dataclass +class JaxPPOConfig: + run: RunConfig = field(default_factory=RunConfig) + opponent: OpponentConfig = field(default_factory=OpponentConfig) + network: NetworkConfig = field(default_factory=NetworkConfig) + ppo: PPOHyperConfig = field(default_factory=PPOHyperConfig) + reward: RewardConfig = field(default_factory=RewardConfig) + evaluation: EvalConfig = field(default_factory=EvalConfig) + + +class ActorCritic(nn.Module): + hidden_size: int = 512 + num_layers: int = 3 + + @nn.compact + def __call__(self, obs: jax.Array) -> tuple[jax.Array, jax.Array]: + x = obs + for _ in range(self.num_layers): + x = nn.Dense(self.hidden_size)(x) + x = nn.relu(x) + logits = nn.Dense(N_ACTIONS)(x) + value = nn.Dense(1)(x) + return logits, jnp.squeeze(value, axis=-1) + + +class TrainState(train_state.TrainState): + pass + + +class Transition(NamedTuple): + obs: jax.Array + legal_mask: jax.Array + action: jax.Array + log_prob: jax.Array + value: jax.Array + reward: jax.Array + done: jax.Array + active: jax.Array + actor_mask: jax.Array + entropy: jax.Array + play_action: jax.Array + + +class EvalBatch(NamedTuple): + wins: jax.Array + losses: jax.Array + ties: jax.Array + score_diff_sum: jax.Array + game_length_sum: jax.Array + play_action_sum: jax.Array + learner_action_sum: jax.Array + opened_colors_sum: jax.Array + positive_expeditions_sum: jax.Array + games: jax.Array + + +def load_config(path: str | Path | None = None, overrides: dict | None = None) -> JaxPPOConfig: + data: dict = {} + if path is not None: + with Path(path).open("r", encoding="utf-8") as handle: + loaded = yaml.safe_load(handle) or {} + if not isinstance(loaded, dict): + msg = f"config root must be a mapping: {path}" + raise ValueError(msg) + data = loaded + if overrides: + data = _deep_merge(data, overrides) + cfg = JaxPPOConfig() + return JaxPPOConfig( + run=RunConfig(**{**asdict(cfg.run), **data.get("run", {})}), + opponent=OpponentConfig(**{**asdict(cfg.opponent), **data.get("opponent", {})}), + network=NetworkConfig(**{**asdict(cfg.network), **data.get("network", {})}), + ppo=PPOHyperConfig(**{**asdict(cfg.ppo), **data.get("ppo", {})}), + reward=RewardConfig(**{**asdict(cfg.reward), **data.get("reward", {})}), + evaluation=EvalConfig(**{**asdict(cfg.evaluation), **data.get("evaluation", {})}), + ) + + +def create_train_state(cfg: JaxPPOConfig, rng: jax.Array) -> TrainState: + model = ActorCritic(cfg.network.hidden_size, cfg.network.num_layers) + params = model.init(rng, jnp.zeros((1, OBS_DIM), dtype=jnp.float32)) + tx = optax.chain( + optax.clip_by_global_norm(cfg.ppo.max_grad_norm), + optax.adam(cfg.ppo.learning_rate), + ) + return TrainState.create(apply_fn=model.apply, params=params, tx=tx) + + +def train(cfg: JaxPPOConfig, *, resume: str | None = None) -> Path: + run_dir = _create_run_dir(cfg) + _write_json(run_dir / "config.json", asdict(cfg)) + metrics_path = run_dir / "metrics.jsonl" + + rng = jax.random.PRNGKey(cfg.run.seed) + rng, init_key, reset_key = jax.random.split(rng, 3) + state = create_train_state(cfg, init_key) + env_state = jax.jit(jax.vmap(reset))(jax.random.split(reset_key, cfg.ppo.batch_games)) + + if resume: + state = restore_checkpoint(Path(resume), state) + + opponent_policy = policy_by_name(cfg.opponent.name) + train_iteration = make_train_iteration(cfg, opponent_policy) + start = time.perf_counter() + + for update in range(cfg.run.total_updates): + shaping_coef = shaping_coefficient( + cfg, update * cfg.ppo.batch_games * cfg.ppo.rollout_steps + ) + iter_start = time.perf_counter() + state, env_state, rng, metrics = train_iteration(state, env_state, rng, shaping_coef) + jax.tree_util.tree_leaves(metrics)[0].block_until_ready() + row = _metrics_to_row(metrics, update, shaping_coef, cfg) + row["iteration_seconds"] = time.perf_counter() - iter_start + row["elapsed_seconds"] = time.perf_counter() - start + _append_jsonl(metrics_path, row) + + if update % cfg.run.log_every == 0: + print( + json.dumps( + { + "update": update, + "return_mean": row["return_mean"], + "game_length_mean": row["game_length_mean"], + "play_action_rate": row["play_action_rate"], + "entropy_mean": row["entropy_mean"], + }, + sort_keys=True, + ), + flush=True, + ) + + if cfg.run.checkpoint_every and (update + 1) % cfg.run.checkpoint_every == 0: + save_checkpoint(run_dir / f"checkpoint_{update + 1:06d}", state, cfg) + save_checkpoint(run_dir / "latest", state, cfg) + + save_checkpoint(run_dir / "latest", state, cfg) + return run_dir + + +def make_train_iteration(cfg: JaxPPOConfig, opponent_policy): + rollout_fn = make_rollout_fn(cfg, opponent_policy) + + @jax.jit + def train_iteration( + state: TrainState, env_state: State, rng: jax.Array, shaping_coef: jax.Array + ): + rng, rollout_key, update_key = jax.random.split(rng, 3) + env_state, transitions, rollout_metrics = rollout_fn( + state, env_state, rollout_key, shaping_coef + ) + advantages, returns = compute_gae( + transitions.reward, + transitions.value, + transitions.done, + cfg.ppo.gamma, + cfg.ppo.gae_lambda, + ) + state, update_metrics = ppo_update(state, transitions, advantages, returns, update_key, cfg) + return state, env_state, rng, {**rollout_metrics, **update_metrics} + + return train_iteration + + +def make_rollout_fn(cfg: JaxPPOConfig, opponent_policy): + learner = jnp.asarray(cfg.run.learner_seat, dtype=jnp.int32) + opponent = jnp.asarray(1 - cfg.run.learner_seat, dtype=jnp.int32) + + @jax.jit + def rollout_fn(state: TrainState, env_state: State, rng: jax.Array, shaping_coef: jax.Array): + def body(carry, _): + env, key = carry + key, learner_key, opponent_key = jax.random.split(key, 3) + obs = jax.vmap(observation, in_axes=(0, None))(env, learner) + legal = jax.vmap(legal_action_mask)(env) + logits, value = state.apply_fn(state.params, obs) + masked_logits = mask_logits(logits, legal) + learner_actions = jax.random.categorical(learner_key, masked_logits, axis=-1).astype( + jnp.int32 + ) + log_prob = action_log_prob(masked_logits, learner_actions) + entropy = categorical_entropy(masked_logits) + + opponent_keys = jax.random.split(opponent_key, cfg.ppo.batch_games) + opponent_actions = jax.vmap(opponent_policy, in_axes=(0, None, 0))( + env, opponent, opponent_keys + ) + learner_turn = env.to_move.astype(jnp.int32) == learner + active = ~env.done + actions = jnp.where(learner_turn, learner_actions, opponent_actions) + + before_diff = batch_score_diff(env, learner) + next_env, _, _ = jax.vmap(step, in_axes=(0, 0))(env, actions) + after_diff = batch_score_diff(next_env, learner) + terminal_reward = jnp.tanh(after_diff / cfg.reward.terminal_scale) + terminal = active & next_env.done + reward = jnp.where(terminal, terminal_reward, 0.0) + reward = reward + shaping_coef * (after_diff - before_diff) + reward = jnp.where(active, reward, 0.0) + + actor_mask = active & learner_turn + place_type = (actions % 12) // 6 + transition = Transition( + obs=obs, + legal_mask=legal, + action=actions, + log_prob=log_prob, + value=value, + reward=reward, + done=next_env.done, + active=active, + actor_mask=actor_mask, + entropy=entropy, + play_action=(place_type == PLAY) & actor_mask, + ) + return (next_env, key), transition + + (next_env, rng), transitions = jax.lax.scan( + body, (env_state, rng), xs=None, length=cfg.ppo.rollout_steps + ) + metrics = rollout_metrics(transitions, next_env, learner) + return next_env, transitions, metrics + + return rollout_fn + + +def random_rollout(cfg: JaxPPOConfig) -> dict: + opponent_policy = policy_by_name(cfg.opponent.name) + rng = jax.random.PRNGKey(cfg.run.seed) + rng, reset_key = jax.random.split(rng) + env_state = jax.jit(jax.vmap(reset))(jax.random.split(reset_key, cfg.ppo.batch_games)) + learner = jnp.asarray(cfg.run.learner_seat, dtype=jnp.int32) + opponent = jnp.asarray(1 - cfg.run.learner_seat, dtype=jnp.int32) + + @jax.jit + def rollout(env_state: State, rng: jax.Array): + def body(carry, _): + env, key = carry + key, learner_key, opponent_key = jax.random.split(key, 3) + learner_keys = jax.random.split(learner_key, cfg.ppo.batch_games) + opponent_keys = jax.random.split(opponent_key, cfg.ppo.batch_games) + learner_actions = jax.vmap(random_legal_action, in_axes=(0, None, 0))( + env, learner, learner_keys + ) + opponent_actions = jax.vmap(opponent_policy, in_axes=(0, None, 0))( + env, opponent, opponent_keys + ) + learner_turn = env.to_move.astype(jnp.int32) == learner + active = ~env.done + actions = jnp.where(learner_turn, learner_actions, opponent_actions) + before_diff = batch_score_diff(env, learner) + next_env, _, _ = jax.vmap(step, in_axes=(0, 0))(env, actions) + after_diff = batch_score_diff(next_env, learner) + reward = jnp.where(active, after_diff - before_diff, 0.0) + actor_mask = active & learner_turn + place_type = (actions % 12) // 6 + dummy_obs = jnp.zeros((cfg.ppo.batch_games, OBS_DIM), dtype=jnp.float32) + dummy_mask = jnp.zeros((cfg.ppo.batch_games, N_ACTIONS), dtype=jnp.bool_) + transition = Transition( + obs=dummy_obs, + legal_mask=dummy_mask, + action=actions, + log_prob=jnp.zeros((cfg.ppo.batch_games,), dtype=jnp.float32), + value=jnp.zeros((cfg.ppo.batch_games,), dtype=jnp.float32), + reward=reward, + done=next_env.done, + active=active, + actor_mask=actor_mask, + entropy=jnp.zeros((cfg.ppo.batch_games,), dtype=jnp.float32), + play_action=(place_type == PLAY) & actor_mask, + ) + return (next_env, key), transition + + (next_env, rng), transitions = jax.lax.scan( + body, (env_state, rng), xs=None, length=cfg.ppo.rollout_steps + ) + return rng, rollout_metrics(transitions, next_env, learner) + + rng, metrics = rollout(env_state, rng) + jax.tree_util.tree_leaves(metrics)[0].block_until_ready() + return _metrics_to_row(metrics, update=0, shaping_coef=0.0, cfg=cfg) + + +def ppo_update( + state: TrainState, + transitions: Transition, + advantages: jax.Array, + returns: jax.Array, + rng: jax.Array, + cfg: JaxPPOConfig, +) -> tuple[TrainState, dict[str, jax.Array]]: + batch_size = cfg.ppo.rollout_steps * cfg.ppo.batch_games + minibatch_size = batch_size // cfg.ppo.minibatches + flat = _flatten_transitions(transitions) + flat_advantages = advantages.reshape((batch_size,)) + flat_returns = returns.reshape((batch_size,)) + actor_mask = flat.actor_mask.astype(jnp.float32) + adv_mean = masked_mean(flat_advantages, actor_mask) + adv_std = jnp.sqrt(masked_mean((flat_advantages - adv_mean) ** 2, actor_mask) + 1.0e-8) + flat_advantages = (flat_advantages - adv_mean) / adv_std + + def loss_fn(params, mb, mb_adv, mb_returns): + logits, value = state.apply_fn(params, mb.obs) + masked_logits = mask_logits(logits, mb.legal_mask) + log_prob = action_log_prob(masked_logits, mb.action) + entropy = categorical_entropy(masked_logits) + ratio = jnp.exp(log_prob - mb.log_prob) + actor_weight = mb.actor_mask.astype(jnp.float32) + active_weight = mb.active.astype(jnp.float32) + unclipped = ratio * mb_adv + clipped = jnp.clip(ratio, 1.0 - cfg.ppo.clip_epsilon, 1.0 + cfg.ppo.clip_epsilon) * mb_adv + actor_loss = -masked_mean(jnp.minimum(unclipped, clipped), actor_weight) + value_loss = masked_mean((mb_returns - value) ** 2, active_weight) + entropy_loss = masked_mean(entropy, actor_weight) + loss = actor_loss + cfg.ppo.value_coef * value_loss - cfg.ppo.entropy_coef * entropy_loss + approx_kl = masked_mean(mb.log_prob - log_prob, actor_weight) + return loss, { + "loss": loss, + "actor_loss": actor_loss, + "value_loss": value_loss, + "entropy_loss": entropy_loss, + "approx_kl": approx_kl, + } + + def epoch_update(carry, key): + train_state = carry + metrics_init = { + "loss": jnp.array(0.0), + "actor_loss": jnp.array(0.0), + "value_loss": jnp.array(0.0), + "entropy_loss": jnp.array(0.0), + "approx_kl": jnp.array(0.0), + } + permutation = jax.random.permutation(key, batch_size) + + def minibatch_update(carry, idx): + train_state, metrics_accum = carry + start = idx * minibatch_size + mb_idx = jax.lax.dynamic_slice(permutation, (start,), (minibatch_size,)) + mb = jax.tree_util.tree_map(lambda x: x[mb_idx], flat) + mb_adv = flat_advantages[mb_idx] + mb_returns = flat_returns[mb_idx] + (loss, metrics), grads = jax.value_and_grad(loss_fn, has_aux=True)( + train_state.params, mb, mb_adv, mb_returns + ) + del loss + train_state = train_state.apply_gradients(grads=grads) + metrics_accum = jax.tree_util.tree_map(lambda a, b: a + b, metrics_accum, metrics) + return (train_state, metrics_accum), None + + (train_state, metrics), _ = jax.lax.scan( + minibatch_update, + (train_state, metrics_init), + jnp.arange(cfg.ppo.minibatches), + ) + metrics = jax.tree_util.tree_map(lambda x: x / cfg.ppo.minibatches, metrics) + return train_state, metrics + + keys = jax.random.split(rng, cfg.ppo.epochs) + state, metrics = jax.lax.scan(epoch_update, state, keys) + metrics = jax.tree_util.tree_map(lambda x: jnp.mean(x, axis=0), metrics) + return state, metrics + + +def compute_gae( + rewards: jax.Array, + values: jax.Array, + dones: jax.Array, + gamma: float, + gae_lambda: float, +) -> tuple[jax.Array, jax.Array]: + def body(carry, x): + next_value, next_advantage = carry + reward, value, done = x + nonterminal = 1.0 - done.astype(jnp.float32) + delta = reward + gamma * next_value * nonterminal - value + advantage = delta + gamma * gae_lambda * nonterminal * next_advantage + return (value, advantage), advantage + + init = (jnp.zeros_like(values[-1]), jnp.zeros_like(values[-1])) + _, advantages_rev = jax.lax.scan(body, init, (rewards[::-1], values[::-1], dones[::-1])) + advantages = advantages_rev[::-1] + return advantages, advantages + values + + +def evaluate( + cfg: JaxPPOConfig, + checkpoint: str | Path, + *, + opponent_name: str | None = None, + games: int | None = None, + duplicate: bool | None = None, + output: str | Path | None = None, +) -> dict: + opponent_name = opponent_name or cfg.opponent.name + games = games or cfg.evaluation.games + duplicate = cfg.evaluation.duplicate if duplicate is None else duplicate + eval_cfg = cfg.evaluation + state = create_train_state(cfg, jax.random.PRNGKey(0)) + state = restore_checkpoint(Path(checkpoint), state) + opponent_policy = policy_by_name(opponent_name) + eval_batch = make_eval_batch_fn(cfg, opponent_policy) + + total = None + for seat in [0, 1] if duplicate else [cfg.run.learner_seat]: + for start in range(0, games, eval_cfg.batch_games): + count = min(eval_cfg.batch_games, games - start) + orders = make_shuffle_bank(eval_cfg.shuffle_bank_seed + start, count) + metrics = eval_batch(state.params, jnp.asarray(orders, dtype=jnp.int8), seat) + metrics = jax.tree_util.tree_map(lambda x: float(x), metrics) + total = metrics if total is None else _sum_dicts(total, metrics) + + assert total is not None + total_games = int(total["games"]) + wins = int(total["wins"]) + win_rate = wins / total_games + wilson_low, wilson_high = wilson_interval(wins, total_games) + result = { + **total, + "games": total_games, + "win_rate": win_rate, + "wilson_low": wilson_low, + "wilson_high": wilson_high, + "mean_score_diff": total["score_diff_sum"] / total_games, + "mean_game_length": total["game_length_sum"] / total_games, + "play_action_rate": total["play_action_sum"] / max(total["learner_action_sum"], 1.0), + "opened_colors_per_game": total["opened_colors_sum"] / total_games, + "positive_expeditions_per_game": total["positive_expeditions_sum"] / total_games, + "opponent": opponent_name, + "duplicate": duplicate, + } + if output: + _write_json(Path(output), result) + return result + + +def make_eval_batch_fn(cfg: JaxPPOConfig, opponent_policy): + opponent_fns = opponent_policy + + @jax.jit + def eval_batch(params, deck_orders: jax.Array, learner_seat: int): + learner = jnp.asarray(learner_seat, dtype=jnp.int32) + opponent = 1 - learner + env_state = jax.vmap(reset_from_order)(deck_orders) + + def body(env, _): + obs = jax.vmap(observation, in_axes=(0, None))(env, learner) + legal = jax.vmap(legal_action_mask)(env) + model = ActorCritic(cfg.network.hidden_size, cfg.network.num_layers) + logits, _ = model.apply(params, obs) + learner_actions = jnp.argmax(mask_logits(logits, legal), axis=-1).astype(jnp.int32) + keys = jax.random.split(jax.random.PRNGKey(0), deck_orders.shape[0]) + opponent_actions = jax.vmap(opponent_fns, in_axes=(0, None, 0))(env, opponent, keys) + learner_turn = env.to_move.astype(jnp.int32) == learner + active = ~env.done + actions = jnp.where(learner_turn, learner_actions, opponent_actions) + next_env, _, _ = jax.vmap(step, in_axes=(0, 0))(env, actions) + place_type = (actions % 12) // 6 + return next_env, ((place_type == PLAY) & learner_turn & active, learner_turn & active) + + final_env, (play_actions, learner_actions) = jax.lax.scan( + body, env_state, xs=None, length=MAX_STEPS + ) + scores = jax.vmap(board_score)(final_env) + diff = scores[:, learner] - scores[:, opponent] + color_scores = jax.vmap(color_scores_for_player, in_axes=(0, None))(final_env, learner) + opened = jnp.sum(final_env.col_len[:, learner, :] > 0, axis=-1) + positive = jnp.sum(color_scores > 0, axis=-1) + return EvalBatch( + wins=jnp.sum(diff > 0), + losses=jnp.sum(diff < 0), + ties=jnp.sum(diff == 0), + score_diff_sum=jnp.sum(diff), + game_length_sum=jnp.sum(final_env.step_count), + play_action_sum=jnp.sum(play_actions), + learner_action_sum=jnp.sum(learner_actions), + opened_colors_sum=jnp.sum(opened), + positive_expeditions_sum=jnp.sum(positive), + games=deck_orders.shape[0], + )._asdict() + + return eval_batch + + +def rollout_metrics( + transitions: Transition, final_env: State, learner: jax.Array +) -> dict[str, jax.Array]: + episode_return = jnp.sum(transitions.reward, axis=0) + actor_count = jnp.sum(transitions.actor_mask) + active_count = jnp.sum(transitions.active) + color_scores = jax.vmap(color_scores_for_player, in_axes=(0, None))(final_env, learner) + opened = jnp.sum(final_env.col_len[:, learner, :] > 0, axis=-1) + positive = jnp.sum(color_scores > 0, axis=-1) + return { + "return_mean": jnp.mean(episode_return), + "return_std": jnp.std(episode_return), + "game_length_mean": jnp.mean(final_env.step_count.astype(jnp.float32)), + "game_length_max": jnp.max(final_env.step_count), + "max_steps_rate": jnp.mean(final_env.step_count >= MAX_STEPS), + "play_action_rate": jnp.sum(transitions.play_action) / jnp.maximum(actor_count, 1), + "opened_colors_mean": jnp.mean(opened.astype(jnp.float32)), + "positive_expeditions_mean": jnp.mean(positive.astype(jnp.float32)), + "entropy_mean": masked_mean( + transitions.entropy, transitions.actor_mask.astype(jnp.float32) + ), + "active_steps": active_count, + "learner_actions": actor_count, + } + + +def color_scores_for_player(state: State, player: jax.Array) -> jax.Array: + card_ids = jnp.arange(N_CARDS, dtype=jnp.int32) + colors = card_ids // CARDS_PER_COLOR + slots = card_ids % CARDS_PER_COLOR + ranks = jnp.where(slots >= 3, slots - 1, 0) + board_loc = LOC_P0_BOARD + player.astype(jnp.int32) + on_board = state.card_loc.astype(jnp.int32) == board_loc + rank_sum = [] + for color in range(N_COLORS): + rank_sum.append(jnp.sum(jnp.where(on_board & (colors == color), ranks, 0))) + rank_sum = jnp.stack(rank_sum).astype(jnp.int32) + length = state.col_len[player.astype(jnp.int32)].astype(jnp.int32) + handshakes = state.col_hs[player.astype(jnp.int32)].astype(jnp.int32) + base = (rank_sum - 20) * (1 + handshakes) + bonus = jnp.where(length >= 8, 20, 0) + return jnp.where(length > 0, base + bonus, 0).astype(jnp.float32) + + +def batch_score_diff(state: State, learner: jax.Array) -> jax.Array: + scores = jax.vmap(board_score)(state) + opponent = 1 - learner.astype(jnp.int32) + return scores[:, learner.astype(jnp.int32)] - scores[:, opponent] + + +def mask_logits(logits: jax.Array, legal_mask: jax.Array) -> jax.Array: + return jnp.where(legal_mask, logits, NEG_INF) + + +def action_log_prob(masked_logits: jax.Array, action: jax.Array) -> jax.Array: + log_probs = jax.nn.log_softmax(masked_logits, axis=-1) + return jnp.take_along_axis(log_probs, action[:, None], axis=-1)[:, 0] + + +def categorical_entropy(masked_logits: jax.Array) -> jax.Array: + log_probs = jax.nn.log_softmax(masked_logits, axis=-1) + probs = jnp.exp(log_probs) + return -jnp.sum(jnp.where(probs > 0, probs * log_probs, 0.0), axis=-1) + + +def masked_mean(values: jax.Array, weights: jax.Array) -> jax.Array: + return jnp.sum(values * weights) / jnp.maximum(jnp.sum(weights), 1.0) + + +def _flatten_transitions(transitions: Transition) -> Transition: + return jax.tree_util.tree_map(lambda x: x.reshape((-1, *x.shape[2:])), transitions) + + +def shaping_coefficient(cfg: JaxPPOConfig, env_steps: int) -> float: + reward_cfg = cfg.reward + if reward_cfg.potential_shaping_anneal_steps <= 0: + return reward_cfg.potential_shaping_final + progress = min(env_steps / reward_cfg.potential_shaping_anneal_steps, 1.0) + return reward_cfg.potential_shaping_initial + progress * ( + reward_cfg.potential_shaping_final - reward_cfg.potential_shaping_initial + ) + + +def make_shuffle_bank(seed: int, games: int) -> list[list[int]]: + import numpy as np + + rng = np.random.default_rng(seed) + return [rng.permutation(N_CARDS).astype("int8").tolist() for _ in range(games)] + + +def wilson_interval(wins: int, games: int, z: float = 1.959963984540054) -> tuple[float, float]: + if games == 0: + return 0.0, 0.0 + p = wins / games + denom = 1.0 + z * z / games + center = (p + z * z / (2 * games)) / denom + margin = z * math.sqrt((p * (1 - p) + z * z / (4 * games)) / games) / denom + return max(0.0, center - margin), min(1.0, center + margin) + + +def save_checkpoint(path: Path, state: TrainState, cfg: JaxPPOConfig) -> None: + del cfg + path = path.resolve() + if path.exists(): + shutil.rmtree(path) + checkpointer = ocp.PyTreeCheckpointer() + checkpointer.save(path, state, force=True) + + +def restore_checkpoint(path: Path, state: TrainState) -> TrainState: + return ocp.PyTreeCheckpointer().restore(path.resolve(), item=state) + + +def cli_main(argv: list[str] | None = None) -> None: + parser = argparse.ArgumentParser(prog="lost-cities-jax-ppo") + sub = parser.add_subparsers(dest="command", required=True) + + smoke = sub.add_parser("rollout-smoke") + smoke.add_argument("--config", required=True) + smoke.add_argument("--output") + smoke.add_argument("--set", action="append", default=[]) + + train_parser = sub.add_parser("train") + train_parser.add_argument("--config", required=True) + train_parser.add_argument("--resume") + train_parser.add_argument("--set", action="append", default=[]) + + eval_parser = sub.add_parser("eval") + eval_parser.add_argument("--config", required=True) + eval_parser.add_argument("--checkpoint", required=True) + eval_parser.add_argument("--opponent") + eval_parser.add_argument("--games", type=int) + eval_parser.add_argument("--duplicate", action="store_true") + eval_parser.add_argument("--output") + eval_parser.add_argument("--set", action="append", default=[]) + + args = parser.parse_args(argv) + cfg = load_config(args.config, overrides=parse_overrides(args.set)) + if args.command == "rollout-smoke": + row = random_rollout(cfg) + print(json.dumps(row, indent=2, sort_keys=True)) + if args.output: + _write_json(Path(args.output), row) + elif args.command == "train": + run_dir = train(cfg, resume=args.resume) + print(run_dir) + elif args.command == "eval": + result = evaluate( + cfg, + args.checkpoint, + opponent_name=args.opponent, + games=args.games, + duplicate=args.duplicate, + output=args.output, + ) + print(json.dumps(result, indent=2, sort_keys=True)) + + +def _create_run_dir(cfg: JaxPPOConfig) -> Path: + timestamp = time.strftime("%Y-%m-%d_%H%M%S") + root = Path(cfg.run.artifact_root).resolve() + run_dir = root / f"{timestamp}_{_slugify(cfg.run.experiment_name)}" + run_dir.mkdir(parents=True, exist_ok=False) + return run_dir + + +def _metrics_to_row( + metrics: dict[str, jax.Array], update: int, shaping_coef: float, cfg: JaxPPOConfig +) -> dict: + row = { + key: float(value) if getattr(value, "shape", ()) == () else value.tolist() + for key, value in metrics.items() + } + row["update"] = update + row["shaping_coef"] = float(shaping_coef) + row["env_steps"] = int((update + 1) * cfg.ppo.batch_games * cfg.ppo.rollout_steps) + row["opponent"] = cfg.opponent.name + return row + + +def _write_json(path: Path, data: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + +def _append_jsonl(path: Path, data: dict) -> None: + with path.open("a", encoding="utf-8") as handle: + handle.write(json.dumps(data, sort_keys=True) + "\n") + + +def _sum_dicts(left: dict, right: dict) -> dict: + return {key: left.get(key, 0.0) + right.get(key, 0.0) for key in set(left) | set(right)} + + +def _deep_merge(base: dict, override: dict) -> dict: + merged = dict(base) + for key, value in override.items(): + if isinstance(value, dict) and isinstance(merged.get(key), dict): + merged[key] = _deep_merge(merged[key], value) + else: + merged[key] = value + return merged + + +def parse_overrides(items: list[str]) -> dict: + root: dict = {} + for item in items: + if "=" not in item: + msg = f"--set must be PATH=VALUE, got: {item}" + raise ValueError(msg) + path, raw_value = item.split("=", 1) + value = yaml.safe_load(raw_value) + cursor = root + parts = path.split(".") + for part in parts[:-1]: + cursor = cursor.setdefault(part, {}) + cursor[parts[-1]] = value + return root + + +def _slugify(value: str) -> str: + return "".join(ch.lower() if ch.isalnum() else "-" for ch in value).strip("-") + + +__all__ = [ + "ActorCritic", + "JaxPPOConfig", + "TrainState", + "cli_main", + "create_train_state", + "evaluate", + "load_config", + "random_rollout", + "train", +] diff --git a/src/lost_cities_jax/ppo_cli.py b/src/lost_cities_jax/ppo_cli.py new file mode 100644 index 0000000..d70d7ef --- /dev/null +++ b/src/lost_cities_jax/ppo_cli.py @@ -0,0 +1,13 @@ +"""Console entrypoint for JAX PPO training.""" + +from __future__ import annotations + +from lost_cities_jax.ppo import cli_main + + +def main() -> None: + cli_main() + + +if __name__ == "__main__": + main() diff --git a/tests/lost_cities_jax/test_ppo_stack.py b/tests/lost_cities_jax/test_ppo_stack.py new file mode 100644 index 0000000..380eeb4 --- /dev/null +++ b/tests/lost_cities_jax/test_ppo_stack.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import jax +import jax.numpy as jnp +import numpy as np + +from lost_cities_jax import legal_action_mask, reset +from lost_cities_jax.opponents import ( + discard_only_action, + heuristic_balanced_action, + heuristic_cautious_action, +) +from lost_cities_jax.ppo import ( + JaxPPOConfig, + NetworkConfig, + OpponentConfig, + PPOHyperConfig, + RunConfig, + create_train_state, + evaluate, + load_config, + make_train_iteration, + policy_by_name, + random_rollout, + train, +) + + +def tiny_config(tmp_path) -> JaxPPOConfig: + return JaxPPOConfig( + run=RunConfig( + experiment_name="pytest-jax-ppo", + seed=123, + total_updates=1, + checkpoint_every=1, + artifact_root=str(tmp_path), + ), + opponent=OpponentConfig(name="discard_only"), + network=NetworkConfig(hidden_size=32, num_layers=1), + ppo=PPOHyperConfig(batch_games=8, rollout_steps=16, epochs=1, minibatches=2), + ) + + +def test_static_opponents_return_legal_actions(): + state = reset(jax.random.PRNGKey(0)) + mask = np.asarray(legal_action_mask(state), dtype=bool) + for fn in [discard_only_action, heuristic_balanced_action, heuristic_cautious_action]: + action = int(fn(state, jnp.int32(0), jax.random.PRNGKey(1))) + assert mask[action] + + +def test_random_rollout_smoke(tmp_path): + row = random_rollout(tiny_config(tmp_path)) + assert row["env_steps"] == 8 * 16 + assert 0.0 <= row["play_action_rate"] <= 1.0 + assert row["game_length_mean"] > 0.0 + + +def test_train_checkpoint_and_eval_smoke(tmp_path): + cfg = tiny_config(tmp_path) + run_dir = train(cfg) + assert (run_dir / "latest").exists() + result = evaluate(cfg, run_dir / "latest", games=8, duplicate=True) + assert result["games"] == 16 + assert 0.0 <= result["win_rate"] <= 1.0 + + +def test_one_jitted_train_iteration_shapes(tmp_path): + cfg = tiny_config(tmp_path) + train_state = create_train_state(cfg, jax.random.PRNGKey(0)) + env_state = jax.jit(jax.vmap(reset))(jax.random.split(jax.random.PRNGKey(1), 8)) + train_iteration = make_train_iteration(cfg, policy_by_name("discard_only")) + + train_state, env_state, rng, metrics = train_iteration( + train_state, env_state, jax.random.PRNGKey(2), jnp.asarray(1.0) + ) + + assert env_state.to_move.shape == (8,) + assert "loss" in metrics + assert jnp.isfinite(metrics["loss"]) + + +def test_load_config_file(): + cfg = load_config("configs/jax_ppo/smoke.yaml") + assert cfg.opponent.name == "discard_only" + assert cfg.ppo.batch_games == 64 diff --git a/uv.lock b/uv.lock index 602f7cd..3af1da5 100644 --- a/uv.lock +++ b/uv.lock @@ -16,6 +16,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/0a/a10b45aab35b175aded078a462dc8d0c698f5b13946e7cb0869097b78bb6/absl_py-2.5.0-py3-none-any.whl", hash = "sha256:0f17b89f2a4eaaedc4f28c622998aa690564b3012a396a4ffad0821007fe03ba", size = 137410, upload-time = "2026-07-03T10:57:46.735Z" }, ] +[[package]] +name = "aiofiles" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -258,9 +267,12 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "chex" }, + { name = "flax" }, { name = "jax" }, { name = "matplotlib" }, { name = "numpy" }, + { name = "optax" }, + { name = "orbax-checkpoint" }, { name = "pydantic" }, { name = "pyyaml" }, { name = "torch" }, @@ -286,9 +298,12 @@ dev = [ [package.metadata] requires-dist = [ { name = "chex", specifier = ">=0.1.92" }, + { name = "flax", specifier = ">=0.12.7" }, { name = "jax", specifier = ">=0.10.2" }, { name = "matplotlib", specifier = ">=3.8" }, { name = "numpy", specifier = ">=1.26.0" }, + { name = "optax", specifier = ">=0.2.8" }, + { name = "orbax-checkpoint", specifier = ">=0.12.1" }, { name = "pydantic", specifier = ">=2.7" }, { name = "pygame", marker = "extra == 'gui'", specifier = ">=2.6.1" }, { name = "pygame-gui", marker = "extra == 'gui'", specifier = ">=0.6.14" }, @@ -429,6 +444,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, ] +[[package]] +name = "etils" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/ce/6e067242fde898841922ac6fc82b0bb2fe35c38e995880bdffdfbe30182a/etils-1.14.0.tar.gz", hash = "sha256:8136e7f4c4173cd0af0ca5481c4475152f0b8686192951eefa60ee8711e1ede4", size = 108127, upload-time = "2026-03-04T17:41:36.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/3d/589663aeeacd59bb2f3e8596bfd3e81cf0fb18d70bb433199041f469771b/etils-1.14.0-py3-none-any.whl", hash = "sha256:b5df7341f54dbe1405a4450b2741207b4a8c279780402b45f87202b94dfc52b4", size = 172934, upload-time = "2026-03-04T17:41:35.01Z" }, +] + +[package.optional-dependencies] +epath = [ + { name = "fsspec" }, + { name = "typing-extensions" }, + { name = "zipp" }, +] +epy = [ + { name = "typing-extensions" }, +] + [[package]] name = "filelock" version = "3.29.0" @@ -438,6 +472,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] +[[package]] +name = "flax" +version = "0.12.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jax" }, + { name = "msgpack" }, + { name = "numpy" }, + { name = "optax" }, + { name = "orbax-checkpoint" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "tensorstore" }, + { name = "treescope" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/19/4a8e128e2296eede00dc7a8586d374ef0b2770146a5cda1f50aa86f001ee/flax-0.12.7.tar.gz", hash = "sha256:abfd6acb17d6b93d1d7d7dfae7d3856222b92b35d35ab2487b77639c31dc673a", size = 5476434, upload-time = "2026-04-22T06:07:12.017Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/65/4bd2abfd4cb6e917b2626de5cbfc034dfc94b74dd95b8272d93f2ad66bed/flax-0.12.7-py3-none-any.whl", hash = "sha256:79d590793fa3a282ac36b4464f2ea9d1e69fe1d026c4618451b01731e8086e32", size = 525130, upload-time = "2026-04-22T06:07:10.254Z" }, +] + [[package]] name = "fonttools" version = "4.62.1" @@ -520,6 +575,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/7a/1c6e3562dfd8950adbb11ffbc65d21e7c89d01a6e4f137fa981056de25c5/gitpython-3.1.50-py3-none-any.whl", hash = "sha256:d352abe2908d07355014abdd21ddf798c2a961469239afec4962e9da884858f9", size = 212507, upload-time = "2026-05-06T04:01:23.799Z" }, ] +[[package]] +name = "humanize" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/ea/13a1ef3c12d12662905801495283530251918b70d62d368f1d2e0272c70d/humanize-4.16.0.tar.gz", hash = "sha256:7dc2244a2f84a4bfb1d36c37bac80cd78e35cdc5c119206d87b018e1445f3a3f", size = 89515, upload-time = "2026-06-30T16:17:29.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/aa/0b7365d30fed43e7a3449aba1fe20a0a7174d9cf13e282af4e69ac825441/humanize-4.16.0-py3-none-any.whl", hash = "sha256:353eb2f34c09d098b2880eee8bef21832eae6d174f48c5762fff7e5fcb74d01d", size = 137209, upload-time = "2026-06-30T16:17:28.36Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -717,6 +781,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -855,6 +931,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6f/87/afead29192170917537934c6aff4b008c805fff7b1ccea0c79120d96beda/matplotlib-3.10.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3fc0364dfbe1d07f6d15c5ebd0c5bf89e126916e5a8667dd4a7a6e84c36653d4", size = 8774002, upload-time = "2026-04-24T00:14:09.816Z" }, ] +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + [[package]] name = "ml-dtypes" version = "0.5.4" @@ -905,6 +990,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] +[[package]] +name = "msgpack" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/6b/e9b1cdc042c4458801d2545ed782a95f3d6ba8e270cce8745b8603c7f748/msgpack-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:29a3f6e9667868429d8240dfd063ea5ffdc1321c13d783aa23827a38de0dcb22", size = 82812, upload-time = "2026-06-18T16:12:45.022Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3a/dd518a1bf78ed1e9ad8afe57307c079a00eafe4b3068932a27ca1ea56b4f/msgpack-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aded5bdf32609dc7987a49bbbd15a8ef096193f96dd8bbeb791de729e650acf5", size = 82739, upload-time = "2026-06-18T16:12:46.025Z" }, + { url = "https://files.pythonhosted.org/packages/70/e0/7ba9e1542bf0771a27b8b37c1316e3f95ae9d748fd765284655c476ad4ef/msgpack-1.2.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:146ee4e9ce80b365c6d4c47073da9da7bcec473e58194ceee5dd7620ace77e06", size = 414233, upload-time = "2026-06-18T16:12:47.029Z" }, + { url = "https://files.pythonhosted.org/packages/03/8d/671d81534ea0e2b0e8a121be100020da09eb78861fe3aa8f3ef7dcd3bed1/msgpack-1.2.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a28d076ca7c82b9c8728ad90b7147489449557038bed50e4241eb832395169b4", size = 423843, upload-time = "2026-06-18T16:12:48.19Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b6/e5c737515ed1f166664b87601b532f58cbb73d8aa6a90b99f7c2c5037e8e/msgpack-1.2.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7d31c0ac0c640f877804c67cb2bc9f4e23dc2db97e96c2e67fa27d38283b41f8", size = 390772, upload-time = "2026-06-18T16:12:49.624Z" }, + { url = "https://files.pythonhosted.org/packages/a8/46/62ed8c2e87d7021eab19921594d961ef3aa3794eec76c716dc30f3bfd433/msgpack-1.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ff92d7feeaf5bc26c51495b69e2f99ed97ab79346fb6555f44be7dd2ac6503b", size = 409559, upload-time = "2026-06-18T16:12:50.936Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/59aa3887b860bbf43532835e192b1c388a17590d6068ae4f8b2bc74c906e/msgpack-1.2.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:779197a6513bab3c3632265e3d0f7cb3227e62510841a6f34f1eaa37efbb345e", size = 387838, upload-time = "2026-06-18T16:12:52.161Z" }, + { url = "https://files.pythonhosted.org/packages/09/11/f8563e471093420cf6478cb3271a0175d8402b82d879783d4035d2d03360/msgpack-1.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67f6dd22fa72a93752643f07889796d62739a13415ee630169a8ce764f86cf9f", size = 421732, upload-time = "2026-06-18T16:12:53.556Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/e673683c4c6c90c1022b24c65af4b03eda72b182a1176ef6449069d66acc/msgpack-1.2.1-cp311-cp311-win32.whl", hash = "sha256:91054a783328e0ea7954b8771095705c8d2243b814743fbaadf14552c9c52c5d", size = 64091, upload-time = "2026-06-18T16:12:54.821Z" }, + { url = "https://files.pythonhosted.org/packages/3f/07/ca212739d179f9083bff2c7c08c24101c3555a334fadc2b876b18768a3ae/msgpack-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2eda0b7ebb1283a98d3e4492ac933c8af6aff59fd3df1c3ed024f536af4b1dc8", size = 70462, upload-time = "2026-06-18T16:12:55.898Z" }, + { url = "https://files.pythonhosted.org/packages/6d/be/6798347b425e26f35db82e69dd83c09716c856a3714e7bffc4c0860fd830/msgpack-1.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ee967f7c7e1df2890c671ff2ee51a28ded0efc95da3e507176dee881ce36c66", size = 65059, upload-time = "2026-06-18T16:12:57.053Z" }, + { url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" }, + { url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" }, + { url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" }, + { url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" }, + { url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" }, + { url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" }, + { url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" }, + { url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" }, + { url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" }, + { url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" }, + { url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" }, + { url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" }, + { url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" }, + { url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" }, + { url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" }, + { url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" }, + { url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" }, + { url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" }, + { url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" }, + { url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" }, + { url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" }, + { url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" }, + { url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" }, + { url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" }, + { url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" }, + { url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" }, + { url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + [[package]] name = "networkx" version = "3.6.1" @@ -1160,6 +1317,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, ] +[[package]] +name = "optax" +version = "0.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "jax" }, + { name = "jaxlib" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/f9/e3d11ae6f298ee941a0690e353a323d158ba5dedc436e75621c310845c5c/optax-0.2.8.tar.gz", hash = "sha256:5b225b35066fc3eebaa4d798f1b4173b4d57d1a480610908981f8343b50af0b0", size = 301193, upload-time = "2026-03-20T23:30:05.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/69/6a93d8600c339d7687a05857c7907bd4dd8cf88691a5ea106d7a50af90a1/optax-0.2.8-py3-none-any.whl", hash = "sha256:e3ca2d36c99daab1800ae9dbc0545034382d6bc780b24d969e1b0df65fa31cb4", size = 402960, upload-time = "2026-03-20T23:30:03.886Z" }, +] + +[[package]] +name = "orbax-checkpoint" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "aiofiles" }, + { name = "etils", extra = ["epath", "epy"] }, + { name = "humanize" }, + { name = "jax" }, + { name = "msgpack" }, + { name = "nest-asyncio", marker = "sys_platform == 'win32'" }, + { name = "numpy" }, + { name = "prometheus-client" }, + { name = "protobuf" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "simplejson" }, + { name = "tensorstore" }, + { name = "typing-extensions" }, + { name = "uvloop", marker = "sys_platform != 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/70/e8b4c7f382c77d198128c50076611b9b3b7b18e795f6d7d5931755af0106/orbax_checkpoint-0.12.1.tar.gz", hash = "sha256:e9b014ccad4c9b2648cdb9946cd2550339bc3eb388ac1f494a27fda4739b4e19", size = 703150, upload-time = "2026-06-24T19:13:15.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/87/0ae1baf8271a7a68bcc5b91103892b201cd723ac3dd67bc6ef3e5fc4747e/orbax_checkpoint-0.12.1-py3-none-any.whl", hash = "sha256:e123b4d6e5d9f9144bb9b5a1d88b1eddc81c71f8a12b07b995f61e0d3022b0f1", size = 1311120, upload-time = "2026-06-24T19:13:14.262Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -1290,6 +1489,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/fb/d9aa83ffe43ce1f19e557c0971d04b90561b0cfd50762aafb01968285553/prometheus_client-0.25.0.tar.gz", hash = "sha256:5e373b75c31afb3c86f1a52fa1ad470c9aace18082d39ec0d2f918d11cc9ba28", size = 86035, upload-time = "2026-04-09T19:53:42.359Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9b/d4b1e644385499c8346fa9b622a3f030dce14cd6ef8a1871c221a17a67e7/prometheus_client-0.25.0-py3-none-any.whl", hash = "sha256:d5aec89e349a6ec230805d0df882f3807f74fd6c1a2fa86864e3c2279059fed1", size = 64154, upload-time = "2026-04-09T19:53:41.324Z" }, +] + [[package]] name = "protobuf" version = "7.34.1" @@ -1305,6 +1513,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pydantic" version = "2.13.4" @@ -1641,6 +1877,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + [[package]] name = "ruff" version = "0.15.12" @@ -1817,6 +2066,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] +[[package]] +name = "simplejson" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/2a/54837395a3487c725669428d513293612a48d82b95a0642c936932e5d898/simplejson-4.1.1.tar.gz", hash = "sha256:c08eb9f7a90f77ae470e19a07472e9a79ebc0d1c2315d86a72767665bd5ba79f", size = 118860, upload-time = "2026-04-24T19:24:59.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/25/39013ffe279d90093ec1c848565b3683c586906c10fa55d9000ec29d046b/simplejson-4.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2867c64d92abd1992c15666fae198203093f593e43d6b81adf176bae530d493a", size = 111538, upload-time = "2026-04-24T19:22:49.051Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ae/2c272971c8a87e2539c54a98eb6ff037bee1e2e93943c3986cf7500a4f3a/simplejson-4.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c47c46e16c8ea9e4850061e6ed5aa2b9cd2074cb2274bfd9c138cba15ce7453", size = 90594, upload-time = "2026-04-24T19:22:50.408Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a2/6eebfb99dedc139f549200f61ade6d1890ac5707c5d427bdfa6fe39c9313/simplejson-4.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e294e33dbf316a9bbdd4030d46503c9b0f19470ae7ad6af5bae6c426bc2e869f", size = 90718, upload-time = "2026-04-24T19:22:51.694Z" }, + { url = "https://files.pythonhosted.org/packages/80/7e/c9e6c0c4ad8415e64dad0c47f619b556b02680a41631b4dbc281d55dc54d/simplejson-4.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7ce252b28fddbdd83db5bd7d93dad2a8a591d7ada098afec9c1b23d6b722a7a4", size = 180901, upload-time = "2026-04-24T19:22:53.025Z" }, + { url = "https://files.pythonhosted.org/packages/34/09/69e331e3994b1ed9be6ce9ace4ade704e7ed503edf869929ca7bb404eda8/simplejson-4.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c44ef6b02a4eb67ed17a72342341792149b3ff46f15426c26e970e49addf327", size = 178133, upload-time = "2026-04-24T19:22:54.574Z" }, + { url = "https://files.pythonhosted.org/packages/5d/40/ed806f24afef295c1032448f5ff6f6f2979392d5645ddb9f4fed7f38194d/simplejson-4.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82bfca2b85a34178c25829c703f0a9e9f113a5af7539285bd3efb583a0bf1ba3", size = 188155, upload-time = "2026-04-24T19:22:56.044Z" }, + { url = "https://files.pythonhosted.org/packages/38/94/8d6f515b827b0f7881a49c8c1ac6920b7ae9428939ef04238c973278b42a/simplejson-4.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0e4b23f71dd781f8830f1663dc01a4944d3dbf87a1f93d78fba1cf64722d0ccf", size = 176225, upload-time = "2026-04-24T19:22:57.981Z" }, + { url = "https://files.pythonhosted.org/packages/c9/fd/6dffb4956563d48bbe46b91ff341adae34920e94008fd6b8d728072abfc7/simplejson-4.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:82fee635d7b73ad801030b05a75fbd34a098da0c2ecf600667a03636d09e1e42", size = 185535, upload-time = "2026-04-24T19:22:59.618Z" }, + { url = "https://files.pythonhosted.org/packages/de/d2/a509ee37763e79aec75d68f8521db1440306edeba3b8b4064ab4ee8bf1d9/simplejson-4.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:68e62eda21192c5ea9bb92d571ca46a4477fef48762f50d433de2b4253051551", size = 179302, upload-time = "2026-04-24T19:23:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/d8/23/5b343bfd2a79d3b6818e4db3586c405a001a090d4c89d336e31273ce7177/simplejson-4.1.1-cp311-cp311-win32.whl", hash = "sha256:ffd3d82294b47f5ec64050021ace95fd62628a0c1cc8bbf4d06d2d1fb697e055", size = 88408, upload-time = "2026-04-24T19:23:02.808Z" }, + { url = "https://files.pythonhosted.org/packages/38/04/df9b37aedbd524dca20840d25ebe01d6ae486b89792aeff5d15b9c4114f7/simplejson-4.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:78a3fe0995be42bed62a26aa78e0e0b4d87c6545785346b9cc898f3389569a35", size = 90526, upload-time = "2026-04-24T19:23:04.408Z" }, + { url = "https://files.pythonhosted.org/packages/60/25/e90998fe8e480eb43b966c09e835379887d427567ebd496563d3b1e16b19/simplejson-4.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:19040a17154dc03d289bab68d73ce0a6a0be01de30c584bbdd93490bead14b22", size = 112414, upload-time = "2026-04-24T19:23:06.084Z" }, + { url = "https://files.pythonhosted.org/packages/9c/a0/abd4785f36c3400f1fbb21f517be39295a750a714f04b7ee175adf6ef580/simplejson-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a94ebaecdbaa80d9551a3ec6bf0c9302fc8b53ab6c1b2bfd498a1df4cb28158d", size = 91120, upload-time = "2026-04-24T19:23:07.877Z" }, + { url = "https://files.pythonhosted.org/packages/b8/78/fc060d2e3b13c6ec59288574b8efac64075e316b2afba4396a56b2422f78/simplejson-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:67341c95c0a168ab4a6d1e807e50463f1c8da932c3286d81e201266c427061fa", size = 91055, upload-time = "2026-04-24T19:23:09.264Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b6/156a8de1e1b47694f0e7de6675866936608d45dc68388fd017d36f8693be/simplejson-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:45ec18e337fec538b7e902d489505c450b2454653d1290f3f50385e6fd8aa607", size = 190297, upload-time = "2026-04-24T19:23:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/86/1c/e4d0eab695be3eb21d0f46bce820752031f03e7113f9c80a9b3c73ee7157/simplejson-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:820c69a4710400e9b248d5670647d60be58824369282d3925e516b3ff1a7cd82", size = 187002, upload-time = "2026-04-24T19:23:12.982Z" }, + { url = "https://files.pythonhosted.org/packages/76/0e/7f5a59d29426b062d5928fb88b403c3f797129d53be7102f955dbe51aa44/simplejson-4.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e708d373a10e4378ef2d59f8361850c7150fd907ed49efe49bc5492160476d1", size = 195146, upload-time = "2026-04-24T19:23:14.517Z" }, + { url = "https://files.pythonhosted.org/packages/78/18/9943db224dd4d5fa3c090c3e56a94c37b254338c83995ec5680285111c40/simplejson-4.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:980fc33353f81fd12d8c49d44f8c2760d1dc8192285e627c5180d141035b228a", size = 183931, upload-time = "2026-04-24T19:23:16.742Z" }, + { url = "https://files.pythonhosted.org/packages/c2/08/9a690da9a766161c06c627d805362cf159f1abe480969372b2897649b955/simplejson-4.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:de2ed102fff88dacf543699f53ee3a533cc11539a39baa176b7e09dd783069d6", size = 192228, upload-time = "2026-04-24T19:23:18.33Z" }, + { url = "https://files.pythonhosted.org/packages/05/88/bd8aad36b451ffb0e0a3f721d695a88befa6d1ac7d1e02ae788ca7ff4029/simplejson-4.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785ff8edc0e28bf773a32543a6bbed46351453c997b3f6709c744e3c2f7eabb", size = 187808, upload-time = "2026-04-24T19:23:21.165Z" }, + { url = "https://files.pythonhosted.org/packages/04/ee/14f91db0d1f481533b651dafbf8cd0da088d9817f7af30c68f7f19f9c847/simplejson-4.1.1-cp312-cp312-win32.whl", hash = "sha256:2e0d5ead6d14610467ec356ec1f6b5d8a56aa216abaad8d41c8b873b16cf313f", size = 88512, upload-time = "2026-04-24T19:23:22.764Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c4/90de06b2d8737c68c05ff9274113f854dbf6a5f28b7a955212111672cb57/simplejson-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:63a5451f557d6be48a231bae932458655c620902b868170b2f1c8afed496f6b4", size = 90748, upload-time = "2026-04-24T19:23:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/37/a9/47b445eeb559c9593453a0648e0fd6d08e8adff64dd5e5ced66726da8a09/simplejson-4.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dff52fc7af272e84fc21cc5a06c927c823ca6ae00af14f3b0d7707b42775ed98", size = 113160, upload-time = "2026-04-24T19:23:26.033Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/cb72db31523c164dea5dc55b02dad065a40c478856bc7534b279d2b51906/simplejson-4.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:971aed0647ad6e840a3943bec812fcda5f2d26a5497a4981d1fb49aa4f9a396c", size = 91521, upload-time = "2026-04-24T19:23:27.572Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e5/54cb7c50ad5fdc1e0a86b7df4b135c2cbd5c4623605aa94466659098e8da/simplejson-4.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:249e2e220aa6d9b9d936bde84eb7bf79d5b6c5a8273c6e411f8b1635a9073f2d", size = 91407, upload-time = "2026-04-24T19:23:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/21a3ede87f0bf82d6c7bcb90480d50a6490eb974c6ab20881188e440957c/simplejson-4.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e5cdd6a5d52299f345c15ab5678cc4249e24f383f361d986afbc3c7072a6b6b", size = 192451, upload-time = "2026-04-24T19:23:30.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/df/9903edd3102bf0b5984edfcb90c88612330996efa3b4fbf8a971d6e17839/simplejson-4.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642cec364e0676e2d5a73fa4d31d0c7c55886997caa2fde24e8292ca44d32728", size = 189015, upload-time = "2026-04-24T19:23:32.647Z" }, + { url = "https://files.pythonhosted.org/packages/98/cd/33230927a780e1398b857e3944abb914556994d252b1d765ae40d112cb25/simplejson-4.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:76fe296ca1df23d290033f10aaacf534fd1b3e3007e7f9ff8aa68b21413aaa78", size = 196658, upload-time = "2026-04-24T19:23:34.563Z" }, + { url = "https://files.pythonhosted.org/packages/cd/84/2c5a7444eb53e9a86d3738299bffddd9f53aeed799ded2f45368221fdb19/simplejson-4.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f0ad25b7dc4e0fb23858355819f2e994f1a5badcdcde8737eac7921c2f1ed2a", size = 185967, upload-time = "2026-04-24T19:23:36.191Z" }, + { url = "https://files.pythonhosted.org/packages/d3/68/454378e06d059cd412a7ed5d87fb6d29fd5b60f13a4d89fc1f764ff434df/simplejson-4.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a59ebd0533f03fd06ff0c42ba0f02d93cbcdd7944922bf3b93911327a95b901f", size = 193940, upload-time = "2026-04-24T19:23:38.151Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d5/a15bf915f623a2c5a079d6e3be8256fdb8ef06f110669493a09b9d6933e0/simplejson-4.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bccbf4419676b517939852e5aeff2af6aee4dc046881c67a1581fa6f1cb01abd", size = 189795, upload-time = "2026-04-24T19:23:40.139Z" }, + { url = "https://files.pythonhosted.org/packages/d2/c9/37212ae7dc4b607f0978c408e8633f05c810884e054c33113184c6c2c8a2/simplejson-4.1.1-cp313-cp313-win32.whl", hash = "sha256:6c845363eb5fd166fb7c72243da38f4fcfde666ede7fdf2cc6fd7762894626f7", size = 88773, upload-time = "2026-04-24T19:23:41.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c7a0a47883a9015b54c9d8a4b62f2aba17bd4335b1787b9b8a0fc2fa6d52/simplejson-4.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:104d8324c34f25b4b90800bc5fa363780cbc3d8496aef061cba7ce1af9162270", size = 90888, upload-time = "2026-04-24T19:23:43.11Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/4a118a6a92eb33bb08c8e2fe7ec85cb96f0673491bb2b829930831ee4fbe/simplejson-4.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:ed7473602b6625de793b6acba49aa949f144a475f538792067e4cf2fda2071f5", size = 110492, upload-time = "2026-04-24T19:23:44.957Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/84d160e9fa8cada1e0a9381cae4fa81eecd573577a5b34366d8ced59bdf7/simplejson-4.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:225c9caa324c5b554d009fb9cac22aee7711e71bd96f487938c659af467e828e", size = 90152, upload-time = "2026-04-24T19:23:46.355Z" }, + { url = "https://files.pythonhosted.org/packages/68/31/9a5432c433a7671107182cdc9a20ea78a70f99c4e5334aa54b6d4d0d79ed/simplejson-4.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:95407269340c7f22f09776ea7b717a52cf56cfcf119b5e45f66faa4a26445bea", size = 90115, upload-time = "2026-04-24T19:23:47.743Z" }, + { url = "https://files.pythonhosted.org/packages/78/91/3635cdb13318cb0a328abaa69e2b91251caad39d6779aa308098f341f6cb/simplejson-4.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3851658d642c1184d2023f0e6c9ce44a21eb1629e74e7c84ef956b128841fe12", size = 184036, upload-time = "2026-04-24T19:23:49.472Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/149b6ec5393f6849d98c59cadba888b710a8ef4b805ab91e11a566960d40/simplejson-4.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95a3bb0f78e85f4937f99092239f2011ce06f0f2d803df5c299cc05abbeae008", size = 180543, upload-time = "2026-04-24T19:23:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/df/7c/a5d968d0b527a748b667e62bea94309ccbcb1e2b108e8f0cf8547efaa12b/simplejson-4.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bbfdaa7c0603f75b7b14b211b7f2be44696d4e26833ad2d91d5c87bf5fb9a920", size = 188725, upload-time = "2026-04-24T19:23:52.995Z" }, + { url = "https://files.pythonhosted.org/packages/db/e3/6a8d11181d587ef00e2db9112357e6832111e56dd56b01b5c11758a1965d/simplejson-4.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:39e3c584071dced8c21b4689f0254303521daeb9b5bc1f4289755d71fa3cb0d3", size = 177492, upload-time = "2026-04-24T19:23:54.581Z" }, + { url = "https://files.pythonhosted.org/packages/67/e3/8b0eb8b06e8198cfbd1270487da163d0093df05cc4f557350cd65e2f7e79/simplejson-4.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:036a27bd0469b9d79557cbddb392969f876cd7f278cfbd0fba81534927a06575", size = 185281, upload-time = "2026-04-24T19:23:56.13Z" }, + { url = "https://files.pythonhosted.org/packages/dc/5f/64990f07ec9e2cb1a814c674e2e21b5693207f74ac70eb72151b847ea4e6/simplejson-4.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b70bfd2f67f3351baba08aa3ae9233c83f21fd95ae5e6b3d0ecb8c647929112f", size = 181848, upload-time = "2026-04-24T19:23:57.92Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/bbc1bc0447f339f79f99ab8c37f7f037cb2f1f93af75d6a4d553096bb0c3/simplejson-4.1.1-cp314-cp314-win32.whl", hash = "sha256:37233c72ce88d06acb92747347742b3c07871eba6789f060c179c9302dde8efe", size = 88761, upload-time = "2026-04-24T19:23:59.397Z" }, + { url = "https://files.pythonhosted.org/packages/18/72/ec1b5cbdcb140c132e6c7bdf99bd73e4f675439e77126c88f472fcffa09c/simplejson-4.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:cc0442dea71cd9cbf30a0b8b9929ab5aa6c02c0443a3d977351e6ec5bada4388", size = 91018, upload-time = "2026-04-24T19:24:00.85Z" }, + { url = "https://files.pythonhosted.org/packages/3d/97/4fa437f68ff72219bac3bf3d050de9c6265691f3a170e16954bd69d7cddd/simplejson-4.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c996a4d38290c515af347740659ce095b425449c164a5c9fa3977caa6eff5dbe", size = 113919, upload-time = "2026-04-24T19:24:02.287Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/59de041d09eb4a9577f7015d7263c32095dfb7fde49717dff62145d89809/simplejson-4.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c65c763fb20d7ca113c1c14dce2fc04a0fc3a57aceff533d6fdac707c7bffb40", size = 91904, upload-time = "2026-04-24T19:24:03.812Z" }, + { url = "https://files.pythonhosted.org/packages/03/8e/46bb345d540f6eb31427d984a4e518cdb182d0621814fee4fee045e8815b/simplejson-4.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0da5c9f57206ee7ef280ff7f1d924937b0a64f9a271a5ef371a2ecdbebba7421", size = 91752, upload-time = "2026-04-24T19:24:05.622Z" }, + { url = "https://files.pythonhosted.org/packages/83/e2/1b2ce97f068835eb3d253c116a4df7a3f436b7bf2fb5ff1ba29287e8b0ec/simplejson-4.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ea3426e786425d10e9e82f8a6eda74a7d6eb10d99165ac3d0d3bbcb65c0ea343", size = 214021, upload-time = "2026-04-24T19:24:07.447Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/d93e556df6a0786298644a7c08304fcbeddc248325f23f38acbebeb21165/simplejson-4.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d75cea7a1025edd7e439b2966b3d977c45b5b899e2adaf422811b3ac702ed9fb", size = 213530, upload-time = "2026-04-24T19:24:09.289Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/c93bf305b9f00d7259e09e713d60e75bd0f7f53da970f716ab90491770e7/simplejson-4.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63c2ada8e58f266491f19eed2eeeb7c25c6141e52f8f9e820f6bb94156cf8dbc", size = 218282, upload-time = "2026-04-24T19:24:10.991Z" }, + { url = "https://files.pythonhosted.org/packages/0c/20/a9b5d2e27ec44b069ee251bd55544fc76929a067107b1050001566ba86f3/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1fffb56305c5b475ee746cf9e04f97423ba5aaacd292dc1255bd75b1d3b124b", size = 209249, upload-time = "2026-04-24T19:24:12.662Z" }, + { url = "https://files.pythonhosted.org/packages/97/e4/e06ee682ed5df67592181f5ecb062e35878967e27f5b6e087237d4548d95/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a6525ec733f43d0541206cffa64fd2aad5a7ae3eb76566aff49cd4db6382209a", size = 213963, upload-time = "2026-04-24T19:24:14.302Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9f/1e160e4cd8cdbf062bf6a454cdf814dc7a48eb47e566fdb8f80ccb202605/simplejson-4.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:861e393260508efa64d8805a8e49c416c3484907e3f146ce966c69552b49b9a3", size = 210474, upload-time = "2026-04-24T19:24:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e6/cecd913df322df5bbe7ebb8ba39e0708e505a165553900da8a7761026d6f/simplejson-4.1.1-cp314-cp314t-win32.whl", hash = "sha256:d083b89d30948a751d3d97476c2ed91e4caaa24a1a1459bdbadb8876242c71fe", size = 91134, upload-time = "2026-04-24T19:24:17.635Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/f540dde99cc1d393bd062ab3b5735b777561a5d8f8a5f2e241164444d77a/simplejson-4.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4cbb299d0528ec0447fe366d8c9641860e28f997a62730690fef905f1f41046e", size = 94467, upload-time = "2026-04-24T19:24:19.109Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6a/8b74c52ffd33dbbde00fe7251fee6a0acdc8cea33f7a43805aed258fb79b/simplejson-4.1.1-py3-none-any.whl", hash = "sha256:2ce92b3748f02423e26d2bfb636fb9d7a8f67c8f5854dcae69d350d123b2eee2", size = 69195, upload-time = "2026-04-24T19:24:57.962Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1847,6 +2160,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "tensorstore" +version = "0.1.84" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/18/c8e8b4faffab1a434b6c013d54cf7f5b754a6849429d9dbb718297705796/tensorstore-0.1.84.tar.gz", hash = "sha256:3cb091dfde68600e6d8f03a389ccc92ffa7c0798a0c600d1013c0138d7163e6b", size = 7208048, upload-time = "2026-05-16T06:17:58.448Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/7630705a1db3a349f162552c7a34f4e74c06bf9800d73c233eb939305f6e/tensorstore-0.1.84-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:6103311700359e9f552e8560d16eecf1548b87e2f8a6a766b9f6a65c8722f879", size = 16556656, upload-time = "2026-05-16T06:17:01.065Z" }, + { url = "https://files.pythonhosted.org/packages/09/2b/fbba516c549fc868d9b9c74e3e593157825b08db0103dcfcde1ae994d1f4/tensorstore-0.1.84-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:340fe971f1808d7060f2273b8eee352780bcb65e5035f78163fa9b8930aa795a", size = 14900174, upload-time = "2026-05-16T06:17:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/55/6d/fe5013aa1b8e8e6a73213285f29a059bfcbefcfebafc937694823bdc5a00/tensorstore-0.1.84-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34352f8ba6e5efba5feb18917d68da1a92bbff80e64c3fd06d3d1af4b343821a", size = 19341729, upload-time = "2026-05-16T06:17:06.242Z" }, + { url = "https://files.pythonhosted.org/packages/11/c5/65e7dfc4108451f5317aca47a7c339954d0b7601ed4db4481a2f80ee2da1/tensorstore-0.1.84-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ae74451ab5cc8a0cd9e51926adf04f4dfdbd8ad5ac31ac71f1caa7bdfb2828d", size = 20952887, upload-time = "2026-05-16T06:17:08.653Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/11a487107c5b73684aeaa4667693d5e73b96bc3ad7211db4a03f6049bf65/tensorstore-0.1.84-cp311-cp311-win_amd64.whl", hash = "sha256:4c9b084b0b44c36eaa9b9c51c3347474cd3c94fda12344fb507e6629357615bf", size = 13391569, upload-time = "2026-05-16T06:17:11.307Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/1b5231e965257c3ee7d4615cb49a0fac53a71a1c34b293bcf524bb7c6d13/tensorstore-0.1.84-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:915371fc2c27540e8b69c573b7a06217fb8d161ec231cedfa9f3d264615a326d", size = 16571584, upload-time = "2026-05-16T06:17:13.283Z" }, + { url = "https://files.pythonhosted.org/packages/88/5d/52e52aa00a5ae3ebe1116ca52ac9f47ef98e94f6c4e411649cd3d1bb79cc/tensorstore-0.1.84-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4477eabe26e2f5131f1b1a3444cd9167fe69fabc29579eab8259d218399b9e6b", size = 14905169, upload-time = "2026-05-16T06:17:15.638Z" }, + { url = "https://files.pythonhosted.org/packages/61/36/f88b4bf267902f12cd2ca33aff10fabd6839dd1ce7d51876ebefa98aaf2c/tensorstore-0.1.84-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ace00cf2e45dc5d64fe3a10c2cbef61343915683808a10a3e081233566a7231", size = 19345134, upload-time = "2026-05-16T06:17:17.984Z" }, + { url = "https://files.pythonhosted.org/packages/18/7c/b7b24e10e5cb0213c85204d53fcd60d0568d986ea0001a00a815e14e01e1/tensorstore-0.1.84-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64c8039558d5607b73903948fce058725731df410c5c196cf58b3fc6222395b5", size = 20968745, upload-time = "2026-05-16T06:17:20.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/33ad454a2b667a93b35e74595a351dbf9b8693440bd68665990663b79164/tensorstore-0.1.84-cp312-cp312-win_amd64.whl", hash = "sha256:08e7ec5b35db5d4c4b6a867be8500448f9bd4e0c9d5a52d7f0b460650622baf6", size = 13398458, upload-time = "2026-05-16T06:17:22.701Z" }, + { url = "https://files.pythonhosted.org/packages/e7/27/3c637c0f987866f6fb92cf96ee4d40eee4b5ab699135803ada851f2a56ad/tensorstore-0.1.84-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:9337d96693b4a0e555fbe63bb228e3e2e681a80e4d371f351fb67810f197f74e", size = 16571472, upload-time = "2026-05-16T06:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/41/83/4f3c6ef9bed01f384036c2030b3901cf075bbc8eff6e4529e502f0283ab5/tensorstore-0.1.84-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:028455cccdc05c31f194048cf459a26669b26d38f0516caf9213e7219b1ee79a", size = 14905288, upload-time = "2026-05-16T06:17:26.753Z" }, + { url = "https://files.pythonhosted.org/packages/4d/28/03e46405ba7c616e7a1ec5425a8f4a1b3f4d6ec2be359cb2f248199849e4/tensorstore-0.1.84-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50afb06c57a509091015af6a85da6f483a7f5ad0372284dd95d5513d877336e4", size = 19344890, upload-time = "2026-05-16T06:17:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/82669e70cef67c803852285ba6f59d7e3d102983c0ab4be8269c14756677/tensorstore-0.1.84-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ea53a851ea86aad3d99c14a790c85468d6324be14c7ac211f1f0265e8fab707", size = 20968230, upload-time = "2026-05-16T06:17:31.374Z" }, + { url = "https://files.pythonhosted.org/packages/a5/12/97d8ad183e3130e168f2feb860edd68f1b72e57f29268d980f3b70e34cd0/tensorstore-0.1.84-cp313-cp313-win_amd64.whl", hash = "sha256:fe9bf1c7fef69884a91222179550f9b5ba6c1454f9534429221824d9b15c00ec", size = 13398858, upload-time = "2026-05-16T06:17:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/39/bd/fda828e7915ddb61704a6f4568b5b7ce9fe607c33b7535cf51b4fd900b38/tensorstore-0.1.84-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97e767a64415297f019dd1e4b9af6a23d72e126e2a2d05cf41083253abe81428", size = 16576451, upload-time = "2026-05-16T06:17:35.993Z" }, + { url = "https://files.pythonhosted.org/packages/8a/99/2e72fcb19404de43f9412880c542a8ef8651bd30183c85454d6ca14ebe56/tensorstore-0.1.84-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7d01775985fcaa2b0f100349766a953c5086e92e746bf395e936151d4d8f9ac", size = 14907896, upload-time = "2026-05-16T06:17:38.458Z" }, + { url = "https://files.pythonhosted.org/packages/25/d4/9a3964cdfdc5a15df6d0485694de9684c13990a5b0f5d88bfa365e0a2936/tensorstore-0.1.84-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2ff5d5536a8a9b1596c51b9075cc9d40b4c4ea4e6cc03c0480111dbe5d956d", size = 19347021, upload-time = "2026-05-16T06:17:41.316Z" }, + { url = "https://files.pythonhosted.org/packages/43/19/70532cb2bf2f6fc3bf252f850bfb528b26eeb9c30c3cafffb075cbb7c77a/tensorstore-0.1.84-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c9108ae6c29adc90b72ca267ba2b577386c5e410ea2f8e87eabce5ebdad327e", size = 20970345, upload-time = "2026-05-16T06:17:43.874Z" }, + { url = "https://files.pythonhosted.org/packages/4e/22/a523e7576c83a6c35bd1415e5f4530b0f1e448c099d7e22684f55792755c/tensorstore-0.1.84-cp314-cp314-win_amd64.whl", hash = "sha256:4096220f4b9a2411c3751597dd8ced2f671d7a217575613c915191e19d5ea150", size = 13787429, upload-time = "2026-05-16T06:17:46.135Z" }, + { url = "https://files.pythonhosted.org/packages/2b/23/40b0a0a91973431770ac09a8b69039bd7a051169f6707b63a1c51ad36782/tensorstore-0.1.84-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:70284a7225d94adad5989de7f9238638ec9fec2bdd8c0fdb86d567f16d59e615", size = 16652804, upload-time = "2026-05-16T06:17:49.226Z" }, + { url = "https://files.pythonhosted.org/packages/01/99/2ea3b37864adb2e9f6e724c6959ab2b7f56aa4dad01964d4b32adf211e68/tensorstore-0.1.84-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98e10cae5b3fc0828967b8ddf36242b3b26ac8bc79880bc3e36346063259212a", size = 14990904, upload-time = "2026-05-16T06:17:51.464Z" }, + { url = "https://files.pythonhosted.org/packages/ff/26/fe72887d7b91e832bef3033d244c4c548993d1b6fb19177dff3895659c12/tensorstore-0.1.84-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98edbc57d453ba8ebcf0d19bc28e4de6de95922bee2eca0805955b0833b8ce26", size = 19365698, upload-time = "2026-05-16T06:17:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/37/74/35a1d41343f86f6e2ef135e81f6b8107b9f16c777a3e8be9e3fbce541d18/tensorstore-0.1.84-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bd20de85c1b83dd3ca94db24e7bd449bdb055590a1b162b05691f6b81fff00f", size = 20986107, upload-time = "2026-05-16T06:17:56.571Z" }, +] + [[package]] name = "toolz" version = "1.1.0" @@ -1903,6 +2252,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, ] +[[package]] +name = "treescope" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/2a/d13d3c38862632742d2fe2f7ae307c431db06538fd05ca03020d207b5dcc/treescope-0.1.10.tar.gz", hash = "sha256:20f74656f34ab2d8716715013e8163a0da79bdc2554c16d5023172c50d27ea95", size = 138870, upload-time = "2025-08-08T05:43:48.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/2b/36e984399089c026a6499ac8f7401d38487cf0183839a4aa78140d373771/treescope-0.1.10-py3-none-any.whl", hash = "sha256:dde52f5314f4c29d22157a6fe4d3bd103f9cae02791c9e672eefa32c9aa1da51", size = 182255, upload-time = "2025-08-08T05:43:46.673Z" }, +] + [[package]] name = "triton" version = "3.6.0" @@ -1952,6 +2313,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" }, + { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" }, + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + [[package]] name = "virtualenv" version = "21.3.1" @@ -1995,3 +2394,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/9b/487413eaccefdb58799a226726e24b486e9192d2671c75a4550c160aba23/wandb-0.26.1-py3-none-win_amd64.whl", hash = "sha256:5817785467d3f1676f1812ec19a89f77f6e56dfe67d9f47080075af95f705d3e", size = 24968155, upload-time = "2026-04-23T16:27:25.731Z" }, { url = "https://files.pythonhosted.org/packages/04/dc/5baf3e99b3eeb709d6f75124b5bec8cb73d4b38d2b10df7fdcfde4966200/wandb-0.26.1-py3-none-win_arm64.whl", hash = "sha256:f848b7744f896bc04cabbb28360a2814d1551a91fa2c456243e06435729c8a2e", size = 22912416, upload-time = "2026-04-23T16:27:29.456Z" }, ] + +[[package]] +name = "zipp" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/d8/eab98a517c14134c0b2eb4e2387bc5f457334293ec5d2dd3857ec2966802/zipp-4.1.0.tar.gz", hash = "sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602", size = 26214, upload-time = "2026-05-18T20:08:57.967Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/13/547360d81e6d88d58492968ffda9f9542854f11310ee556fef14260cc886/zipp-4.1.0-py3-none-any.whl", hash = "sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f", size = 10238, upload-time = "2026-05-18T20:08:57.045Z" }, +]