Deep CFR YAML config 추가
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
run:
|
||||
iterations: 1
|
||||
seed: 1
|
||||
device: cpu
|
||||
|
||||
network:
|
||||
hidden_size: 16
|
||||
|
||||
traversal:
|
||||
traversals_per_iteration: 1
|
||||
max_depth: 2
|
||||
max_nodes: 64
|
||||
num_workers: 0
|
||||
worker_chunk_size: 1
|
||||
|
||||
optimization:
|
||||
advantage_train_steps: 1
|
||||
strategy_train_steps: 1
|
||||
batch_size: 2
|
||||
learning_rate: 0.001
|
||||
|
||||
memory:
|
||||
advantage_capacity: 1000
|
||||
strategy_capacity: 1000
|
||||
|
||||
checkpoint:
|
||||
directory: runs/deep_cfr/smoke
|
||||
save_every_iteration: false
|
||||
|
||||
evaluation:
|
||||
eval_every: 0
|
||||
games: 2
|
||||
opponents:
|
||||
- random
|
||||
max_steps: 10000
|
||||
@@ -97,8 +97,8 @@ extras.
|
||||
|
||||
Still smaller than legacy:
|
||||
|
||||
1. Config is a single `DeepCFRConfig` dataclass rather than a deeply nested
|
||||
YAML-first config tree.
|
||||
1. Config is YAML-first and nested through Pydantic, but only one smoke preset
|
||||
exists under `configs/deep_cfr/`.
|
||||
2. Multiprocessing exists, but it is intentionally simple:
|
||||
- no progress callback per worker batch
|
||||
- no hotspot timing profile
|
||||
|
||||
@@ -9,6 +9,8 @@ authors = [
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"numpy>=1.26.0",
|
||||
"pydantic>=2.7",
|
||||
"PyYAML>=6.0",
|
||||
"torch>=2.3.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from dataclasses import replace
|
||||
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
|
||||
@@ -13,14 +12,19 @@ def benchmark_traversal(
|
||||
*,
|
||||
num_workers: int = 0,
|
||||
) -> dict[str, float | int]:
|
||||
base = config or DeepCFRConfig(
|
||||
iterations=1,
|
||||
traversals_per_iteration=8,
|
||||
max_traversal_depth=4,
|
||||
save_every_iteration=False,
|
||||
base = config or DeepCFRConfig.model_validate(
|
||||
{
|
||||
"run": {"iterations": 1},
|
||||
"traversal": {"traversals_per_iteration": 8, "max_depth": 4},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
)
|
||||
cfg = replace(base, num_workers=num_workers, save_every_iteration=False, eval_every=0)
|
||||
trainer = DeepCFRTrainer(cfg, classic_config(seed=cfg.seed))
|
||||
data = base.model_dump(mode="python")
|
||||
data["traversal"]["num_workers"] = num_workers
|
||||
data["checkpoint"]["save_every_iteration"] = False
|
||||
data["evaluation"]["eval_every"] = 0
|
||||
cfg = DeepCFRConfig.model_validate(data)
|
||||
trainer = DeepCFRTrainer(cfg, classic_config(seed=cfg.run.seed))
|
||||
started = time.perf_counter()
|
||||
metrics = trainer.run_iteration(1)
|
||||
elapsed = time.perf_counter() - started
|
||||
|
||||
@@ -2,14 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.benchmark import (
|
||||
benchmark_traversal,
|
||||
benchmark_traversal_modes,
|
||||
)
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig, config_from_dict
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig, load_config
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.evaluate import (
|
||||
evaluate_strategy_network,
|
||||
load_strategy_policy_from_checkpoint,
|
||||
@@ -27,27 +26,48 @@ from coolrl_lost_cities.games.classic.game import classic_config
|
||||
def _load_config(path: str | None) -> DeepCFRConfig:
|
||||
if path is None:
|
||||
return DeepCFRConfig()
|
||||
return config_from_dict(json.loads(Path(path).read_text(encoding="utf-8")))
|
||||
return load_config(path)
|
||||
|
||||
|
||||
def _deep_update(base: dict[str, Any], patch: dict[str, Any]) -> None:
|
||||
for key, value in patch.items():
|
||||
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
||||
_deep_update(base[key], value)
|
||||
else:
|
||||
base[key] = value
|
||||
|
||||
|
||||
def _with_overrides(config: DeepCFRConfig, overrides: dict[str, Any]) -> DeepCFRConfig:
|
||||
data = config.model_dump(mode="python")
|
||||
_deep_update(data, overrides)
|
||||
return DeepCFRConfig.model_validate(data)
|
||||
|
||||
|
||||
def train_command(args: argparse.Namespace) -> None:
|
||||
config = _load_config(args.config)
|
||||
overrides = {}
|
||||
for key in (
|
||||
"iterations",
|
||||
"traversals_per_iteration",
|
||||
"checkpoint_dir",
|
||||
"eval_every",
|
||||
"eval_games",
|
||||
"seed",
|
||||
):
|
||||
value = getattr(args, key)
|
||||
if value is not None:
|
||||
overrides[key] = value
|
||||
overrides: dict[str, Any] = {}
|
||||
if args.iterations is not None:
|
||||
overrides.setdefault("run", {})["iterations"] = args.iterations
|
||||
if args.seed is not None:
|
||||
overrides.setdefault("run", {})["seed"] = args.seed
|
||||
if args.traversals_per_iteration is not None:
|
||||
overrides.setdefault("traversal", {})["traversals_per_iteration"] = (
|
||||
args.traversals_per_iteration
|
||||
)
|
||||
if args.checkpoint_dir is not None:
|
||||
overrides.setdefault("checkpoint", {})["directory"] = args.checkpoint_dir
|
||||
if args.eval_every is not None:
|
||||
overrides.setdefault("evaluation", {})["eval_every"] = args.eval_every
|
||||
if args.eval_games is not None:
|
||||
overrides.setdefault("evaluation", {})["games"] = args.eval_games
|
||||
if args.no_save:
|
||||
overrides["save_every_iteration"] = False
|
||||
config = replace(config, **overrides)
|
||||
trainer = DeepCFRTrainer(config, classic_config(seed=config.seed), device=args.device)
|
||||
overrides.setdefault("checkpoint", {})["save_every_iteration"] = False
|
||||
config = _with_overrides(config, overrides)
|
||||
trainer = DeepCFRTrainer(
|
||||
config,
|
||||
classic_config(seed=config.run.seed),
|
||||
device=args.device or config.run.device,
|
||||
)
|
||||
if args.resume:
|
||||
trainer.load_checkpoint(args.resume)
|
||||
metrics = trainer.train()
|
||||
@@ -70,11 +90,15 @@ def eval_command(args: argparse.Namespace) -> None:
|
||||
|
||||
|
||||
def benchmark_command(args: argparse.Namespace) -> None:
|
||||
config = DeepCFRConfig(
|
||||
traversals_per_iteration=args.traversals,
|
||||
max_traversal_depth=args.depth,
|
||||
seed=args.seed,
|
||||
save_every_iteration=False,
|
||||
config = DeepCFRConfig.model_validate(
|
||||
{
|
||||
"run": {"seed": args.seed},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": args.traversals,
|
||||
"max_depth": args.depth,
|
||||
},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
)
|
||||
if args.compare:
|
||||
print(json.dumps(benchmark_traversal_modes(config), indent=2, sort_keys=True))
|
||||
@@ -135,7 +159,7 @@ def main(argv: list[str] | None = None) -> None:
|
||||
train.add_argument("--traversals-per-iteration", type=int)
|
||||
train.add_argument("--checkpoint-dir")
|
||||
train.add_argument("--resume")
|
||||
train.add_argument("--device", default="cpu")
|
||||
train.add_argument("--device")
|
||||
train.add_argument("--eval-every", type=int)
|
||||
train.add_argument("--eval-games", type=int)
|
||||
train.add_argument("--seed", type=int)
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DeepCFRConfig:
|
||||
|
||||
class StrictModel(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class RunConfig(StrictModel):
|
||||
iterations: int = 1
|
||||
seed: int = 1
|
||||
device: str = "cpu"
|
||||
|
||||
|
||||
class NetworkConfig(StrictModel):
|
||||
hidden_size: int = 64
|
||||
|
||||
|
||||
class TraversalConfig(StrictModel):
|
||||
traversals_per_iteration: int = 2
|
||||
max_traversal_depth: int | None = 8
|
||||
max_nodes_per_traversal: int | None = 10_000
|
||||
max_depth: int | None = 8
|
||||
max_nodes: int | None = 10_000
|
||||
regret_matching_epsilon: float = 1.0e-8
|
||||
outcome_sampling_epsilon: float = 0.0
|
||||
outcome_sampling_value_clip: float | None = None
|
||||
@@ -20,59 +37,116 @@ class DeepCFRConfig:
|
||||
cutoff_rollout_policy: str = "random"
|
||||
cutoff_rollout_max_steps: int = 10_000
|
||||
opponent_policy: str = "network"
|
||||
self_play_snapshot_every: int = 1
|
||||
self_play_max_snapshots: int = 20
|
||||
self_play_anchor_probability: float = 0.0
|
||||
self_play_current_weight: float = 0.5
|
||||
self_play_recent_weight: float = 0.3
|
||||
self_play_older_weight: float = 0.2
|
||||
self_play_anchor_weight: float = 0.0
|
||||
self_play_recent_window: int = 5
|
||||
strategy_sample_interval: int = 1
|
||||
store_strategy_on_traverser_nodes: bool = True
|
||||
store_strategy_on_opponent_nodes: bool = True
|
||||
advantage_memory_capacity: int = 2_000_000
|
||||
strategy_memory_capacity: int = 2_000_000
|
||||
advantage_train_steps: int = 1
|
||||
strategy_train_steps: int = 1
|
||||
batch_size: int = 32
|
||||
hidden_size: int = 64
|
||||
learning_rate: float = 1.0e-3
|
||||
seed: int = 1
|
||||
checkpoint_dir: str = "runs/deep_cfr/default"
|
||||
save_every_iteration: bool = True
|
||||
eval_every: int = 0
|
||||
eval_games: int = 10
|
||||
eval_opponents: tuple[str, ...] = ("random",)
|
||||
eval_max_steps: int = 10_000
|
||||
num_workers: int | str = 0
|
||||
traversal_worker_chunk_size: int = 4
|
||||
worker_chunk_size: int = 4
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
@field_validator("outcome_unsampled_regret")
|
||||
@classmethod
|
||||
def _validate_unsampled_regret(cls, value: str) -> str:
|
||||
if value not in {"negative_node_value", "zero"}:
|
||||
raise ValueError("must be 'negative_node_value' or 'zero'")
|
||||
return value
|
||||
|
||||
@property
|
||||
def checkpoint_path(self) -> Path:
|
||||
return Path(self.checkpoint_dir)
|
||||
@field_validator("cutoff_value_mode")
|
||||
@classmethod
|
||||
def _validate_cutoff_value_mode(cls, value: str) -> str:
|
||||
if value not in {"score_diff", "random_rollout"}:
|
||||
raise ValueError("must be 'score_diff' or 'random_rollout'")
|
||||
return value
|
||||
|
||||
@field_validator("cutoff_rollout_policy")
|
||||
@classmethod
|
||||
def _validate_cutoff_rollout_policy(cls, value: str) -> str:
|
||||
if value not in {"random", "safe_heuristic"}:
|
||||
raise ValueError("must be 'random' or 'safe_heuristic'")
|
||||
return value
|
||||
|
||||
@field_validator("opponent_policy")
|
||||
@classmethod
|
||||
def _validate_opponent_policy(cls, value: str) -> str:
|
||||
if value not in {"network", "safe_heuristic", "self_play_league"}:
|
||||
raise ValueError("must be 'network', 'safe_heuristic', or 'self_play_league'")
|
||||
return value
|
||||
|
||||
def resolved_num_workers(self, batches: int | None = None) -> int:
|
||||
if isinstance(self.num_workers, str):
|
||||
token = self.num_workers.strip().lower()
|
||||
if token == "auto":
|
||||
guess = max(1, (os_cpu_count() or 2) // 2)
|
||||
guess = max(1, (os.cpu_count() or 2) // 2)
|
||||
return min(guess, batches) if batches is not None and batches > 0 else guess
|
||||
return max(0, int(token))
|
||||
return max(0, int(self.num_workers))
|
||||
|
||||
|
||||
def config_from_dict(data: dict[str, Any]) -> DeepCFRConfig:
|
||||
values = dict(data)
|
||||
if "eval_opponents" in values:
|
||||
values["eval_opponents"] = tuple(values["eval_opponents"])
|
||||
return DeepCFRConfig(**values)
|
||||
class SelfPlayLeagueConfig(StrictModel):
|
||||
snapshot_every: int = 1
|
||||
max_snapshots: int = 20
|
||||
anchor_probability: float = 0.0
|
||||
current_weight: float = 0.5
|
||||
recent_weight: float = 0.3
|
||||
older_weight: float = 0.2
|
||||
anchor_weight: float = 0.0
|
||||
recent_window: int = 5
|
||||
|
||||
|
||||
def os_cpu_count() -> int | None:
|
||||
import os
|
||||
class OptimizationConfig(StrictModel):
|
||||
advantage_train_steps: int = 1
|
||||
strategy_train_steps: int = 1
|
||||
batch_size: int = 32
|
||||
learning_rate: float = 1.0e-3
|
||||
|
||||
return os.cpu_count()
|
||||
|
||||
class MemoryConfig(StrictModel):
|
||||
advantage_capacity: int = 2_000_000
|
||||
strategy_capacity: int = 2_000_000
|
||||
|
||||
|
||||
class CheckpointConfig(StrictModel):
|
||||
directory: str = "runs/deep_cfr/default"
|
||||
save_every_iteration: bool = True
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return Path(self.directory)
|
||||
|
||||
|
||||
class EvaluationConfig(StrictModel):
|
||||
eval_every: int = 0
|
||||
games: int = 10
|
||||
opponents: tuple[str, ...] = ("random",)
|
||||
max_steps: int = 10_000
|
||||
|
||||
|
||||
class DeepCFRConfig(StrictModel):
|
||||
run: RunConfig = Field(default_factory=RunConfig)
|
||||
network: NetworkConfig = Field(default_factory=NetworkConfig)
|
||||
traversal: TraversalConfig = Field(default_factory=TraversalConfig)
|
||||
self_play: SelfPlayLeagueConfig = Field(default_factory=SelfPlayLeagueConfig)
|
||||
optimization: OptimizationConfig = Field(default_factory=OptimizationConfig)
|
||||
memory: MemoryConfig = Field(default_factory=MemoryConfig)
|
||||
checkpoint: CheckpointConfig = Field(default_factory=CheckpointConfig)
|
||||
evaluation: EvaluationConfig = Field(default_factory=EvaluationConfig)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return self.model_dump(mode="json")
|
||||
|
||||
@property
|
||||
def checkpoint_path(self) -> Path:
|
||||
return self.checkpoint.path
|
||||
|
||||
|
||||
def config_from_dict(data: Mapping[str, Any]) -> DeepCFRConfig:
|
||||
return DeepCFRConfig.model_validate(data)
|
||||
|
||||
|
||||
def load_config(path: str | Path) -> DeepCFRConfig:
|
||||
config_path = Path(path)
|
||||
text = config_path.read_text(encoding="utf-8")
|
||||
if config_path.suffix.lower() in {".yaml", ".yml"}:
|
||||
data = yaml.safe_load(text) or {}
|
||||
else:
|
||||
data = json.loads(text)
|
||||
return config_from_dict(data)
|
||||
|
||||
@@ -97,7 +97,7 @@ def load_strategy_policy_from_checkpoint(
|
||||
network = DeepCFRMLP(
|
||||
int(payload["input_dim"]),
|
||||
int(payload["action_size"]),
|
||||
cfg.hidden_size,
|
||||
cfg.network.hidden_size,
|
||||
).to(device)
|
||||
network.load_state_dict(payload["strategy_network"])
|
||||
network.eval()
|
||||
|
||||
@@ -68,31 +68,33 @@ class DeepCFRTrainer:
|
||||
device: str = "cpu",
|
||||
) -> None:
|
||||
self.config = config or DeepCFRConfig()
|
||||
self.game_config = game_config or LostCitiesConfig(seed=self.config.seed)
|
||||
self.game_config = game_config or LostCitiesConfig(seed=self.config.run.seed)
|
||||
self.device = torch.device(device)
|
||||
|
||||
probe = GameState.new_game(self.game_config, seed=self.config.seed)
|
||||
probe = GameState.new_game(self.game_config, seed=self.config.run.seed)
|
||||
self.input_dim = input_dim(probe)
|
||||
self.action_size = 2 * probe.config.hand_size + 1 + probe.config.n_colors
|
||||
|
||||
torch.manual_seed(self.config.seed)
|
||||
torch.manual_seed(self.config.run.seed)
|
||||
self.advantage_networks = [
|
||||
DeepCFRMLP(self.input_dim, self.action_size, self.config.hidden_size).to(self.device)
|
||||
DeepCFRMLP(self.input_dim, self.action_size, self.config.network.hidden_size).to(
|
||||
self.device
|
||||
)
|
||||
for _ in range(2)
|
||||
]
|
||||
self.strategy_network = DeepCFRMLP(
|
||||
self.input_dim, self.action_size, self.config.hidden_size
|
||||
self.input_dim, self.action_size, self.config.network.hidden_size
|
||||
).to(self.device)
|
||||
self.advantage_optimizers = [
|
||||
torch.optim.Adam(network.parameters(), lr=self.config.learning_rate)
|
||||
torch.optim.Adam(network.parameters(), lr=self.config.optimization.learning_rate)
|
||||
for network in self.advantage_networks
|
||||
]
|
||||
self.strategy_optimizer = torch.optim.Adam(
|
||||
self.strategy_network.parameters(), lr=self.config.learning_rate
|
||||
self.strategy_network.parameters(), lr=self.config.optimization.learning_rate
|
||||
)
|
||||
self.advantage_memory = ReservoirMemory(self.config.advantage_memory_capacity)
|
||||
self.strategy_memory = ReservoirMemory(self.config.strategy_memory_capacity)
|
||||
self.rng = np.random.default_rng(self.config.seed + 101)
|
||||
self.advantage_memory = ReservoirMemory(self.config.memory.advantage_capacity)
|
||||
self.strategy_memory = ReservoirMemory(self.config.memory.strategy_capacity)
|
||||
self.rng = np.random.default_rng(self.config.run.seed + 101)
|
||||
self.iteration = 0
|
||||
self.run_dir = self.config.checkpoint_path
|
||||
self.metrics_path = self.run_dir / "metrics.jsonl"
|
||||
@@ -138,7 +140,7 @@ class DeepCFRTrainer:
|
||||
|
||||
def run_iteration(self, iteration: int) -> IterationMetrics:
|
||||
self.iteration = iteration
|
||||
if self.config.resolved_num_workers() > 1:
|
||||
if self.config.traversal.resolved_num_workers() > 1:
|
||||
total_stats = self._run_traversals_parallel(iteration)
|
||||
else:
|
||||
total_stats = self._run_traversals_single_process(iteration)
|
||||
@@ -168,34 +170,34 @@ class DeepCFRTrainer:
|
||||
self.strategy_memory,
|
||||
device=self.device,
|
||||
action_size=self.action_size,
|
||||
epsilon=self.config.regret_matching_epsilon,
|
||||
strategy_sample_interval=self.config.strategy_sample_interval,
|
||||
store_strategy_on_traverser_nodes=self.config.store_strategy_on_traverser_nodes,
|
||||
store_strategy_on_opponent_nodes=self.config.store_strategy_on_opponent_nodes,
|
||||
max_depth=self.config.max_traversal_depth,
|
||||
max_nodes=self.config.max_nodes_per_traversal,
|
||||
outcome_sampling_epsilon=self.config.outcome_sampling_epsilon,
|
||||
outcome_sampling_value_clip=self.config.outcome_sampling_value_clip,
|
||||
outcome_unsampled_regret=self.config.outcome_unsampled_regret,
|
||||
cutoff_value_mode=self.config.cutoff_value_mode,
|
||||
cutoff_rollouts=self.config.cutoff_rollouts,
|
||||
cutoff_rollout_policy=self.config.cutoff_rollout_policy,
|
||||
cutoff_rollout_max_steps=self.config.cutoff_rollout_max_steps,
|
||||
opponent_policy=self.config.opponent_policy,
|
||||
epsilon=self.config.traversal.regret_matching_epsilon,
|
||||
strategy_sample_interval=self.config.traversal.strategy_sample_interval,
|
||||
store_strategy_on_traverser_nodes=self.config.traversal.store_strategy_on_traverser_nodes,
|
||||
store_strategy_on_opponent_nodes=self.config.traversal.store_strategy_on_opponent_nodes,
|
||||
max_depth=self.config.traversal.max_depth,
|
||||
max_nodes=self.config.traversal.max_nodes,
|
||||
outcome_sampling_epsilon=self.config.traversal.outcome_sampling_epsilon,
|
||||
outcome_sampling_value_clip=self.config.traversal.outcome_sampling_value_clip,
|
||||
outcome_unsampled_regret=self.config.traversal.outcome_unsampled_regret,
|
||||
cutoff_value_mode=self.config.traversal.cutoff_value_mode,
|
||||
cutoff_rollouts=self.config.traversal.cutoff_rollouts,
|
||||
cutoff_rollout_policy=self.config.traversal.cutoff_rollout_policy,
|
||||
cutoff_rollout_max_steps=self.config.traversal.cutoff_rollout_max_steps,
|
||||
opponent_policy=self.config.traversal.opponent_policy,
|
||||
league_advantage_networks=self._materialize_league_networks(),
|
||||
self_play_anchor_probability=self.config.self_play_anchor_probability,
|
||||
self_play_current_weight=self.config.self_play_current_weight,
|
||||
self_play_recent_weight=self.config.self_play_recent_weight,
|
||||
self_play_older_weight=self.config.self_play_older_weight,
|
||||
self_play_anchor_weight=self.config.self_play_anchor_weight,
|
||||
self_play_recent_window=self.config.self_play_recent_window,
|
||||
self_play_anchor_probability=self.config.self_play.anchor_probability,
|
||||
self_play_current_weight=self.config.self_play.current_weight,
|
||||
self_play_recent_weight=self.config.self_play.recent_weight,
|
||||
self_play_older_weight=self.config.self_play.older_weight,
|
||||
self_play_anchor_weight=self.config.self_play.anchor_weight,
|
||||
self_play_recent_window=self.config.self_play.recent_window,
|
||||
rng=self.rng,
|
||||
)
|
||||
for network in self.advantage_networks:
|
||||
network.eval()
|
||||
for traversal_index in range(self.config.traversals_per_iteration):
|
||||
for traversal_index in range(self.config.traversal.traversals_per_iteration):
|
||||
for player in range(2):
|
||||
seed = self.config.seed + iteration * 10_000 + traversal_index * 10 + player
|
||||
seed = self.config.run.seed + iteration * 10_000 + traversal_index * 10 + player
|
||||
state = GameState.new_game(self.game_config, seed=seed)
|
||||
_, stats = traverser.traverse(state, player, iteration)
|
||||
total_stats.accumulate(stats)
|
||||
@@ -206,7 +208,7 @@ class DeepCFRTrainer:
|
||||
total_stats = TraversalStats()
|
||||
if not batches:
|
||||
return total_stats
|
||||
max_workers = self.config.resolved_num_workers(len(batches))
|
||||
max_workers = self.config.traversal.resolved_num_workers(len(batches))
|
||||
with ProcessPoolExecutor(
|
||||
max_workers=max_workers,
|
||||
mp_context=mp.get_context("spawn"),
|
||||
@@ -225,12 +227,12 @@ class DeepCFRTrainer:
|
||||
{name: value.detach().cpu() for name, value in network.state_dict().items()}
|
||||
for network in self.advantage_networks
|
||||
]
|
||||
chunk_size = max(1, self.config.traversal_worker_chunk_size)
|
||||
chunk_size = max(1, self.config.traversal.worker_chunk_size)
|
||||
batch_index = 0
|
||||
for player in range(2):
|
||||
seeds = [
|
||||
self.config.seed + iteration * 10_000 + index * 10 + player
|
||||
for index in range(self.config.traversals_per_iteration)
|
||||
self.config.run.seed + iteration * 10_000 + index * 10 + player
|
||||
for index in range(self.config.traversal.traversals_per_iteration)
|
||||
]
|
||||
for start in range(0, len(seeds), chunk_size):
|
||||
chunk = seeds[start : start + chunk_size]
|
||||
@@ -245,7 +247,7 @@ class DeepCFRTrainer:
|
||||
action_size=self.action_size,
|
||||
advantage_networks=network_payloads,
|
||||
league_advantage_networks=self._league_payloads(),
|
||||
worker_seed=self.config.seed + iteration * 1_000_003 + batch_index,
|
||||
worker_seed=self.config.run.seed + iteration * 1_000_003 + batch_index,
|
||||
)
|
||||
)
|
||||
batch_index += 1
|
||||
@@ -258,14 +260,14 @@ class DeepCFRTrainer:
|
||||
]
|
||||
|
||||
def _maybe_record_self_play_snapshot(self, iteration: int) -> None:
|
||||
if self.config.opponent_policy != "self_play_league":
|
||||
if self.config.traversal.opponent_policy != "self_play_league":
|
||||
return
|
||||
if self.config.self_play_max_snapshots <= 0:
|
||||
if self.config.self_play.max_snapshots <= 0:
|
||||
return
|
||||
if iteration % max(1, self.config.self_play_snapshot_every) != 0:
|
||||
if iteration % max(1, self.config.self_play.snapshot_every) != 0:
|
||||
return
|
||||
self.self_play_league_snapshots.append(self._frozen_advantage_state_dicts())
|
||||
overflow = len(self.self_play_league_snapshots) - self.config.self_play_max_snapshots
|
||||
overflow = len(self.self_play_league_snapshots) - self.config.self_play.max_snapshots
|
||||
if overflow > 0:
|
||||
del self.self_play_league_snapshots[:overflow]
|
||||
|
||||
@@ -273,7 +275,7 @@ class DeepCFRTrainer:
|
||||
league: list[list[nn.Module]] = []
|
||||
for snapshot in self.self_play_league_snapshots:
|
||||
networks = [
|
||||
DeepCFRMLP(self.input_dim, self.action_size, self.config.hidden_size).to(
|
||||
DeepCFRMLP(self.input_dim, self.action_size, self.config.network.hidden_size).to(
|
||||
self.device
|
||||
)
|
||||
for _ in range(2)
|
||||
@@ -291,7 +293,7 @@ class DeepCFRTrainer:
|
||||
self._start_run_logging()
|
||||
metrics: list[IterationMetrics] = []
|
||||
start = self.iteration + 1
|
||||
stop = self.iteration + self.config.iterations
|
||||
stop = self.iteration + self.config.run.iterations
|
||||
for iteration in range(start, stop + 1):
|
||||
started = time.perf_counter()
|
||||
item = self.run_iteration(iteration)
|
||||
@@ -299,7 +301,7 @@ class DeepCFRTrainer:
|
||||
metrics.append(item)
|
||||
self._append_metrics(item, elapsed)
|
||||
self._maybe_record_self_play_snapshot(iteration)
|
||||
if self.config.save_every_iteration:
|
||||
if self.config.checkpoint.save_every_iteration:
|
||||
checkpoint_dir = self.run_dir
|
||||
self.save_checkpoint(checkpoint_dir / f"iteration_{iteration:05d}.pt", item)
|
||||
self.save_checkpoint(checkpoint_dir / "latest.pt", item)
|
||||
@@ -316,7 +318,9 @@ class DeepCFRTrainer:
|
||||
if self.iteration == 0 and self.metrics_path.exists():
|
||||
self.metrics_path.unlink()
|
||||
with self.log_path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(f"Deep CFR run start iteration={self.iteration} seed={self.config.seed}\n")
|
||||
handle.write(
|
||||
f"Deep CFR run start iteration={self.iteration} seed={self.config.run.seed}\n"
|
||||
)
|
||||
|
||||
def _append_metrics(self, metrics: IterationMetrics, iteration_seconds: float) -> None:
|
||||
data = metrics.to_dict()
|
||||
@@ -332,18 +336,21 @@ class DeepCFRTrainer:
|
||||
)
|
||||
|
||||
def _evaluate(self, iteration: int) -> dict[str, float | int]:
|
||||
if self.config.eval_every <= 0 or iteration % self.config.eval_every != 0:
|
||||
if (
|
||||
self.config.evaluation.eval_every <= 0
|
||||
or iteration % self.config.evaluation.eval_every != 0
|
||||
):
|
||||
return {}
|
||||
results: dict[str, float | int] = {}
|
||||
for opponent in self.config.eval_opponents:
|
||||
for opponent in self.config.evaluation.opponents:
|
||||
result = evaluate_strategy_network(
|
||||
self.strategy_network,
|
||||
self.game_config,
|
||||
games=self.config.eval_games,
|
||||
seed=self.config.seed + iteration * 1000,
|
||||
games=self.config.evaluation.games,
|
||||
seed=self.config.run.seed + iteration * 1000,
|
||||
opponent=opponent,
|
||||
device=self.device,
|
||||
max_steps=self.config.eval_max_steps,
|
||||
max_steps=self.config.evaluation.max_steps,
|
||||
)
|
||||
for key, value in result.items():
|
||||
results[f"eval_{opponent}_{key}"] = value
|
||||
@@ -365,7 +372,7 @@ class DeepCFRTrainer:
|
||||
return self._train_strategy(self.strategy_network, self.strategy_optimizer, samples)
|
||||
|
||||
def _batch(self, samples: list[TrainingSample], step: int) -> list[TrainingSample]:
|
||||
batch_size = min(self.config.batch_size, len(samples))
|
||||
batch_size = min(self.config.optimization.batch_size, len(samples))
|
||||
offset = (step * batch_size) % len(samples)
|
||||
batch = samples[offset : offset + batch_size]
|
||||
if len(batch) < batch_size:
|
||||
@@ -401,9 +408,11 @@ class DeepCFRTrainer:
|
||||
) -> float:
|
||||
last_loss = 0.0
|
||||
network.train()
|
||||
for _step in range(max(self.config.advantage_train_steps, 0)):
|
||||
for _step in range(max(self.config.optimization.advantage_train_steps, 0)):
|
||||
x, y, legal = self._batch_tensors(
|
||||
self.advantage_memory.sample(self.config.batch_size, self.rng, player=player)
|
||||
self.advantage_memory.sample(
|
||||
self.config.optimization.batch_size, self.rng, player=player
|
||||
)
|
||||
)
|
||||
pred = network(x)
|
||||
diff = (pred - y).masked_fill(~legal, 0.0)
|
||||
@@ -422,9 +431,9 @@ class DeepCFRTrainer:
|
||||
) -> float:
|
||||
last_loss = 0.0
|
||||
network.train()
|
||||
for _step in range(max(self.config.strategy_train_steps, 0)):
|
||||
for _step in range(max(self.config.optimization.strategy_train_steps, 0)):
|
||||
x, y, legal = self._batch_tensors(
|
||||
self.strategy_memory.sample(self.config.batch_size, self.rng)
|
||||
self.strategy_memory.sample(self.config.optimization.batch_size, self.rng)
|
||||
)
|
||||
logits = network(x).masked_fill(~legal, torch.finfo(torch.float32).min)
|
||||
log_probs = nn.functional.log_softmax(logits, dim=-1).masked_fill(~legal, 0.0)
|
||||
|
||||
@@ -40,7 +40,8 @@ def run_traversal_worker_batch(batch: TraversalWorkerBatch) -> TraversalWorkerRe
|
||||
cfg = config_from_dict(batch.config)
|
||||
device = torch.device("cpu")
|
||||
networks = [
|
||||
DeepCFRMLP(batch.input_dim, batch.action_size, cfg.hidden_size).to(device) for _ in range(2)
|
||||
DeepCFRMLP(batch.input_dim, batch.action_size, cfg.network.hidden_size).to(device)
|
||||
for _ in range(2)
|
||||
]
|
||||
for network, state_dict in zip(networks, batch.advantage_networks, strict=True):
|
||||
network.load_state_dict(state_dict)
|
||||
@@ -48,7 +49,7 @@ def run_traversal_worker_batch(batch: TraversalWorkerBatch) -> TraversalWorkerRe
|
||||
league_networks: list[list[torch.nn.Module]] = []
|
||||
for snapshot in batch.league_advantage_networks:
|
||||
snapshot_networks = [
|
||||
DeepCFRMLP(batch.input_dim, batch.action_size, cfg.hidden_size).to(device)
|
||||
DeepCFRMLP(batch.input_dim, batch.action_size, cfg.network.hidden_size).to(device)
|
||||
for _ in range(2)
|
||||
]
|
||||
for network, state_dict in zip(snapshot_networks, snapshot, strict=True):
|
||||
@@ -63,27 +64,27 @@ def run_traversal_worker_batch(batch: TraversalWorkerBatch) -> TraversalWorkerRe
|
||||
strategy_memory,
|
||||
device=device,
|
||||
action_size=batch.action_size,
|
||||
epsilon=cfg.regret_matching_epsilon,
|
||||
strategy_sample_interval=cfg.strategy_sample_interval,
|
||||
store_strategy_on_traverser_nodes=cfg.store_strategy_on_traverser_nodes,
|
||||
store_strategy_on_opponent_nodes=cfg.store_strategy_on_opponent_nodes,
|
||||
max_depth=cfg.max_traversal_depth,
|
||||
max_nodes=cfg.max_nodes_per_traversal,
|
||||
outcome_sampling_epsilon=cfg.outcome_sampling_epsilon,
|
||||
outcome_sampling_value_clip=cfg.outcome_sampling_value_clip,
|
||||
outcome_unsampled_regret=cfg.outcome_unsampled_regret,
|
||||
cutoff_value_mode=cfg.cutoff_value_mode,
|
||||
cutoff_rollouts=cfg.cutoff_rollouts,
|
||||
cutoff_rollout_policy=cfg.cutoff_rollout_policy,
|
||||
cutoff_rollout_max_steps=cfg.cutoff_rollout_max_steps,
|
||||
opponent_policy=cfg.opponent_policy,
|
||||
epsilon=cfg.traversal.regret_matching_epsilon,
|
||||
strategy_sample_interval=cfg.traversal.strategy_sample_interval,
|
||||
store_strategy_on_traverser_nodes=cfg.traversal.store_strategy_on_traverser_nodes,
|
||||
store_strategy_on_opponent_nodes=cfg.traversal.store_strategy_on_opponent_nodes,
|
||||
max_depth=cfg.traversal.max_depth,
|
||||
max_nodes=cfg.traversal.max_nodes,
|
||||
outcome_sampling_epsilon=cfg.traversal.outcome_sampling_epsilon,
|
||||
outcome_sampling_value_clip=cfg.traversal.outcome_sampling_value_clip,
|
||||
outcome_unsampled_regret=cfg.traversal.outcome_unsampled_regret,
|
||||
cutoff_value_mode=cfg.traversal.cutoff_value_mode,
|
||||
cutoff_rollouts=cfg.traversal.cutoff_rollouts,
|
||||
cutoff_rollout_policy=cfg.traversal.cutoff_rollout_policy,
|
||||
cutoff_rollout_max_steps=cfg.traversal.cutoff_rollout_max_steps,
|
||||
opponent_policy=cfg.traversal.opponent_policy,
|
||||
league_advantage_networks=league_networks,
|
||||
self_play_anchor_probability=cfg.self_play_anchor_probability,
|
||||
self_play_current_weight=cfg.self_play_current_weight,
|
||||
self_play_recent_weight=cfg.self_play_recent_weight,
|
||||
self_play_older_weight=cfg.self_play_older_weight,
|
||||
self_play_anchor_weight=cfg.self_play_anchor_weight,
|
||||
self_play_recent_window=cfg.self_play_recent_window,
|
||||
self_play_anchor_probability=cfg.self_play.anchor_probability,
|
||||
self_play_current_weight=cfg.self_play.current_weight,
|
||||
self_play_recent_weight=cfg.self_play.recent_weight,
|
||||
self_play_older_weight=cfg.self_play.older_weight,
|
||||
self_play_anchor_weight=cfg.self_play.anchor_weight,
|
||||
self_play_recent_window=cfg.self_play.recent_window,
|
||||
rng=np.random.default_rng(batch.worker_seed),
|
||||
)
|
||||
game_config = LostCitiesConfig(**batch.game_config)
|
||||
|
||||
@@ -7,25 +7,43 @@ from coolrl_lost_cities.games.classic.deep_cfr.benchmark import (
|
||||
benchmark_traversal,
|
||||
benchmark_traversal_modes,
|
||||
)
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig, load_config
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.memory import ReservoirMemory, TrainingSample
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
|
||||
from coolrl_lost_cities.games.classic.deep_cfr.traverser import DeepCFRTraverser
|
||||
|
||||
|
||||
def _deep_cfr_config(data: dict) -> DeepCFRConfig:
|
||||
return DeepCFRConfig.model_validate(data)
|
||||
|
||||
|
||||
def test_deep_cfr_loads_smoke_yaml_config() -> None:
|
||||
config = load_config("configs/deep_cfr/smoke.yaml")
|
||||
|
||||
assert config.run.iterations == 1
|
||||
assert config.network.hidden_size == 16
|
||||
assert config.traversal.traversals_per_iteration == 1
|
||||
assert config.checkpoint.directory == "runs/deep_cfr/smoke"
|
||||
|
||||
|
||||
def test_deep_cfr_trainer_smoke_run() -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=1,
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=3,
|
||||
max_nodes_per_traversal=64,
|
||||
advantage_train_steps=1,
|
||||
strategy_train_steps=1,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=23,
|
||||
save_every_iteration=False,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 1, "seed": 23},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 1,
|
||||
"max_depth": 3,
|
||||
"max_nodes": 64,
|
||||
},
|
||||
"optimization": {
|
||||
"advantage_train_steps": 1,
|
||||
"strategy_train_steps": 1,
|
||||
"batch_size": 2,
|
||||
},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=23),
|
||||
)
|
||||
@@ -43,15 +61,18 @@ def test_deep_cfr_trainer_smoke_run() -> None:
|
||||
|
||||
def test_deep_cfr_recursive_traverser_restores_state_and_collects_samples() -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=1,
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=2,
|
||||
max_nodes_per_traversal=32,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=29,
|
||||
save_every_iteration=False,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 1, "seed": 29},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 1,
|
||||
"max_depth": 2,
|
||||
"max_nodes": 32,
|
||||
},
|
||||
"optimization": {"batch_size": 2},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=29),
|
||||
)
|
||||
@@ -85,22 +106,25 @@ def test_deep_cfr_recursive_traverser_restores_state_and_collects_samples() -> N
|
||||
|
||||
def test_deep_cfr_traverser_supports_outcome_sampling_and_rollout_cutoffs() -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=1,
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=1,
|
||||
max_nodes_per_traversal=32,
|
||||
outcome_sampling_epsilon=0.25,
|
||||
outcome_sampling_value_clip=100.0,
|
||||
outcome_unsampled_regret="zero",
|
||||
cutoff_value_mode="random_rollout",
|
||||
cutoff_rollouts=2,
|
||||
cutoff_rollout_policy="random",
|
||||
cutoff_rollout_max_steps=16,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=31,
|
||||
save_every_iteration=False,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 1, "seed": 31},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 1,
|
||||
"max_depth": 1,
|
||||
"max_nodes": 32,
|
||||
"outcome_sampling_epsilon": 0.25,
|
||||
"outcome_sampling_value_clip": 100.0,
|
||||
"outcome_unsampled_regret": "zero",
|
||||
"cutoff_value_mode": "random_rollout",
|
||||
"cutoff_rollouts": 2,
|
||||
"cutoff_rollout_policy": "random",
|
||||
"cutoff_rollout_max_steps": 16,
|
||||
},
|
||||
"optimization": {"batch_size": 2},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=31),
|
||||
)
|
||||
@@ -161,19 +185,22 @@ def test_reservoir_memory_caps_samples_and_filters_player_batches() -> None:
|
||||
def test_deep_cfr_trainer_saves_loads_and_evaluates_checkpoint(tmp_path) -> None:
|
||||
checkpoint_dir = tmp_path / "deep_cfr"
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=1,
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=2,
|
||||
max_nodes_per_traversal=32,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=41,
|
||||
checkpoint_dir=str(checkpoint_dir),
|
||||
save_every_iteration=True,
|
||||
eval_every=1,
|
||||
eval_games=2,
|
||||
eval_opponents=("random",),
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 1, "seed": 41},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 1,
|
||||
"max_depth": 2,
|
||||
"max_nodes": 32,
|
||||
},
|
||||
"optimization": {"batch_size": 2},
|
||||
"checkpoint": {
|
||||
"directory": str(checkpoint_dir),
|
||||
"save_every_iteration": True,
|
||||
},
|
||||
"evaluation": {"eval_every": 1, "games": 2, "opponents": ("random",)},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=41),
|
||||
)
|
||||
@@ -181,11 +208,15 @@ def test_deep_cfr_trainer_saves_loads_and_evaluates_checkpoint(tmp_path) -> None
|
||||
metrics = trainer.train()
|
||||
latest = checkpoint_dir / "latest.pt"
|
||||
restored = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
hidden_size=16,
|
||||
seed=41,
|
||||
checkpoint_dir=str(checkpoint_dir),
|
||||
save_every_iteration=False,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"seed": 41},
|
||||
"network": {"hidden_size": 16},
|
||||
"checkpoint": {
|
||||
"directory": str(checkpoint_dir),
|
||||
"save_every_iteration": False,
|
||||
},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=41),
|
||||
)
|
||||
@@ -202,18 +233,23 @@ def test_deep_cfr_trainer_saves_loads_and_evaluates_checkpoint(tmp_path) -> None
|
||||
|
||||
def test_deep_cfr_trainer_multiprocessing_smoke_run(tmp_path) -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=1,
|
||||
traversals_per_iteration=2,
|
||||
max_traversal_depth=2,
|
||||
max_nodes_per_traversal=32,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=43,
|
||||
checkpoint_dir=str(tmp_path / "mp"),
|
||||
save_every_iteration=False,
|
||||
num_workers=2,
|
||||
traversal_worker_chunk_size=1,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 1, "seed": 43},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 2,
|
||||
"max_depth": 2,
|
||||
"max_nodes": 32,
|
||||
"num_workers": 2,
|
||||
"worker_chunk_size": 1,
|
||||
},
|
||||
"optimization": {"batch_size": 2},
|
||||
"checkpoint": {
|
||||
"directory": str(tmp_path / "mp"),
|
||||
"save_every_iteration": False,
|
||||
},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=43),
|
||||
)
|
||||
@@ -226,24 +262,26 @@ def test_deep_cfr_trainer_multiprocessing_smoke_run(tmp_path) -> None:
|
||||
|
||||
def test_deep_cfr_traversal_benchmark_smoke() -> None:
|
||||
result = benchmark_traversal(
|
||||
DeepCFRConfig(
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=2,
|
||||
hidden_size=16,
|
||||
save_every_iteration=False,
|
||||
seed=47,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"seed": 47},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {"traversals_per_iteration": 1, "max_depth": 2},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
assert result["traversal_nodes"] > 0
|
||||
assert result["nodes_per_second"] > 0.0
|
||||
comparison = benchmark_traversal_modes(
|
||||
DeepCFRConfig(
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=2,
|
||||
hidden_size=16,
|
||||
save_every_iteration=False,
|
||||
seed=48,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"seed": 48},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {"traversals_per_iteration": 1, "max_depth": 2},
|
||||
"checkpoint": {"save_every_iteration": False},
|
||||
}
|
||||
)
|
||||
)
|
||||
assert comparison["summary"]["speedup"] > 0.0
|
||||
@@ -251,20 +289,27 @@ def test_deep_cfr_traversal_benchmark_smoke() -> None:
|
||||
|
||||
def test_deep_cfr_self_play_league_records_snapshots(tmp_path) -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=2,
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=2,
|
||||
max_nodes_per_traversal=32,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=53,
|
||||
checkpoint_dir=str(tmp_path / "league"),
|
||||
save_every_iteration=False,
|
||||
opponent_policy="self_play_league",
|
||||
self_play_snapshot_every=1,
|
||||
self_play_max_snapshots=1,
|
||||
self_play_anchor_probability=1.0,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 2, "seed": 53},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 1,
|
||||
"max_depth": 2,
|
||||
"max_nodes": 32,
|
||||
"opponent_policy": "self_play_league",
|
||||
},
|
||||
"self_play": {
|
||||
"snapshot_every": 1,
|
||||
"max_snapshots": 1,
|
||||
"anchor_probability": 1.0,
|
||||
},
|
||||
"optimization": {"batch_size": 2},
|
||||
"checkpoint": {
|
||||
"directory": str(tmp_path / "league"),
|
||||
"save_every_iteration": False,
|
||||
},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=53),
|
||||
)
|
||||
@@ -277,24 +322,31 @@ def test_deep_cfr_self_play_league_records_snapshots(tmp_path) -> None:
|
||||
|
||||
def test_deep_cfr_weighted_self_play_league_uses_snapshot_bucket(tmp_path) -> None:
|
||||
trainer = DeepCFRTrainer(
|
||||
DeepCFRConfig(
|
||||
iterations=2,
|
||||
traversals_per_iteration=1,
|
||||
max_traversal_depth=2,
|
||||
max_nodes_per_traversal=32,
|
||||
batch_size=2,
|
||||
hidden_size=16,
|
||||
seed=59,
|
||||
checkpoint_dir=str(tmp_path / "weighted-league"),
|
||||
save_every_iteration=False,
|
||||
opponent_policy="self_play_league",
|
||||
self_play_snapshot_every=1,
|
||||
self_play_max_snapshots=2,
|
||||
self_play_current_weight=0.0,
|
||||
self_play_recent_weight=1.0,
|
||||
self_play_older_weight=0.0,
|
||||
self_play_anchor_weight=0.0,
|
||||
self_play_recent_window=1,
|
||||
_deep_cfr_config(
|
||||
{
|
||||
"run": {"iterations": 2, "seed": 59},
|
||||
"network": {"hidden_size": 16},
|
||||
"traversal": {
|
||||
"traversals_per_iteration": 1,
|
||||
"max_depth": 2,
|
||||
"max_nodes": 32,
|
||||
"opponent_policy": "self_play_league",
|
||||
},
|
||||
"self_play": {
|
||||
"snapshot_every": 1,
|
||||
"max_snapshots": 2,
|
||||
"current_weight": 0.0,
|
||||
"recent_weight": 1.0,
|
||||
"older_weight": 0.0,
|
||||
"anchor_weight": 0.0,
|
||||
"recent_window": 1,
|
||||
},
|
||||
"optimization": {"batch_size": 2},
|
||||
"checkpoint": {
|
||||
"directory": str(tmp_path / "weighted-league"),
|
||||
"save_every_iteration": False,
|
||||
},
|
||||
}
|
||||
),
|
||||
LostCitiesConfig(seed=59),
|
||||
)
|
||||
|
||||
@@ -2,6 +2,15 @@ version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-types"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.5.0"
|
||||
@@ -26,6 +35,8 @@ version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "torch" },
|
||||
]
|
||||
|
||||
@@ -46,8 +57,10 @@ dev = [
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numpy", specifier = ">=1.26.0" },
|
||||
{ name = "pydantic", specifier = ">=2.7" },
|
||||
{ name = "pygame", marker = "extra == 'gui'", specifier = ">=2.6.1" },
|
||||
{ name = "pygame-gui", marker = "extra == 'gui'", specifier = ">=0.6.14" },
|
||||
{ name = "pyyaml", specifier = ">=6.0" },
|
||||
{ name = "torch", specifier = ">=2.3.0" },
|
||||
]
|
||||
provides-extras = ["gui"]
|
||||
@@ -594,6 +607,123 @@ 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 = "pydantic"
|
||||
version = "2.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-types" },
|
||||
{ name = "pydantic-core" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "typing-inspection" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic-core"
|
||||
version = "2.46.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygame"
|
||||
version = "2.6.1"
|
||||
@@ -898,6 +1028,18 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "21.3.1"
|
||||
|
||||
Reference in New Issue
Block a user