diff --git a/.github/workflows/all-tok-checks.yml b/.github/workflows/all-tok-checks.yml new file mode 100644 index 00000000..4994f04c --- /dev/null +++ b/.github/workflows/all-tok-checks.yml @@ -0,0 +1,52 @@ +name: All Tokenizer Checks + +on: + push: + paths: + - 'maze_dataset/utils.py' # temporary + - 'maze_dataset/token_utils.py' # temporary + - 'maze_dataset/constants.py' + - 'maze_dataset/tokenization/*.py' + - 'maze_dataset/tokenization/MazeTokenizerModular_hashes.npz' + - 'notebooks/demo_mazetokenizermodular.ipynb' + - 'tests/all_tokenizers/*.py' + - 'pyproject.toml' # on new version or update deps + - '.github/workflows/all-tok-checks.yml' # changing this file + - '.lastversion' # on new release + workflow_dispatch: + inputs: + n_to_test: + description: 'Number of tokenizers to test' + required: false + default: 5000 + type: number + pytest_parallel: + description: '1 to parallelize tests with -n auto, to run without parallelization' + required: false + default: 1 + type: number + +jobs: + all_tok_test: + name: All Tokenizer Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + curl -sSL https://install.python-poetry.org | python3 - + poetry lock --check + poetry install + - name: tokenizer hash tests + run: | + make test_tok_hashes + - name: all tokenizer tests + run: | + N_TO_TEST=${{ github.event.inputs.n_to_test || '5000' }} + PYTEST_PARALLEL=${{ github.event.inputs.pytest_parallel || '1' }} + make test_all_tok SKIP_HASH_TEST=1 NUM_TOKENIZERS_TO_TEST=$N_TO_TEST PYTEST_PARALLEL=$PYTEST_PARALLEL \ No newline at end of file diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 31b7e2e8..d230640f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: pull_request: branches: - - main + - '*' push: branches: - main @@ -31,6 +31,7 @@ jobs: run: | pip install black python -m black --check . + test: name: Test env: @@ -70,3 +71,6 @@ jobs: - name: Notebook tests run: make test_notebooks + + - name: Test Tokenizer Hashes + run: make test_tok_hashes diff --git a/.gitignore b/.gitignore index 2abdec07..4cd1ffc8 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ tests/_temp/** htmlcov/ .vscode/** notebooks/data/** +*.bak .pypi-token .commit_log @@ -36,6 +37,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +.python-version # PyInstaller # Usually these files are written by a python script from a template diff --git a/README.md b/README.md index da7cd9ca..77f4965a 100644 --- a/README.md +++ b/README.md @@ -75,8 +75,8 @@ m.as_ascii() # RGB image, optionally without solution or endpoints, suitable for CNNs m.as_pixels() # text format for autoreregressive transformers -from maze_dataset.tokenization import MazeTokenizer, TokenizationMode -m.as_tokens(maze_tokenizer=MazeTokenizer( +from maze_dataset.tokenization import MazeTokenizerModular, TokenizationMode +m.as_tokens(maze_tokenizer=MazeTokenizerModular( tokenization_mode=TokenizationMode.AOTP_UT_rasterized, max_grid_size=100, )) # advanced visualization with many features diff --git a/docs/mazeplot_3x3.png b/docs/mazeplot_3x3.png new file mode 100644 index 00000000..4c40722c Binary files /dev/null and b/docs/mazeplot_3x3.png differ diff --git a/makefile b/makefile index ed224393..f11c1f5d 100644 --- a/makefile +++ b/makefile @@ -65,12 +65,18 @@ check-format: clean # coverage reports & benchmarks # -------------------------------------------------- # whether to run pytest with coverage report generation +PYTEST_OPTIONS ?= + COV ?= 1 ifeq ($(COV),1) - PYTEST_OPTIONS=--cov=. -else - PYTEST_OPTIONS= + PYTEST_OPTIONS+=--cov=. +endif + +PYTEST_PARALLEL ?= 0 + +ifeq ($(PYTEST_PARALLEL),1) + PYTEST_OPTIONS+=-n auto endif .PHONY: cov @@ -94,6 +100,30 @@ unit: @echo "run unit tests" $(POETRY_RUN_PYTHON) -m pytest $(PYTEST_OPTIONS) tests/unit +.PHONY: save_tok_hashes +save_tok_hashes: + @echo "generate and save tokenizer hashes" + $(POETRY_RUN_PYTHON) -m maze_dataset.tokenization.save_hashes -p + +.PHONY: test_tok_hashes +test_tok_hashes: + @echo "re-run tokenizer hashes and compare" + $(POETRY_RUN_PYTHON) -m maze_dataset.tokenization.save_hashes -p --check + + +.PHONY: test_all_tok +test_all_tok: + @echo "run tests on all tokenizers. can pass NUM_TOKENIZERS_TO_TEST arg or SKIP_HASH_TEST" + @echo "NUM_TOKENIZERS_TO_TEST=$(NUM_TOKENIZERS_TO_TEST)" + @if [ "$(SKIP_HASH_TEST)" != "1" ]; then \ + echo "Running tokenizer hash tests"; \ + $(MAKE) test_tok_hashes; \ + else \ + echo "Skipping tokenizer hash tests"; \ + fi + $(POETRY_RUN_PYTHON) -m pytest $(PYTEST_OPTIONS) --verbosity=-1 --durations=50 tests/all_tokenizers + + .PHONY: convert_notebooks convert_notebooks: diff --git a/maze_dataset/__init__.py b/maze_dataset/__init__.py index 05e9620e..8020d6cc 100644 --- a/maze_dataset/__init__.py +++ b/maze_dataset/__init__.py @@ -1,5 +1,11 @@ from maze_dataset.constants import ( SPECIAL_TOKENS, + VOCAB, + VOCAB_LIST, + VOCAB_TOKEN_TO_INDEX, + Connection, + ConnectionArray, + ConnectionList, Coord, CoordArray, CoordList, @@ -15,15 +21,22 @@ set_serialize_minimal_threshold, ) from maze_dataset.generation.generators import LatticeMazeGenerators -from maze_dataset.maze.lattice_maze import LatticeMaze, SolvedMaze +from maze_dataset.maze.lattice_maze import LatticeMaze, SolvedMaze, TargetedLatticeMaze __all__ = [ "Coord", "CoordTup", "CoordList", "CoordArray", + "Connection", + "ConnectionList", + "ConnectionArray", "SPECIAL_TOKENS", + "VOCAB", + "VOCAB_LIST", + "VOCAB_TOKEN_TO_INDEX", "LatticeMaze", + "TargetedLatticeMaze", "SolvedMaze", "MazeDataset", "MazeDatasetConfig", diff --git a/maze_dataset/constants.py b/maze_dataset/constants.py index 883fa2ef..33ad0f68 100644 --- a/maze_dataset/constants.py +++ b/maze_dataset/constants.py @@ -1,13 +1,18 @@ import warnings -from dataclasses import dataclass +from dataclasses import dataclass, field, make_dataclass import numpy as np -from jaxtyping import Int8 +from jaxtyping import Bool, Int8 -Coord = Int8[np.ndarray, "x y"] +from maze_dataset.utils import corner_first_ndindex + +Coord = Int8[np.ndarray, "row_col"] CoordTup = tuple[int, int] -CoordArray = Int8[np.ndarray, "coord x y"] +CoordArray = Int8[np.ndarray, "coord row_col"] CoordList = list[CoordTup] +Connection = Int8[np.ndarray, "coord=2 row_col=2"] +ConnectionList = Bool[np.ndarray, "lattice_dim=2 row col"] +ConnectionArray = Int8[np.ndarray, "edges leading_trailing_coord=2 row_col=2"] class SpecialTokensError(Exception): @@ -115,3 +120,75 @@ def keys(self): [-1, 0], # left ] ) + + +_VOCAB_FIELDS: list = [ + # *[(k, str, field(default=v)) for k, v in SPECIAL_TOKENS.items()], + ("COORD_PRE", str, field(default="(")), + ("COORD_INTRA", str, field(default=",")), + ("COORD_POST", str, field(default=")")), + ("TARGET_INTRA", str, field(default="=")), + ("TARGET_POST", str, field(default="||")), + ("PATH_INTRA", str, field(default=":")), + ("PATH_POST", str, field(default="THEN")), + ("NEGATIVE", str, field(default="-")), + ("UNKNOWN", str, field(default="")), + *[ + (f"TARGET_{a}", str, field(default=f"TARGET_{a}")) + for a in "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + ], + ("TARGET_NORTH", str, field(default="TARGET_NORTH")), + ("TARGET_SOUTH", str, field(default="TARGET_SOUTH")), + ("TARGET_EAST", str, field(default="TARGET_EAST")), + ("TARGET_WEST", str, field(default="TARGET_WEST")), + ("TARGET_NORTHEAST", str, field(default="TARGET_NORTHEAST")), + ("TARGET_NORTHWEST", str, field(default="TARGET_NORTHWEST")), + ("TARGET_SOUTHEAST", str, field(default="TARGET_SOUTHEAST")), + ("TARGET_SOUTHWEST", str, field(default="TARGET_SOUTHWEST")), + ("TARGET_CENTER", str, field(default="TARGET_CENTER")), + ("PATH_NORTH", str, field(default="NORTH")), + ("PATH_SOUTH", str, field(default="SOUTH")), + ("PATH_EAST", str, field(default="EAST")), + ("PATH_WEST", str, field(default="WEST")), + ("PATH_FORWARD", str, field(default="FORWARD")), + ("PATH_BACKWARD", str, field(default="BACKWARD")), + ("PATH_LEFT", str, field(default="LEFT")), + ("PATH_RIGHT", str, field(default="RIGHT")), + ("PATH_STAY", str, field(default="STAY")), + *[ + (f"I_{i:03}", str, field(default=f"+{i}")) for i in range(256) + ], # General purpose positive int tokens. Used by `StepTokenizers.Distance`. + *[ + (f"CTT_{i}", str, field(default=f"{i}")) for i in range(128) + ], # Coord tuple tokens + *[ + (f"I_N{-i:03}", str, field(default=f"{i}")) for i in range(-256, 0) + ], # General purpose negative int tokens + ("PATH_PRE", str, field(default="STEP")), + ("ADJLIST_PRE", str, field(default="ADJ_GROUP")), + ("ADJLIST_INTRA", str, field(default="&")), + ("ADJLIST_WALL", str, field(default="")), + *[(f"RESERVE_{i}", str, field(default=f"")) for i in range(708, 1596)], + *[ + (f"UT_{x:02}_{y:02}", str, field(default=f"({x},{y})")) + for x, y in corner_first_ndindex(50) + ], +] + + +_VOCAB_BASE: type = make_dataclass( + "_VOCAB_BASE", fields=_VOCAB_FIELDS, bases=(_SPECIAL_TOKENS_BASE,), frozen=True +) +# TODO: edit __getitem__ to add warning for accessing a RESERVE token + +VOCAB: _VOCAB_BASE = _VOCAB_BASE() +VOCAB_LIST: list[str] = list(VOCAB.values()) +VOCAB_TOKEN_TO_INDEX: dict[str, int] = {token: i for i, token in enumerate(VOCAB_LIST)} + +# CARDINAL_MAP: Maps tuple(coord1 - coord0) : cardinal direction +CARDINAL_MAP: dict[tuple[int, int], str] = { + (-1, 0): VOCAB.PATH_NORTH, + (1, 0): VOCAB.PATH_SOUTH, + (0, -1): VOCAB.PATH_WEST, + (0, 1): VOCAB.PATH_EAST, +} diff --git a/maze_dataset/dataset/configs.py b/maze_dataset/dataset/configs.py index 4257d2b7..ea57f8d1 100644 --- a/maze_dataset/dataset/configs.py +++ b/maze_dataset/dataset/configs.py @@ -1,7 +1,10 @@ +import copy +from typing import Mapping + from maze_dataset.dataset.maze_dataset import MazeDatasetConfig from maze_dataset.generation.generators import LatticeMazeGenerators -MAZE_DATASET_CONFIGS: dict[str, MazeDatasetConfig] = { +_MAZE_DATASET_CONFIGS_SRC: dict[str, MazeDatasetConfig] = { cfg.to_fname(): cfg for cfg in [ MazeDatasetConfig( @@ -24,3 +27,31 @@ ), ] } + + +class _MazeDatsetConfigsWrapper(Mapping[str, MazeDatasetConfig]): + def __init__(self, configs: dict[str, MazeDatasetConfig]): + self._configs = configs + + def __getitem__(self, item: str) -> MazeDatasetConfig: + return self._configs[item] + + def __len__(self) -> int: + return len(self._configs) + + def __iter__(self): + return iter(self._configs) + + def keys(self): + return self._configs.keys() + + def items(self): + return [(k, copy.deepcopy(v)) for k, v in self._configs.items()] + + def values(self): + return [copy.deepcopy(v) for v in self._configs.values()] + + +MAZE_DATASET_CONFIGS: _MazeDatsetConfigsWrapper = _MazeDatsetConfigsWrapper( + _MAZE_DATASET_CONFIGS_SRC +) diff --git a/maze_dataset/dataset/dataset.py b/maze_dataset/dataset/dataset.py index a94000ea..ed555d43 100644 --- a/maze_dataset/dataset/dataset.py +++ b/maze_dataset/dataset/dataset.py @@ -88,6 +88,7 @@ def __post_init__(self): def summary(self) -> dict: """return a summary of the config""" + # do we run this to make sure it doesn't error? self_ser: dict = self.serialize() return dict( name=self.name, diff --git a/maze_dataset/dataset/maze_dataset.py b/maze_dataset/dataset/maze_dataset.py index 3d3398c4..5b708902 100644 --- a/maze_dataset/dataset/maze_dataset.py +++ b/maze_dataset/dataset/maze_dataset.py @@ -33,6 +33,7 @@ # If `n_mazes>=SERIALIZE_MINIMAL_THRESHOLD`, then the MazeDataset will use `serialize_minimal`. # Setting to None means that `serialize_minimal` will never be used. +# Set to -1 to make calls to `read` use `MazeDataset._load_legacy`. Used for profiling only. SERIALIZE_MINIMAL_THRESHOLD: int | None = 100 @@ -145,26 +146,23 @@ def to_fname(self) -> str: def summary(self) -> dict: """return a summary of the config""" + # do we run this to make sure it doesn't error? super_summary: dict = super().summary() self_ser: dict = self.serialize() - return { - **dict( - name=self.name, - fname=self.to_fname(), - sdc_hash=self.stable_hash_cfg(), - seed=self.seed, - seq_len_min=self.seq_len_min, - seq_len_max=self.seq_len_max, - applied_filters=self.applied_filters, - ), - **{ - "grid_n": self_ser["grid_n"], - "grid_shape": self_ser["grid_shape"], - "n_mazes": self_ser["n_mazes"], - "maze_ctor_name": self_ser["maze_ctor"]["__name__"], - "maze_ctor_kwargs": self_ser["maze_ctor_kwargs"], - }, - } + return dict( + name=self.name, + fname=self.to_fname(), + sdc_hash=self.stable_hash_cfg(), + seed=self.seed, + seq_len_min=self.seq_len_min, + seq_len_max=self.seq_len_max, + applied_filters=self.applied_filters, + grid_n=self_ser["grid_n"], + n_mazes=self_ser["n_mazes"], + maze_ctor_name=self_ser["maze_ctor"]["__name__"], + maze_ctor_kwargs=self_ser["maze_ctor_kwargs"], + endpoint_kwargs=self_ser["endpoint_kwargs"], + ) # TODO: don't use this unless generating in parallel! @@ -225,7 +223,7 @@ def __getitem__(self, i: int) -> SolvedMaze: return self.mazes[i] def __deepcopy__(self, memo) -> "MazeDataset": - return MazeDataset.load(self.serialize()) + return MazeDataset.load(self._serialize_full()) def as_tokens( self, @@ -331,9 +329,16 @@ def load(cls, data: JSONitem) -> "MazeDataset": return cls._load_minimal(data) elif data["__format__"] == "MazeDataset:minimal_soln_cat": return cls._load_minimal_soln_cat(data) - else: - assert data["__format__"] == "MazeDataset" + elif data["__format__"] == "MazeDataset": + if ( + SERIALIZE_MINIMAL_THRESHOLD == -1 + ): # Allow access to `_load_legacy` for profiling + return cls._load_legacy(data) return cls._load_full(data) + else: + raise KeyError( + f"`__format__` string {data['__format__']} is not a recognized `MazeDataset` format." + ) @classmethod def _load_full(cls, data: JSONitem) -> "MazeDataset": @@ -396,6 +401,17 @@ def _load_minimal_soln_cat(cls, data: JSONitem) -> "MazeDataset": ], ) + @classmethod + def _load_legacy(cls, data: JSONitem) -> "MazeDataset": + """Legacy `load` method from <0.5.2. Used exclusively for profiling comparison.""" + assert data["__format__"] == "MazeDataset" + return cls( + **{ + key: load_item_recursive(data[key], tuple()) + for key in ["cfg", "mazes", "generation_metadata_collected"] + } + ) + def serialize(self) -> JSONitem: """serialize to zanj/json""" if ( @@ -710,6 +726,10 @@ def collect_generation_meta( ) -> MazeDataset: if dataset.generation_metadata_collected is not None: return dataset + else: + assert ( + dataset[0].generation_meta is not None + ), "generation meta is not collected and original is not present" # if the generation meta is already collected, don't collect it again, do nothing new_dataset: MazeDataset diff --git a/maze_dataset/generation/__init__.py b/maze_dataset/generation/__init__.py index ca0cac77..4e661f41 100644 --- a/maze_dataset/generation/__init__.py +++ b/maze_dataset/generation/__init__.py @@ -2,10 +2,12 @@ GENERATORS_MAP, LatticeMazeGenerators, get_maze_with_solution, + numpy_rng, ) __all__ = [ "GENERATORS_MAP", "LatticeMazeGenerators", "get_maze_with_solution", + "numpy_rng", ] diff --git a/maze_dataset/generation/generators.py b/maze_dataset/generation/generators.py index e6739554..08b59e11 100644 --- a/maze_dataset/generation/generators.py +++ b/maze_dataset/generation/generators.py @@ -4,11 +4,15 @@ import numpy as np from jaxtyping import Bool +from muutils.mlutils import GLOBAL_SEED from maze_dataset.constants import CoordArray from maze_dataset.maze import ConnectionList, Coord, LatticeMaze, SolvedMaze from maze_dataset.maze.lattice_maze import NEIGHBORS_MASK, _fill_edges_with_walls +numpy_rng = np.random.default_rng(GLOBAL_SEED) +random.seed(GLOBAL_SEED) + def _random_start_coord(grid_shape: Coord, start_coord: Coord | None) -> Coord: if start_coord is None: diff --git a/maze_dataset/maze/lattice_maze.py b/maze_dataset/maze/lattice_maze.py index 1457942d..392ce151 100644 --- a/maze_dataset/maze/lattice_maze.py +++ b/maze_dataset/maze/lattice_maze.py @@ -4,31 +4,39 @@ from itertools import chain import numpy as np -from jaxtyping import Bool, Float, Int, Int8, Shaped +from jaxtyping import Bool, Int, Int8, Shaped from muutils.json_serialize.serializable_dataclass import ( SerializableDataclass, serializable_dataclass, serializable_field, ) -from muutils.misc import list_split +from muutils.misc import isinstance_by_type_name, list_split from maze_dataset.constants import ( NEIGHBORS_MASK, SPECIAL_TOKENS, + ConnectionList, Coord, CoordArray, CoordList, CoordTup, ) -from maze_dataset.tokenization import ( - MazeTokenizer, - TokenizationMode, +from maze_dataset.token_utils import ( + TokenizerDeprecationWarning, + connection_list_to_adj_list, get_adj_list_tokens, + get_origin_tokens, get_path_tokens, + get_target_tokens, ) -from maze_dataset.tokenization.token_utils import get_origin_tokens, get_target_tokens -ConnectionList = Bool[np.ndarray, "lattice_dim x y"] +if typing.TYPE_CHECKING: + from maze_dataset.tokenization import ( + MazeTokenizer, + MazeTokenizerModular, + TokenizationMode, + ) + RGB = tuple[int, int, int] PixelGrid = Int[np.ndarray, "x y rgb"] @@ -179,7 +187,25 @@ def is_valid_path(self, path: CoordArray, empty_is_valid: bool = False) -> bool: return False return True + def coord_degrees(self) -> Int8[np.ndarray, "row col"]: + """ + Returns an array with the connectivity degree of each coord. + I.e., how many neighbors each coord has. + """ + int_conn: Int8[np.ndarray, "lattice_dim=2 row col"] = ( + self.connection_list.astype(np.int8) + ) + degrees: Int8[np.ndarray, "row col"] = np.sum( + int_conn, axis=0 + ) # Connections to east and south + degrees[:, 1:] += int_conn[1, :, :-1] # Connections to west + degrees[1:, :] += int_conn[0, :-1, :] # Connections to north + return degrees + def get_coord_neighbors(self, c: Coord) -> CoordArray: + """ + Returns an array of the neighboring, connected coords of `c`. + """ neighbors: list[Coord] = [ neighbor for neighbor in (c + NEIGHBORS_MASK) @@ -450,38 +476,7 @@ def generate_random_path( def as_adj_list( self, shuffle_d0: bool = True, shuffle_d1: bool = True ) -> Int8[np.ndarray, "conn start_end coord"]: - adj_list: Int8[np.ndarray, "conn start_end coord"] = np.full( - (self.n_connections, 2, 2), - -1, - ) - - if shuffle_d1: - flip_d1: Float[np.array, "conn"] = np.random.rand(self.n_connections) - - # loop over all nonzero elements of the connection list - i: int = 0 - for d, x, y in np.ndindex(self.connection_list.shape): - if self.connection_list[d, x, y]: - c_start: CoordTup = (x, y) - c_end: CoordTup = ( - x + (1 if d == 0 else 0), - y + (1 if d == 1 else 0), - ) - adj_list[i, 0] = np.array(c_start) - adj_list[i, 1] = np.array(c_end) - - # flip if shuffling - if shuffle_d1 and (flip_d1[i] > 0.5): - c_s, c_e = adj_list[i, 0].copy(), adj_list[i, 1].copy() - adj_list[i, 0] = c_e - adj_list[i, 1] = c_s - - i += 1 - - if shuffle_d0: - np.random.shuffle(adj_list) - - return adj_list + return connection_list_to_adj_list(self.connection_list, shuffle_d0, shuffle_d1) @classmethod def from_adj_list( @@ -521,6 +516,27 @@ def from_adj_list( ) def as_adj_list_tokens(self) -> list[str | CoordTup]: + warnings.warn( + "`LatticeMaze.as_adj_list_tokens` will be removed from the public API in a future release.", + TokenizerDeprecationWarning, + ) + return [ + SPECIAL_TOKENS.ADJLIST_START, + *chain.from_iterable( + [ + [ + tuple(c_s), + SPECIAL_TOKENS.CONNECTOR, + tuple(c_e), + SPECIAL_TOKENS.ADJACENCY_ENDLINE, + ] + for c_s, c_e in self.as_adj_list() + ] + ), + SPECIAL_TOKENS.ADJLIST_END, + ] + + def _as_adj_list_tokens(self) -> list[str | CoordTup]: return [ SPECIAL_TOKENS.ADJLIST_START, *chain.from_iterable( @@ -539,35 +555,47 @@ def as_adj_list_tokens(self) -> list[str | CoordTup]: def _as_coords_and_special_AOTP(self) -> list[CoordTup | str]: """turn the maze into adjacency list, origin, target, and solution -- keep coords as tuples""" - output: list[str] = self.as_adj_list_tokens() + + output: list[str] = self._as_adj_list_tokens() # if getattr(self, "start_pos", None) is not None: if isinstance(self, TargetedLatticeMaze): - output += self.get_start_pos_tokens() + output += self._get_start_pos_tokens() if isinstance(self, TargetedLatticeMaze): - output += self.get_end_pos_tokens() + output += self._get_end_pos_tokens() if isinstance(self, SolvedMaze): - output += self.get_solution_tokens() + output += self._get_solution_tokens() return output - def as_tokens( - self, - maze_tokenizer: MazeTokenizer | TokenizationMode, + def _as_tokens( + self, maze_tokenizer: "MazeTokenizer | TokenizationMode" ) -> list[str]: - """serialize maze and solution to tokens""" - if isinstance(maze_tokenizer, TokenizationMode): - maze_tokenizer = MazeTokenizer(maze_tokenizer) - if maze_tokenizer.is_AOTP(): + if isinstance_by_type_name(maze_tokenizer, "TokenizationMode"): + maze_tokenizer = maze_tokenizer.to_legacy_tokenizer() + if ( + isinstance_by_type_name(maze_tokenizer, "MazeTokenizer") + and maze_tokenizer.is_AOTP() + ): coords_raw: list[CoordTup | str] = self._as_coords_and_special_AOTP() coords_processed: list[str] = maze_tokenizer.coords_to_strings( coords=coords_raw, when_noncoord="include" ) return coords_processed else: - raise NotImplementedError("only AOTP tokenization is supported") + raise NotImplementedError(f"Unsupported tokenizer type: {maze_tokenizer}") + + def as_tokens( + self, + maze_tokenizer: "MazeTokenizer | TokenizationMode | MazeTokenizerModular", + ) -> list[str]: + """serialize maze and solution to tokens""" + if isinstance_by_type_name(maze_tokenizer, "MazeTokenizerModular"): + return maze_tokenizer.to_tokens(self) + else: + return self._as_tokens(maze_tokenizer) @classmethod def _from_tokens_AOTP( - cls, tokens: list[str], maze_tokenizer: MazeTokenizer + cls, tokens: list[str], maze_tokenizer: "MazeTokenizer | MazeTokenizerModular" ) -> "LatticeMaze": """create a LatticeMaze from a list of tokens""" @@ -666,10 +694,23 @@ def _from_tokens_AOTP( @classmethod def from_tokens( - cls, tokens: list[str], maze_tokenizer: MazeTokenizer | TokenizationMode + cls, + tokens: list[str], + maze_tokenizer: "MazeTokenizer | TokenizationMode | MazeTokenizerModular", ) -> "LatticeMaze": - if isinstance(maze_tokenizer, TokenizationMode): - maze_tokenizer = MazeTokenizer(maze_tokenizer) + """ + Constructs a maze from a tokenization. + Only legacy tokenizers and their `MazeTokenizerModular` analogs are supported. + """ + if isinstance_by_type_name(maze_tokenizer, "TokenizationMode"): + maze_tokenizer = maze_tokenizer.to_legacy_tokenizer() + if ( + isinstance_by_type_name(maze_tokenizer, "MazeTokenizerModular") + and not maze_tokenizer.is_legacy_equivalent() + ): + raise NotImplementedError( + f"Only legacy tokenizers and their exact `MazeTokenizerModular` analogs supported, not {maze_tokenizer}." + ) if isinstance(tokens, str): tokens = tokens.split() @@ -820,7 +861,7 @@ def from_pixels( connection_list: ConnectionList grid_shape: tuple[int, int] - # if a binary pixel grid, return regular LaticeMaze + # if a binary pixel grid, return regular LatticeMaze if len(pixel_grid.shape) == 2: connection_list, grid_shape = cls._from_pixel_grid_bw(pixel_grid) return LatticeMaze(connection_list=connection_list) @@ -1012,20 +1053,34 @@ def __post_init__(self) -> None: f"end_pos {self.end_pos} is out of bounds for grid shape {self.grid_shape}" ) - def get_start_pos_tokens(self) -> list[str | CoordTup]: + def _get_start_pos_tokens(self) -> list[str | CoordTup]: return [ SPECIAL_TOKENS.ORIGIN_START, tuple(self.start_pos), SPECIAL_TOKENS.ORIGIN_END, ] - def get_end_pos_tokens(self) -> list[str | CoordTup]: + def get_start_pos_tokens(self) -> list[str | CoordTup]: + warnings.warn( + "`TargetedLatticeMaze.get_start_pos_tokens` will be removed from the public API in a future release.", + TokenizerDeprecationWarning, + ) + return self._get_start_pos_tokens() + + def _get_end_pos_tokens(self) -> list[str | CoordTup]: return [ SPECIAL_TOKENS.TARGET_START, tuple(self.end_pos), SPECIAL_TOKENS.TARGET_END, ] + def get_end_pos_tokens(self) -> list[str | CoordTup]: + warnings.warn( + "`TargetedLatticeMaze.get_end_pos_tokens` will be removed from the public API in a future release.", + TokenizerDeprecationWarning, + ) + return self._get_end_pos_tokens() + @classmethod def from_lattice_maze( cls, @@ -1095,18 +1150,25 @@ def __init__( def __hash__(self) -> int: return hash((self.connection_list.tobytes(), self.solution.tobytes())) - def get_solution_tokens(self) -> list[str | CoordTup]: + def _get_solution_tokens(self) -> list[str | CoordTup]: return [ SPECIAL_TOKENS.PATH_START, *[tuple(c) for c in self.solution], SPECIAL_TOKENS.PATH_END, ] + def get_solution_tokens(self) -> list[str | CoordTup]: + warnings.warn( + "`LatticeMaze.get_solution_tokens` is deprecated.", + TokenizerDeprecationWarning, + ) + return self._get_solution_tokens() + # for backwards compatibility @property def maze(self) -> LatticeMaze: warnings.warn( - "maze is deprecated, SolvedMaze now inherits from LatticeMaze.", + "`maze` is deprecated, SolvedMaze now inherits from LatticeMaze.", DeprecationWarning, ) return LatticeMaze(connection_list=self.connection_list) @@ -1139,7 +1201,10 @@ def from_targeted_lattice_maze( generation_meta=targeted_lattice_maze.generation_meta, ) - def get_solution_forking_points(self) -> tuple[list[int], CoordArray]: + def get_solution_forking_points( + self, + always_include_endpoints: bool = False, + ) -> tuple[list[int], CoordArray]: """coordinates and their indicies from the solution where a fork is present - if the start point is not a dead end, this counts as a fork @@ -1153,7 +1218,9 @@ def get_solution_forking_points(self) -> tuple[list[int], CoordArray]: # since the previous coord doesn't count as a choice is_endpoint: bool = idx == 0 or idx == self.solution.shape[0] - 1 theshold: int = 1 if is_endpoint else 2 - if self.get_coord_neighbors(coord).shape[0] > theshold: + if self.get_coord_neighbors(coord).shape[0] > theshold or ( + is_endpoint and always_include_endpoints + ): output_idxs.append(idx) output_coords.append(coord) diff --git a/maze_dataset/plotting/print_tokens.py b/maze_dataset/plotting/print_tokens.py index 3f56545f..a722f271 100644 --- a/maze_dataset/plotting/print_tokens.py +++ b/maze_dataset/plotting/print_tokens.py @@ -1,13 +1,15 @@ import html +import textwrap from typing import Literal, Sequence import matplotlib import numpy as np from IPython.display import HTML, display from jaxtyping import UInt8 +from muutils.misc import flatten from maze_dataset.constants import SPECIAL_TOKENS -from maze_dataset.tokenization import tokens_between +from maze_dataset.token_utils import tokens_between RGBArray = UInt8[np.ndarray, "n 3"] @@ -46,8 +48,14 @@ def color_tokens_rgb( fmt: FormatType = "html", template: str | None = None, clr_join: str | None = None, + max_length: int | None = None, ) -> str: - """tokens will not be escaped if `fmt` is None""" + """ + tokens will not be escaped if `fmt` is None + + # Parameters: + - `max_length: int | None`: Max number of characters before triggering a line wrap, i.e., making a new colorbox. If `None`, no limit on max length. + """ # process format if fmt is None: assert template is not None @@ -58,6 +66,24 @@ def color_tokens_rgb( template = TEMPLATES[fmt] clr_join = _COLOR_JOIN[fmt] + if max_length is not None: + wrapped = list( + map( + lambda x: textwrap.wrap( + x, width=max_length, break_long_words=False, break_on_hyphens=False + ), + tokens, + ) + ) + colors = list( + flatten( + [[colors[i]] * len(wrapped[i]) for i in range(len(wrapped))], + levels_to_flatten=1, + ) + ) + wrapped = list(flatten(wrapped, levels_to_flatten=1)) + tokens = wrapped + # put everything together output = [ template.format( @@ -127,9 +153,7 @@ def color_tokens_cmap( def color_maze_tokens_AOTP( - tokens: list[str], - fmt: FormatType = "html", - template: str | None = None, + tokens: list[str], fmt: FormatType = "html", template: str | None = None, **kwargs ) -> str: output: list[str] = [ " ".join( @@ -145,10 +169,7 @@ def color_maze_tokens_AOTP( ) return color_tokens_rgb( - tokens=output, - colors=colors, - fmt=fmt, - template=template, + tokens=output, colors=colors, fmt=fmt, template=template, **kwargs ) diff --git a/maze_dataset/testing_utils.py b/maze_dataset/testing_utils.py new file mode 100644 index 00000000..17979d7d --- /dev/null +++ b/maze_dataset/testing_utils.py @@ -0,0 +1,177 @@ +""" +Shared utilities for tests only. +Do not import into any module outside of the tests directory +""" + +import itertools +from typing import Final, NamedTuple + +import frozendict +import numpy as np + +from maze_dataset import ( + CoordArray, + LatticeMaze, + LatticeMazeGenerators, + MazeDataset, + MazeDatasetConfig, + SolvedMaze, + TargetedLatticeMaze, +) +from maze_dataset.generation import LatticeMazeGenerators +from maze_dataset.tokenization import ( + MazeTokenizer, + MazeTokenizerModular, + TokenizationMode, +) + +GRID_N: Final[int] = 5 +N_MAZES: Final[int] = 5 +CFG: Final[MazeDatasetConfig] = MazeDatasetConfig( + name="test", + grid_n=GRID_N, + n_mazes=N_MAZES, + maze_ctor=LatticeMazeGenerators.gen_dfs, +) +MAZE_DATASET: Final[MazeDataset] = MazeDataset.from_config( + CFG, + do_download=False, + load_local=False, + do_generate=True, + save_local=False, + verbose=True, + gen_parallel=False, +) +LATTICE_MAZES: Final[tuple[LatticeMaze]] = tuple( + LatticeMazeGenerators.gen_dfs(np.array([GRID_N, GRID_N])) for _ in range(N_MAZES) +) +_PATHS = tuple(maze.generate_random_path() for maze in LATTICE_MAZES) +TARGETED_MAZES: Final[tuple[TargetedLatticeMaze]] = tuple( + TargetedLatticeMaze.from_lattice_maze(maze, path[0], path[-1]) + for maze, path in zip(LATTICE_MAZES, _PATHS) +) +# MIXED_MAZES alternates the maze types, so you can slice a contiguous subset and still get all types +MIXED_MAZES: Final[tuple[LatticeMaze | TargetedLatticeMaze | SolvedMaze]] = tuple( + x + for x in itertools.chain.from_iterable( + itertools.zip_longest(MAZE_DATASET.mazes, TARGETED_MAZES, LATTICE_MAZES) + ) +) + + +class MANUAL_MAZE(NamedTuple): + tokens: str + ascii: tuple[str] + straightaway_footprints: CoordArray + + +ASCII_MAZES: Final[frozendict.frozendict[str, MANUAL_MAZE]] = frozendict.frozendict( + small_3x3=MANUAL_MAZE( + tokens=" (2,0) <--> (2,1) ; (0,0) <--> (0,1) ; (0,0) <--> (1,0) ; (0,2) <--> (1,2) ; (1,0) <--> (2,0) ; (0,2) <--> (0,1) ; (2,2) <--> (2,1) ; (1,1) <--> (2,1) ; (0,0) (2,1) (0,0) (1,0) (2,0) (2,1) ", + ascii=( + "#######", + "#S #", + "#X### #", + "#X# # #", + "#X# ###", + "#XXE #", + "#######", + ), + straightaway_footprints=np.array( + [ + [0, 0], + [2, 0], + [2, 1], + ] + ), + ), + big_10x10=MANUAL_MAZE( + tokens=" (8,2) <--> (8,3) ; (3,7) <--> (3,6) ; (6,7) <--> (6,8) ; (4,6) <--> (5,6) ; (9,5) <--> (9,4) ; (3,3) <--> (3,4) ; (5,1) <--> (4,1) ; (2,6) <--> (2,7) ; (8,5) <--> (8,4) ; (1,9) <--> (2,9) ; (4,1) <--> (4,2) ; (0,8) <--> (0,7) ; (5,4) <--> (5,3) ; (6,3) <--> (6,4) ; (5,0) <--> (4,0) ; (5,3) <--> (5,2) ; (3,1) <--> (2,1) ; (9,1) <--> (9,0) ; (3,5) <--> (3,6) ; (5,5) <--> (6,5) ; (7,1) <--> (7,2) ; (0,1) <--> (1,1) ; (7,8) <--> (8,8) ; (3,9) <--> (4,9) ; (4,6) <--> (4,7) ; (0,6) <--> (0,7) ; (3,4) <--> (3,5) ; (6,0) <--> (5,0) ; (7,7) <--> (7,6) ; (1,6) <--> (0,6) ; (6,1) <--> (6,0) ; (8,6) <--> (8,7) ; (9,9) <--> (9,8) ; (1,8) <--> (1,9) ; (2,1) <--> (2,2) ; (9,2) <--> (9,3) ; (5,9) <--> (6,9) ; (3,2) <--> (2,2) ; (0,8) <--> (0,9) ; (5,6) <--> (5,7) ; (2,3) <--> (2,4) ; (4,5) <--> (4,4) ; (8,9) <--> (8,8) ; (9,6) <--> (8,6) ; (3,7) <--> (3,8) ; (8,0) <--> (7,0) ; (6,1) <--> (6,2) ; (0,1) <--> (0,0) ; (7,3) <--> (7,4) ; (9,4) <--> (9,3) ; (9,6) <--> (9,5) ; (8,7) <--> (7,7) ; (5,2) <--> (5,1) ; (0,0) <--> (1,0) ; (7,2) <--> (7,3) ; (2,5) <--> (2,6) ; (4,9) <--> (5,9) ; (5,5) <--> (5,4) ; (5,6) <--> (6,6) ; (7,8) <--> (7,9) ; (1,7) <--> (2,7) ; (4,6) <--> (4,5) ; (1,1) <--> (1,2) ; (3,1) <--> (3,0) ; (1,5) <--> (1,6) ; (8,3) <--> (8,4) ; (9,9) <--> (8,9) ; (8,5) <--> (7,5) ; (1,4) <--> (2,4) ; (3,0) <--> (4,0) ; (3,3) <--> (4,3) ; (6,9) <--> (6,8) ; (1,0) <--> (2,0) ; (6,0) <--> (7,0) ; (8,0) <--> (9,0) ; (2,3) <--> (2,2) ; (2,8) <--> (3,8) ; (5,7) <--> (6,7) ; (1,3) <--> (0,3) ; (9,7) <--> (9,8) ; (7,5) <--> (7,4) ; (1,8) <--> (2,8) ; (6,5) <--> (6,4) ; (0,2) <--> (1,2) ; (0,7) <--> (1,7) ; (0,3) <--> (0,2) ; (4,3) <--> (4,2) ; (5,8) <--> (4,8) ; (9,1) <--> (8,1) ; (9,2) <--> (8,2) ; (1,3) <--> (1,4) ; (2,9) <--> (3,9) ; (4,8) <--> (4,7) ; (0,5) <--> (0,4) ; (8,1) <--> (7,1) ; (0,3) <--> (0,4) ; (9,7) <--> (9,6) ; (7,6) <--> (6,6) ; (1,5) <--> (0,5) ; (6,2) (2,1) (6,2) (6,1) (6,0) (5,0) (4,0) (3,0) (3,1) (2,1) ", + ascii=( + "#####################", + "# # # #", + "# # # # ### # # #####", + "# # # # # # #", + "# ####### ##### # # #", + "# #E # # # #", + "###X# ########### # #", + "#XXX# # # #", + "#X##### ########### #", + "#X# # # #", + "#X# ######### ### # #", + "#X# # # # #", + "#X######### # # ### #", + "#XXXXS# # # #", + "# ########### #######", + "# # # # #", + "# # ####### ### # ###", + "# # # # # #", + "# # # ####### ##### #", + "# # #", + "#####################", + ), + straightaway_footprints=np.array( + [ + [6, 2], + [6, 0], + [3, 0], + [3, 1], + [2, 1], + ] + ), + ), + longer_10x10=MANUAL_MAZE( + tokens=" (8,2) <--> (8,3) ; (3,7) <--> (3,6) ; (6,7) <--> (6,8) ; (4,6) <--> (5,6) ; (9,5) <--> (9,4) ; (3,3) <--> (3,4) ; (5,1) <--> (4,1) ; (2,6) <--> (2,7) ; (8,5) <--> (8,4) ; (1,9) <--> (2,9) ; (4,1) <--> (4,2) ; (0,8) <--> (0,7) ; (5,4) <--> (5,3) ; (6,3) <--> (6,4) ; (5,0) <--> (4,0) ; (5,3) <--> (5,2) ; (3,1) <--> (2,1) ; (9,1) <--> (9,0) ; (3,5) <--> (3,6) ; (5,5) <--> (6,5) ; (7,1) <--> (7,2) ; (0,1) <--> (1,1) ; (7,8) <--> (8,8) ; (3,9) <--> (4,9) ; (4,6) <--> (4,7) ; (0,6) <--> (0,7) ; (3,4) <--> (3,5) ; (6,0) <--> (5,0) ; (7,7) <--> (7,6) ; (1,6) <--> (0,6) ; (6,1) <--> (6,0) ; (8,6) <--> (8,7) ; (9,9) <--> (9,8) ; (1,8) <--> (1,9) ; (2,1) <--> (2,2) ; (9,2) <--> (9,3) ; (5,9) <--> (6,9) ; (3,2) <--> (2,2) ; (0,8) <--> (0,9) ; (5,6) <--> (5,7) ; (2,3) <--> (2,4) ; (4,5) <--> (4,4) ; (8,9) <--> (8,8) ; (9,6) <--> (8,6) ; (3,7) <--> (3,8) ; (8,0) <--> (7,0) ; (6,1) <--> (6,2) ; (0,1) <--> (0,0) ; (7,3) <--> (7,4) ; (9,4) <--> (9,3) ; (9,6) <--> (9,5) ; (8,7) <--> (7,7) ; (5,2) <--> (5,1) ; (0,0) <--> (1,0) ; (7,2) <--> (7,3) ; (2,5) <--> (2,6) ; (4,9) <--> (5,9) ; (5,5) <--> (5,4) ; (5,6) <--> (6,6) ; (7,8) <--> (7,9) ; (1,7) <--> (2,7) ; (4,6) <--> (4,5) ; (1,1) <--> (1,2) ; (3,1) <--> (3,0) ; (1,5) <--> (1,6) ; (8,3) <--> (8,4) ; (9,9) <--> (8,9) ; (8,5) <--> (7,5) ; (1,4) <--> (2,4) ; (3,0) <--> (4,0) ; (3,3) <--> (4,3) ; (6,9) <--> (6,8) ; (1,0) <--> (2,0) ; (6,0) <--> (7,0) ; (8,0) <--> (9,0) ; (2,3) <--> (2,2) ; (2,8) <--> (3,8) ; (5,7) <--> (6,7) ; (1,3) <--> (0,3) ; (9,7) <--> (9,8) ; (7,5) <--> (7,4) ; (1,8) <--> (2,8) ; (6,5) <--> (6,4) ; (0,2) <--> (1,2) ; (0,7) <--> (1,7) ; (0,3) <--> (0,2) ; (4,3) <--> (4,2) ; (5,8) <--> (4,8) ; (9,1) <--> (8,1) ; (9,2) <--> (8,2) ; (1,3) <--> (1,4) ; (2,9) <--> (3,9) ; (4,8) <--> (4,7) ; (0,5) <--> (0,4) ; (8,1) <--> (7,1) ; (0,3) <--> (0,4) ; (9,7) <--> (9,6) ; (7,6) <--> (6,6) ; (1,5) <--> (0,5) ; (6,2) (2,1) (6,2) (6,1) (6,0) (5,0) (4,0) (3,0) (3,1) (2,1) (2,2) (2,3) (2,4) (1,4) (1,3) (0,3) (0,4) (0,5) (1,5) (1,6) (0,6) (0,7) (0,8) ", + ascii=( + "#####################", + "# # XXXXX#XXXXE #", + "# # # #X###X#X# #####", + "# # #XXX#XXX# # #", + "# #######X##### # # #", + "# #XXXXXXX# # # #", + "###X# ########### # #", + "#XXX# # # #", + "#X##### ########### #", + "#X# # # #", + "#X# ######### ### # #", + "#X# # # # #", + "#X######### # # ### #", + "#XXXXS# # # #", + "# ########### #######", + "# # # # #", + "# # ####### ### # ###", + "# # # # # #", + "# # # ####### ##### #", + "# # #", + "#####################", + ), + straightaway_footprints=np.array( + [ + [6, 2], + [6, 0], + [3, 0], + [3, 1], + [2, 1], + [2, 4], + [1, 4], + [1, 3], + [0, 3], + [0, 5], + [1, 5], + [1, 6], + [0, 6], + [0, 8], + ] + ), + ), +) + +# A list of legacy `MazeTokenizer`s and their `MazeTokenizerModular` equivalents. +# Used for unit tests where both versions are supported +LEGACY_AND_EQUIVALENT_TOKENIZERS: list[MazeTokenizer, MazeTokenizerModular] = [ + *[ + MazeTokenizer(tokenization_mode=tok_mode, max_grid_size=20) + for tok_mode in TokenizationMode + ], + *[MazeTokenizerModular.from_legacy(tok_mode) for tok_mode in TokenizationMode], +] diff --git a/maze_dataset/token_utils.py b/maze_dataset/token_utils.py new file mode 100644 index 00000000..9fb847f6 --- /dev/null +++ b/maze_dataset/token_utils.py @@ -0,0 +1,455 @@ +"""a whole bunch of utilities for tokenization""" + +import re +import typing +import warnings +from collections import Counter +from typing import Callable + +import numpy as np +from jaxtyping import Bool, Float, Int, Int8 +from muutils.errormode import ErrorMode +from muutils.misc import list_join +from muutils.misc.sequence import WhenMissing + +from maze_dataset.constants import ( + CARDINAL_MAP, + SPECIAL_TOKENS, + VOCAB, + ConnectionArray, + ConnectionList, + CoordTup, +) + +# filtering things from a prompt or generated text +# ================================================== + + +def remove_padding_from_token_str(token_str: str) -> str: + token_str = token_str.replace(f"{SPECIAL_TOKENS.PADDING} ", "") + token_str = token_str.replace(f"{SPECIAL_TOKENS.PADDING}", "") + return token_str + + +def tokens_between( + tokens: list[str], + start_value: str, + end_value: str, + include_start: bool = False, + include_end: bool = False, + except_when_tokens_not_unique: bool = False, +) -> list[str]: + if start_value == end_value: + raise ValueError( + f"start_value and end_value cannot be the same: {start_value = } {end_value = }" + ) + if except_when_tokens_not_unique: + if (tokens.count(start_value) != 1) or (tokens.count(end_value) != 1): + raise ValueError( + "start_value or end_value is not unique in the input tokens:", + f"{tokens.count(start_value) = } {tokens.count(end_value) = }" + f"{start_value = } {end_value = }", + f"{tokens = }", + ) + else: + if (tokens.count(start_value) < 1) or (tokens.count(end_value) < 1): + raise ValueError( + "start_value or end_value is not present in the input tokens:", + f"{tokens.count(start_value) = } {tokens.count(end_value) = }", + f"{start_value = } {end_value = }", + f"{tokens = }", + ) + + start_idx: int = tokens.index(start_value) + int(not include_start) + end_idx: int = tokens.index(end_value) + int(include_end) + + assert start_idx < end_idx, "Start must come before end" + + return tokens[start_idx:end_idx] + + +def get_adj_list_tokens(tokens: list[str]) -> list[str]: + return tokens_between( + tokens, SPECIAL_TOKENS.ADJLIST_START, SPECIAL_TOKENS.ADJLIST_END + ) + + +def get_path_tokens(tokens: list[str], trim_end: bool = False) -> list[str]: + """The path is considered everything from the first path coord to the path_end token, if it exists.""" + if SPECIAL_TOKENS.PATH_START not in tokens: + raise ValueError( + f"Path start token {SPECIAL_TOKENS.PATH_START} not found in tokens:\n{tokens}" + ) + start_idx: int = tokens.index(SPECIAL_TOKENS.PATH_START) + int(trim_end) + end_idx: int | None = None + if trim_end and (SPECIAL_TOKENS.PATH_END in tokens): + end_idx = tokens.index(SPECIAL_TOKENS.PATH_END) + return tokens[start_idx:end_idx] + + +def get_context_tokens(tokens: list[str]) -> list[str]: + return tokens_between( + tokens, + SPECIAL_TOKENS.ADJLIST_START, + SPECIAL_TOKENS.PATH_START, + include_start=True, + include_end=True, + ) + + +def get_origin_tokens(tokens: list[str]) -> list[str]: + return tokens_between( + tokens, + SPECIAL_TOKENS.ORIGIN_START, + SPECIAL_TOKENS.ORIGIN_END, + include_start=False, + include_end=False, + ) + + +def get_target_tokens(tokens: list[str]) -> list[str]: + return tokens_between( + tokens, + SPECIAL_TOKENS.TARGET_START, + SPECIAL_TOKENS.TARGET_END, + include_start=False, + include_end=False, + ) + + +def get_cardinal_direction(coords: Int[np.ndarray, "start_end=2 row_col=2"]) -> str: + """Returns the cardinal direction token corresponding to traveling from `coords[0]` to `coords[1]`.""" + return CARDINAL_MAP[tuple(coords[1] - coords[0])] + + +def get_relative_direction(coords: Int[np.ndarray, "prev_cur_next=3 row_col=2"]) -> str: + """Returns the relative first-person direction token corresponding to traveling from `coords[1]` to `coords[2]`. + # Parameters + - `coords`: Contains 3 Coords, each of which must neighbor the previous Coord. + - `coords[0]`: The previous location, used to determine the current absolute direction that the "agent" is facing. + - `coords[1]`: The current location + - `coords[2]`: The next location. May be equal to the current location. + """ + if coords.shape != (3, 2): + raise ValueError(f"`coords` must have shape (3,2). Got {coords.shape} instead.") + directions = coords[1:] - coords[:-1] + if not np.all(np.linalg.norm(directions, axis=1) <= np.array([1.1, 1.1])): + # Use floats as constant since `np.linalg.norm` returns float array + raise ValueError( + f"Adjacent `coords` must be neighboring or equivalent. Got {coords} instead." + ) + if np.array_equal(coords[1], coords[2]): + return VOCAB.PATH_STAY + if np.array_equal(coords[0], coords[2]): + return VOCAB.PATH_BACKWARD + if np.array_equal(coords[0], coords[1]): + raise ValueError( + f"Previous first-person direction indeterminate from {coords=}." + ) + if np.array_equal(directions[0], directions[1]): + return VOCAB.PATH_FORWARD + directions = np.append( + directions, [[0], [0]], axis=1 + ) # Augment to represent unit basis vectors in 3D + match np.cross(directions[0], directions[1])[-1]: + case 1: + return VOCAB.PATH_LEFT + case -1: + return VOCAB.PATH_RIGHT + + +class TokenizerPendingDeprecationWarning(PendingDeprecationWarning): + """Pending deprecation warnings related to the `MazeTokenizerModular` upgrade.""" + + pass + + +def str_is_coord(coord_str: str, allow_whitespace: bool = True) -> bool: + """return True if the string represents a coordinate, False otherwise""" + + warnings.warn( + "`util.str_is_coord` only supports legacy UT strings. Function will be replaced with a generalized version in a future release.", + TokenizerPendingDeprecationWarning, + ) + strip_func: Callable[[str], str] = lambda x: x.strip() if allow_whitespace else x + + coord_str = strip_func(coord_str) + + return all( + [ + coord_str.startswith("("), + coord_str.endswith(")"), + "," in coord_str, + all( + [ + strip_func(x).isdigit() + for x in strip_func(coord_str.lstrip("(").rstrip(")")).split(",") + ] + ), + ] + ) + + +class TokenizerDeprecationWarning(DeprecationWarning): + """Deprecation warnings related to the `MazeTokenizerModular` upgrade.""" + + pass + + +# coordinate to strings +# ================================================== + + +def _coord_to_strings_UT(coord: typing.Sequence[int]) -> list[str]: + """convert a coordinate to a string: `(i,j)`->"(i,j)" + always returns a list of length 1""" + return [f"({','.join(str(c) for c in coord)})"] + + +def _coord_to_strings_indexed(coord: typing.Sequence[int]) -> list[str]: + """convert a coordinate to a list of indexed strings: `(i,j)`->"(", "i", ",", "j", ")" """ + return [ + "(", + *list_join([str(c) for c in coord], lambda: ","), + ")", + ] + + +def coord_str_to_tuple( + coord_str: str, allow_whitespace: bool = True +) -> tuple[int, ...]: + """convert a coordinate string to a tuple""" + strip_func: Callable[[str], str] = lambda x: x.strip() if allow_whitespace else x + coord_str = strip_func(coord_str) + stripped: str = strip_func(coord_str.lstrip("(").rstrip(")")) + return tuple(int(strip_func(x)) for x in stripped.split(",")) + + +def coord_str_to_coord_np(coord_str: str, allow_whitespace: bool = True) -> np.ndarray: + """convert a coordinate string to a numpy array""" + return np.array(coord_str_to_tuple(coord_str, allow_whitespace=allow_whitespace)) + + +def coord_str_to_tuple_noneable(coord_str: str) -> CoordTup | None: + """convert a coordinate string to a tuple, or None if the string is not a coordinate string""" + if not str_is_coord(coord_str): + return None + return coord_str_to_tuple(coord_str) + + +def coords_string_split_UT(coords: str) -> list[str]: + """Splits a string of tokens into a list containing the UT tokens for each coordinate. + + Not capable of producing indexed tokens ("(", "1", ",", "2", ")"), only unique tokens ("(1,2)"). + Non-whitespace portions of the input string not matched are preserved in the same list: + "(1,2) (5,6)" -> ["(1,2)", "", "(5,6)"] + """ + # ty gpt4 + return re.findall(r"\([^)]*\)|\S+", coords) + + +# back and forth in wrapped form +# ================================================== +def strings_to_coords( + text: str | list[str], + when_noncoord: WhenMissing = "skip", +) -> list[str | CoordTup]: + """converts a list of tokens to a list of coordinates + + returns list[CoordTup] if `when_noncoord` is "skip" or "error" + returns list[str | CoordTup] if `when_noncoord` is "include" + """ + warnings.warn( + "`util.strings_to_coords` only supports legacy UT strings. Function will be replaced with a generalized version in a future release.", + TokenizerPendingDeprecationWarning, + ) + tokens_joined: str = text if isinstance(text, str) else " ".join(text) + tokens_processed: list[str] = coords_string_split_UT(tokens_joined) + result: list[str] = list() + for token in tokens_processed: + coord: CoordTup | None = coord_str_to_tuple_noneable(token) + if coord is None: + if when_noncoord == "skip": + continue + elif when_noncoord == "error": + raise ValueError( + f"Invalid non-coordinate token '{token}' in text: '{text}'" + ) + elif when_noncoord == "include": + result.append(token) + else: + raise ValueError(f"Invalid when_noncoord value '{when_noncoord}'") + else: + result.append(coord) + return result + + +def coords_to_strings( + coords: list[str | CoordTup], + coord_to_strings_func: Callable[[CoordTup], list[str]], + when_noncoord: WhenMissing = "skip", +) -> list[str]: + """converts a list of coordinates to a list of strings (tokens) + + expects list[CoordTup] if `when_noncoord` is "error" + expects list[str | CoordTup] if `when_noncoord` is "include" or "skip" + """ + result: list[str] = list() + for coord in coords: + if isinstance(coord, str): + if when_noncoord == "skip": + continue + elif when_noncoord == "error": + raise ValueError( + f"Invalid non-coordinate '{coord}' in list of coords: '{coords}'" + ) + elif when_noncoord == "include": + result.append(coord) + else: + raise ValueError(f"Invalid when_noncoord value '{when_noncoord}'") + else: + result.extend(coord_to_strings_func(coord)) + return result + + +def get_token_regions(toks: list[str]) -> tuple[list[str], list[str]]: + adj_list_start, adj_list_end = toks.index("") + 1, toks.index( + "" + ) + adj_list = toks[adj_list_start:adj_list_end] + non_adj_list = toks[:adj_list_start] + toks[adj_list_end:] + return adj_list, non_adj_list + + +def equal_except_adj_list_sequence( + rollout1: list[str], + rollout2: list[str], + do_except: bool = False, + when_counter_mismatch: ErrorMode = ErrorMode.EXCEPT, + when_len_mismatch: ErrorMode = ErrorMode.EXCEPT, +) -> bool: + """Returns if the rollout strings are equal, allowing for differently sequenced adjacency lists. + and tokens must be in the rollouts. + Intended ONLY for determining if two tokenization schemes are the same for rollouts generated from the same maze. + This function should NOT be used to determine if two rollouts encode the same `LatticeMaze` object. + + # Warning: CTT False Positives + This function is not robustly correct for some corner cases using `CoordTokenizers.CTT`. + If rollouts are passed for identical tokenizers processing two slightly different mazes, a false positive is possible. + More specifically, some cases of zero-sum adding and removing of connections in a maze within square regions along the diagonal will produce a false positive. + """ + + if len(rollout1) != len(rollout2): + if do_except: + when_len_mismatch.process( + f"Rollouts are not the same length: {len(rollout1)} != {len(rollout2)}" + ) + return False + if ("" in rollout1) ^ ("" in rollout2): + if do_except: + raise ValueError( + f"Rollouts do not have the same token: `{'' in rollout1 = }` != `{'' in rollout2 = }`" + ) + return False + if ("" in rollout1) ^ ("" in rollout2): + if do_except: + raise ValueError( + f"Rollouts do not have the same token: `{'' in rollout1 = }` != `{'' in rollout2 = }`" + ) + return False + + adj_list1, non_adj_list1 = get_token_regions(rollout1) + adj_list2, non_adj_list2 = get_token_regions(rollout2) + if non_adj_list1 != non_adj_list2: + if do_except: + when_len_mismatch.process( + f"Non-adjacency list tokens are not the same:\n{non_adj_list1}\n!=\n{non_adj_list2}" + ) + raise ValueError( + f"Non-adjacency list tokens are not the same:\n{non_adj_list1}\n!=\n{non_adj_list2}" + ) + return False + counter1: Counter = Counter(adj_list1) + counter2: Counter = Counter(adj_list2) + counters_eq: bool = counter1 == counter2 + if not counters_eq: + if do_except: + when_counter_mismatch.process( + f"Adjacency list counters are not the same:\n{counter1}\n!=\n{counter2}\n{counter1 - counter2 = }" + ) + return False + + return True + + +def connection_list_to_adj_list( + conn_list: ConnectionList, + shuffle_d0: bool = True, + shuffle_d1: bool = True, +) -> Int8[np.ndarray, "conn start_end=2 coord=2"]: + """converts a `ConnectionList` (special lattice format) to a shuffled adjacency list + + # Parameters: + - `conn_list: ConnectionList` + special internal format for graphs which are subgraphs of a lattice + - `shuffle_d0: bool` + shuffle the adjacency list along the 0th axis (order of pairs) + - `shuffle_d1: bool` + shuffle the adjacency list along the 1st axis (order of coordinates in each pair). + If `False`, all pairs have the smaller coord first. + + + # Returns: + - `Int8[np.ndarray, "conn start_end=2 coord=2"]` + adjacency list in the shape `(n_connections, 2, 2)` + """ + + n_connections: int = conn_list.sum() + adj_list: Int8[np.ndarray, "conn start_end=2 coord=2"] = np.full( + (n_connections, 2, 2), -1, dtype=np.int8 + ) + + if shuffle_d1: + flip_d1: Float[np.ndarray, "conn"] = np.random.rand(n_connections) + + # loop over all nonzero elements of the connection list + i: int = 0 + for d, x, y in np.ndindex(conn_list.shape): + if conn_list[d, x, y]: + c_start: CoordTup = (x, y) + c_end: CoordTup = ( + x + (1 if d == 0 else 0), + y + (1 if d == 1 else 0), + ) + adj_list[i, 0] = np.array(c_start, dtype=np.int8) + adj_list[i, 1] = np.array(c_end, dtype=np.int8) + + # flip if shuffling + if shuffle_d1 and (flip_d1[i] > 0.5): + c_s, c_e = adj_list[i, 0].copy(), adj_list[i, 1].copy() + adj_list[i, 0] = c_e + adj_list[i, 1] = c_s + + i += 1 + + if shuffle_d0: + np.random.shuffle(adj_list) + + return adj_list + + +def is_connection( + edges: ConnectionArray, connection_list: ConnectionList +) -> Bool[np.ndarray, "is_connection=edges"]: + """ + Returns if each edge in `edges` is a connection (`True`) or wall (`False`) in `connection_list`. + """ + sorted_edges = np.sort(edges, axis=1) + edge_direction = ( + (sorted_edges[:, 1, :] - sorted_edges[:, 0, :])[:, 0] == 0 + ).astype(np.int8) + return connection_list[edge_direction, sorted_edges[:, 0, 0], sorted_edges[:, 0, 1]] + + +# string to coordinate representation +# ================================================== diff --git a/maze_dataset/tokenization/MazeTokenizerModular_hashes.npz b/maze_dataset/tokenization/MazeTokenizerModular_hashes.npz new file mode 100644 index 00000000..3e9079ab Binary files /dev/null and b/maze_dataset/tokenization/MazeTokenizerModular_hashes.npz differ diff --git a/maze_dataset/tokenization/__init__.py b/maze_dataset/tokenization/__init__.py index 7d320c8b..52d05de9 100644 --- a/maze_dataset/tokenization/__init__.py +++ b/maze_dataset/tokenization/__init__.py @@ -1,24 +1,36 @@ -from maze_dataset.tokenization.maze_tokenizer import MazeTokenizer, TokenizationMode -from maze_dataset.tokenization.token_utils import ( - get_adj_list_tokens, - get_context_tokens, - get_origin_tokens, - get_path_tokens, - get_target_tokens, +from maze_dataset.tokenization.maze_tokenizer import ( + AdjListTokenizers, + CoordTokenizers, + EdgeGroupings, + EdgePermuters, + EdgeSubsets, + MazeTokenizer, + MazeTokenizerModular, + PathTokenizers, + PromptSequencers, + StepSizes, + StepTokenizers, + TargetTokenizers, + TokenizationMode, + _TokenizerElement, get_tokens_up_to_path_start, - tokens_between, ) -from maze_dataset.tokenization.util import coord_str_to_tuple __all__ = [ "MazeTokenizer", "TokenizationMode", + "_TokenizerElement", + "MazeTokenizerModular", + "PromptSequencers", + "CoordTokenizers", + "AdjListTokenizers", + "EdgeGroupings", + "EdgePermuters", + "EdgeSubsets", + "TargetTokenizers", + "StepSizes", + "StepTokenizers", + "PathTokenizers", "coord_str_to_tuple", - "get_adj_list_tokens", - "get_context_tokens", - "get_origin_tokens", - "get_path_tokens", - "get_target_tokens", "get_tokens_up_to_path_start", - "tokens_between", ] diff --git a/maze_dataset/tokenization/all_tokenizers.py b/maze_dataset/tokenization/all_tokenizers.py new file mode 100644 index 00000000..720f4bdc --- /dev/null +++ b/maze_dataset/tokenization/all_tokenizers.py @@ -0,0 +1,181 @@ +"""Contains `ALL_TOKENIZERS` and supporting limited-use functions. + +# `ALL_TOKENIZERS` +A comprehensive collection of all valid `MazeTokenizerModular` objects. +This is an overwhelming majority subset of the set of all possible `MazeTokenizerModular` objects. +Other tokenizers not contained in `ALL_TOKENIZERS` may be possible to construct, but they are untested and not guaranteed to work. +This collection is in a separate module since it is expensive to compute and will grow more expensive as features are added to `MazeTokenizerModular`. + +## Use Cases +In general, uses for this module are limited to development of the library and specific research studying many tokenization behaviors. +- Unit testing: + - Tokenizers to use in unit tests are sampled from `ALL_TOKENIZERS` +- Large-scale tokenizer research: + - Specific research training models on many tokenization behaviors can use `ALL_TOKENIZERS` as the maximally inclusive collection + - `ALL_TOKENIZERS` may be subsequently filtered using `MazeTokenizerModular.has_element` +For other uses, it's likely that the computational expense can be avoided by using +- `maze_tokenizer.get_all_tokenizer_hashes()` for membership checks +- `utils.all_instances` for generating smaller subsets of `MazeTokenizerModular` or `_TokenizerElement` objects + +# `EVERY_TEST_TOKENIZERS` +A collection of the tokenizers which should always be included in unit tests when test fuzzing is used. +This collection should be expanded as specific tokenizers become canonical or popular. +""" + +import functools +import multiprocessing +import random +from functools import cache +from pathlib import Path +from typing import Callable + +import frozendict +import numpy as np +from jaxtyping import Int64 +from muutils.spinner import NoOpContextManager, SpinnerContext +from tqdm import tqdm + +from maze_dataset.tokenization import ( + CoordTokenizers, + MazeTokenizerModular, + PromptSequencers, + StepTokenizers, + _TokenizerElement, +) +from maze_dataset.utils import FiniteValued, all_instances + +# Always include this as the first item in the dict `validation_funcs` whenever using `all_instances` with `MazeTokenizerModular` +MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS: frozendict.frozendict[ + type[FiniteValued], Callable[[FiniteValued], bool] +] = frozendict.frozendict( + { + _TokenizerElement: lambda x: x.is_valid(), + # Currently no need for `MazeTokenizerModular.is_valid` since that method contains no special cases not already covered by `_TokenizerElement.is_valid` + # MazeTokenizerModular: lambda x: x.is_valid(), + StepTokenizers.StepTokenizerPermutation: lambda x: len(set(x)) == len(x) + and x != (StepTokenizers.Distance(),), + } +) + + +@cache +def get_all_tokenizers() -> list[MazeTokenizerModular]: + """ + Computes a complete list of all valid tokenizers. + Warning: This is an expensive function. + """ + return list( + all_instances( + MazeTokenizerModular, + validation_funcs=MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS, + ) + ) + + +EVERY_TEST_TOKENIZERS: list[MazeTokenizerModular] = [ + MazeTokenizerModular(), + MazeTokenizerModular( + prompt_sequencer=PromptSequencers.AOTP(coord_tokenizer=CoordTokenizers.CTT()) + ), + # TODO: add more here as specific tokenizers become canonical and frequently used +] + + +@cache +def all_tokenizers_set() -> set[MazeTokenizerModular]: + """Casts ALL_TOKENIZERS to a set.""" + return set(get_all_tokenizers()) + + +@cache +def _all_tokenizers_except_every_test_tokenizers() -> list[MazeTokenizerModular]: + """Returns""" + return list(all_tokenizers_set().difference(EVERY_TEST_TOKENIZERS)) + + +def sample_all_tokenizers(n: int) -> list[MazeTokenizerModular]: + """Samples `n` tokenizers from `ALL_TOKENIZERS`.""" + return random.sample(get_all_tokenizers(), n) + + +def sample_tokenizers_for_test(n: int | None) -> list[MazeTokenizerModular]: + """Returns a sample of size `n` of unique elements from `ALL_TOKENIZERS`, + always including every element in `EVERY_TEST_TOKENIZERS`. + """ + if n is None: + return get_all_tokenizers() + + if n < len(EVERY_TEST_TOKENIZERS): + raise ValueError( + f"`n` must be at least {len(EVERY_TEST_TOKENIZERS) = } such that the sample can contain `EVERY_TEST_TOKENIZERS`." + ) + sample: list[MazeTokenizerModular] = random.sample( + _all_tokenizers_except_every_test_tokenizers(), n - len(EVERY_TEST_TOKENIZERS) + ) + sample.extend(EVERY_TEST_TOKENIZERS) + return sample + + +def save_hashes( + path: Path | None = None, + verbose: bool = False, + parallelize: bool | int = False, +) -> Int64[np.ndarray, "tokenizers"]: + """Computes, sorts, and saves the hashes of every member of `ALL_TOKENIZERS`.""" + spinner = ( + functools.partial(SpinnerContext, spinner_chars="square_dot") + if verbose + else NoOpContextManager + ) + + # get all tokenizers + with spinner(initial_value="getting all tokenizers...", update_interval=2.0): + all_tokenizers = get_all_tokenizers() + + # compute hashes + if parallelize: + n_cpus: int = ( + parallelize if int(parallelize) > 1 else multiprocessing.cpu_count() + ) + with spinner( + initial_value=f"using {n_cpus} processes to compute {len(all_tokenizers)} tokenizer hashes...", + update_interval=2.0, + ): + with multiprocessing.Pool(processes=n_cpus) as pool: + hashes_list: list[int] = list(pool.map(hash, all_tokenizers)) + + with spinner(initial_value="converting hashes to numpy array..."): + hashes_array: "Int64[np.ndarray, 'tokenizers+dupes']" = np.array( + hashes_list, dtype=np.int64 + ) + else: + with spinner( + initial_value=f"computing {len(all_tokenizers)} tokenizer hashes..." + ): + hashes_array: "Int64[np.ndarray, 'tokenizers+dupes']" = np.array( + [ + hash(obj) # uses stable hash + for obj in tqdm(all_tokenizers, disable=not verbose) + ], + dtype=np.int64, + ) + + # make sure there are no dupes + with spinner(initial_value="sorting and checking for hash collisions..."): + sorted_hashes, counts = np.unique(hashes_array, return_counts=True) + if sorted_hashes.shape[0] != hashes_array.shape[0]: + collisions = sorted_hashes[counts > 1] + raise ValueError( + f"{hashes_array.shape[0] - sorted_hashes.shape[0]} tokenizer hash collisions: {collisions}\nReport error to the developer to increase the hash size or otherwise update the tokenizer hashing algorithm." + ) + + # save and return + with spinner(initial_value="saving hashes...", update_interval=0.5): + if path is None: + path = Path(__file__).parent / "MazeTokenizerModular_hashes.npz" + np.savez_compressed( + path, + hashes=sorted_hashes, + ) + + return sorted_hashes diff --git a/maze_dataset/tokenization/maze_tokenizer.py b/maze_dataset/tokenization/maze_tokenizer.py index f49bb483..f6fa85c8 100644 --- a/maze_dataset/tokenization/maze_tokenizer.py +++ b/maze_dataset/tokenization/maze_tokenizer.py @@ -1,25 +1,61 @@ -"""TokenizationMode enum and the MazeTokenizer class""" +"""MazeTokenizerModular and the legacy TokenizationMode enum, MazeTokenizer class""" +import abc +import hashlib +import random +import warnings from enum import Enum from functools import cached_property -from typing import Callable, Iterable, Mapping, Sequence +from pathlib import Path +from typing import ( + Any, + Callable, + Iterable, + Literal, + Mapping, + Sequence, + TypedDict, + TypeVar, +) import numpy as np +from jaxtyping import Bool, Int, Int64 from muutils.json_serialize import ( SerializableDataclass, serializable_dataclass, serializable_field, ) from muutils.kappa import Kappa - -from maze_dataset.constants import SPECIAL_TOKENS, CoordTup -from maze_dataset.tokenization.util import ( +from muutils.misc import empty_sequence_if_attr_false, flatten +from muutils.misc.sequence import WhenMissing +from zanj.loading import load_item_recursive + +# from maze_dataset import SolvedMaze +from maze_dataset.constants import ( + SPECIAL_TOKENS, + VOCAB, + VOCAB_LIST, + VOCAB_TOKEN_TO_INDEX, + ConnectionArray, + ConnectionList, + Coord, + CoordTup, +) +from maze_dataset.generation import numpy_rng +from maze_dataset.maze.lattice_maze import LatticeMaze, SolvedMaze +from maze_dataset.token_utils import ( + TokenizerPendingDeprecationWarning, _coord_to_strings_indexed, _coord_to_strings_UT, + connection_list_to_adj_list, coords_to_strings, + get_cardinal_direction, + get_relative_direction, + is_connection, strings_to_coords, + tokens_between, ) -from maze_dataset.utils import WhenMissing, corner_first_ndindex +from maze_dataset.utils import corner_first_ndindex, lattice_connection_array class TokenError(ValueError): @@ -29,7 +65,7 @@ class TokenError(ValueError): class TokenizationMode(Enum): - """mode of tokenization + """LEGACY: mode of tokenization # Abbreviations: - `AOTP`: Ajacency list, Origin, Target, Path @@ -48,6 +84,9 @@ class TokenizationMode(Enum): AOTP_UT_uniform = "AOTP_UT_uniform" AOTP_CTT_indexed = "AOTP_CTT_indexed" + def to_legacy_tokenizer(self, max_grid_size: int | None = None): + return MazeTokenizer(tokenization_mode=self, max_grid_size=max_grid_size) + _NDINDEX_FUNC_MAP: dict[ TokenizationMode, Callable[[int], Iterable[tuple[int, ...]]] @@ -64,6 +103,27 @@ def is_UT(tokenization_mode: TokenizationMode) -> bool: ) +def get_tokens_up_to_path_start( + tokens: list[str], + include_start_coord: bool = True, + tokenization_mode: TokenizationMode = TokenizationMode.AOTP_UT_uniform, +) -> list[str]: + warnings.warn( + "`maze_tokenizer.get_tokens_up_to_path_start` will be deprecated for a `MazeTokenizerModular`-compatible function in a future release.", + TokenizerPendingDeprecationWarning, + ) + path_start_idx: int = tokens.index(SPECIAL_TOKENS.PATH_START) + 1 + if include_start_coord: + if is_UT(tokenization_mode): + return tokens[: path_start_idx + 1] + elif tokenization_mode == TokenizationMode.AOTP_CTT_indexed: + return tokens[: path_start_idx + 5] + else: + raise ValueError(f"Invalid tokenization mode: {tokenization_mode}") + else: + return tokens[:path_start_idx] + + _MAZETOKENIZER_PROPERTIES_TO_SERIALIZE: list[str] = [ "name", "max_grid_size", @@ -78,7 +138,7 @@ def is_UT(tokenization_mode: TokenizationMode) -> bool: properties_to_serialize=_MAZETOKENIZER_PROPERTIES_TO_SERIALIZE, kw_only=True ) class MazeTokenizer(SerializableDataclass): - """Tokenizer for mazes + """LEGACY: Tokenizer for mazes # Parameters: - `tokenization_mode: TokenizationMode` @@ -388,3 +448,1724 @@ def clear_cache(self): delattr(self, name) except AttributeError as e: pass + + +@serializable_dataclass(frozen=True, kw_only=True) +class _TokenizerElement(SerializableDataclass, abc.ABC): + """Superclass for tokenizer elements. + Subclasses contain modular functionality for maze tokenization. + + # Development + Due to the functionality of `ALL_TOKENIZERS`, `_TokenizerElement` subclasses may only contain fields of type `utils.FiniteValued`. + Implementing a subclass with an `int` or `float`-typed field, for example, is not supported. + In the event that adding such fields is deemed necessary, `ALL_TOKENIZERS` must be updated. + """ + + @staticmethod + def _stringify(k: str, v: Any): + if isinstance(v, bool): + return f"{k}={str(v)[0]}" + if isinstance(v, _TokenizerElement): + return v.name + if isinstance(v, tuple): + return f"{k}={''.join(['(', *[str(x)+', ' for x in v], ')'])}" + else: + return f"{k}={v}" + + @property + def name(self) -> str: + members_str: str = ", ".join( + [self._stringify(k, v) for k, v in self.__dict__.items() if k != "_type_"] + ) + output: str = f"{type(self).__name__}({members_str})" + if "." in output and output.index("(") > output.index("."): + return "".join(output.split(".")[1:]) + else: + return output + + def __str__(self): + return self.name + + def __init_subclass__(cls, **kwargs): + """ + Hack: dataclass hashes don't include the class itself in the hash function inputs. + This causes dataclasses with identical fields but different types to hash identically. + This hack circumvents this by adding a slightly hidden field to every subclass with a value of `repr(cls)`. + To maintain compatibility with `all_instances`, the static type of the new field can only have 1 possible value. + So we type it as a singleton `Literal` type. + muutils 0.6.1 doesn't support `Literal` type validation, so `assert_type=False`. + Ignore Pylance complaining about the arg to `Literal` being an expression. + """ + super().__init_subclass__(**kwargs) + cls._type_ = serializable_field( + init=True, repr=False, default=repr(cls), assert_type=False + ) + cls.__annotations__["_type_"] = Literal[repr(cls)] # type: ignore + + def __hash__(self): + "Stable hash to identify unique `MazeTokenizerModular` instances. uses name" + return int.from_bytes( + hashlib.blake2b(self.name.encode("utf-8")).digest(), + byteorder="big", + ) + + @classmethod + def _level_one_subclass(cls) -> type["_TokenizerElement"]: + """Returns the immediate subclass of `_TokenizerElement` of which `cls` is an instance.""" + return ( + set(cls.__mro__).intersection(set(_TokenizerElement.__subclasses__())).pop() + ) + + def tokenizer_elements(self, deep: bool = True) -> list["_TokenizerElement"]: + """ + Returns a list of all `_TokenizerElement` instances contained in the subtree. + Currently only detects `_TokenizerElement` instances which are either direct attributes of another instance or + which sit inside a `tuple` without further nesting. + + # Parameters + - `deep: bool`: Whether to return elements nested arbitrarily deeply or just a single layer. + """ + if not any(type(el) == tuple for el in self.__dict__.values()): + return list( + flatten( + [ + [el] + el.tokenizer_elements() + for el in self.__dict__.values() + if isinstance(el, _TokenizerElement) + ] + ) + if deep + else filter( + lambda x: isinstance(x, _TokenizerElement), self.__dict__.values() + ) + ) + else: + non_tuple_elems: list[_TokenizerElement] = list( + flatten( + [ + [el] + el.tokenizer_elements() + for el in self.__dict__.values() + if isinstance(el, _TokenizerElement) + ] + if deep + else filter( + lambda x: isinstance(x, _TokenizerElement), + self.__dict__.values(), + ) + ) + ) + tuple_elems: list[_TokenizerElement] = list( + flatten( + [ + ( + [ + [tup_el] + tup_el.tokenizer_elements() + for tup_el in el + if isinstance(tup_el, _TokenizerElement) + ] + if deep + else filter(lambda x: isinstance(x, _TokenizerElement), el) + ) + for el in self.__dict__.values() + if isinstance(el, tuple) + ] + ) + ) + non_tuple_elems.extend(tuple_elems) + return non_tuple_elems + + def tokenizer_element_tree(self, depth: int = 0, abstract: bool = False) -> str: + """ + Returns a string representation of the tree of tokenizer elements contained in `self`. + + # Parameters + - `depth: int`: Current depth in the tree. Used internally for recursion, no need to specify. + - `abstract: bool`: Whether to print the name of the abstract base class or the concrete class for each `_TokenizerElement` instance. + """ + name: str = "\t" * depth + ( + type(self).__name__ + if not abstract + else type(self)._level_one_subclass().__name__ + ) + return ( + name + + "\n" + + "".join( + el.tokenizer_element_tree(depth + 1, abstract) + for el in self.tokenizer_elements(deep=False) + ) + ) + + def tokenizer_element_dict(self) -> dict: + """ + Returns a dictionary representation of the tree of tokenizer elements contained in `self`. + """ + return { + type(self).__name__: { + key: ( + val.tokenizer_element_dict() + if isinstance(val, _TokenizerElement) + else ( + val + if not isinstance(val, tuple) + else [ + ( + el.tokenizer_element_dict() + if isinstance(el, _TokenizerElement) + else el + ) + for el in val + ] + ) + ) + for key, val in self.__dict__.items() + if key != "_type_" + } + } + + @classmethod + @abc.abstractmethod + def attribute_key(cls) -> str: + """Returns the binding used in `MazeTokenizerModular` for that type of `_TokenizerElement`.""" + raise NotImplementedError + + def to_tokens(self, *args, **kwargs) -> list[str]: + """Converts a maze element into a list of tokens. + Not all `_TokenizerElement` subclasses produce tokens, so this is not an abstract method. + Those subclasses which do produce tokens should override this method. + """ + raise NotImplementedError + + @abc.abstractmethod + def is_valid(self) -> bool: + """Returns if `self` contains data members capable of producing an overall valid `MazeTokenizerModular`. + Some `_TokenizerElement` instances may be created which are not useful despite obeying data member type hints. + `is_valid` allows for more precise detection of invalid `_TokenizerElement`s beyond type hinting alone. + If type hints are sufficient to constrain the possible instances of some subclass, then this method may simply `return True` for that subclass. + + # Types of Invalidity + In nontrivial implementations of this method, each conditional clause should contain a comment classifying the reason for invalidity and one of the types below. + Invalidity types, in ascending order of invalidity: + - Uninteresting: These tokenizers might be used to train functional models, but the schemes are not interesting to study. + E.g., `_TokenizerElement`s which are strictly worse than some alternative. + - Duplicate: These tokenizers have identical tokenization behavior as some other valid tokenizers. + - Untrainable: Training functional models using these tokenizers would be (nearly) impossible. + - Erroneous: These tokenizers might raise exceptions during use. + + # Development + `is_invalid` is implemented to always return `True` in some abstract classes where all currently possible subclass instances are valid. + When adding new subclasses or data members, the developer should check if any such blanket statement of validity still holds and update it as neccesary. + + ## Nesting + In general, when implementing this method, there is no need to recursively call `is_valid` on nested `_TokenizerElement`s contained in the class. + In other words, failures of `is_valid` need not bubble up to the top of the nested `_TokenizerElement` tree. + `MazeTokenizerModular.is_valid` calls `is_valid` on each of its `_TokenizerElement`s individually, so failure at any level will be detected. + + ## Types of Invalidity + If it's judged to be useful, the types of invalidity could be implemented with an Enum or similar rather than only living in comments. + This could be used to create more or less stringent filters on the valid `_TokenizerElement` instances. + """ + raise NotImplementedError + + +T = TypeVar("T", bound=_TokenizerElement) + + +def mark_as_unsupported(is_valid: Callable[[T], bool], *args) -> T: + """mark a _TokenizerElement as unsupported. + + Classes marked with this decorator won't show up in ALL_TOKENIZERS and thus wont be tested. + The classes marked in release 1.0.0 did work reliably before being marked, but they can't be instantiated since the decorator adds an abstract method. + The decorator exists to prune the space of tokenizers returned by `all_instances` both for testing and usage. + Previously, the space was too large, resulting in impractical runtimes. + These decorators could be removed in future releases to expand the space of possible tokenizers. + """ + + def wrapper(cls): + cls.is_valid = is_valid + return cls + + return wrapper + + +class __TokenizerElementNamespace(abc.ABC): + """ABC for namespaces + + # Properties + - key: The binding used in `MazeTokenizerModular` for instances of the classes contained within that `__TokenizerElementNamespace`. + """ + + key: str = NotImplementedError + + +def _load_tokenizer_element( + data: dict[str, Any], namespace: type[__TokenizerElementNamespace] +) -> _TokenizerElement: + """Loads a `TokenizerElement` stored via zanj.""" + key: str = namespace.key + format: str = data[key]["__format__"] + cls_name: str = format.split("(")[0] + cls: type[_TokenizerElement] = getattr(namespace, cls_name) + kwargs: dict[str, Any] = { + k: load_item_recursive(data[key][k], tuple()) for k, v in data[key].items() + } + if "__format__" in kwargs: + kwargs.pop("__format__") + return cls(**kwargs) + + +class CoordTokenizers(__TokenizerElementNamespace): + key = "coord_tokenizer" + + @serializable_dataclass(frozen=True, kw_only=True) + class _CoordTokenizer(_TokenizerElement, abc.ABC): + """ + Superclass for classes which tokenize singular coords in a maze. + """ + + @abc.abstractmethod + def to_tokens(self, coord: Coord | CoordTup) -> list[str]: + pass + + @classmethod + def attribute_key(cls) -> str: + return CoordTokenizers.key + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + @serializable_dataclass(frozen=True, kw_only=True) + class UT(_CoordTokenizer): + """Unique token coordinate tokenizer.""" + + def to_tokens(self, coord: Coord | CoordTup) -> list[str]: + return ["".join(["(", str(coord[0]), ",", str(coord[1]), ")"])] + + @serializable_dataclass(frozen=True, kw_only=True) + class CTT(_CoordTokenizer): + """Coordinate tuple tokenizer + + # Parameters + - `pre`: Whether all coords include an integral preceding delimiter token + - `intra`: Whether all coords include a delimiter token between coordinates + - `post`: Whether all coords include an integral following delimiter token + """ + + pre: bool = serializable_field(default=True) + intra: bool = serializable_field(default=True) + post: bool = serializable_field(default=True) + # Implement methods + + def to_tokens(self, coord: Coord | CoordTup) -> list[str]: + return [ + *empty_sequence_if_attr_false([VOCAB.COORD_PRE], self, "pre"), + str(coord[0]), + *empty_sequence_if_attr_false([VOCAB.COORD_INTRA], self, "intra"), + str(coord[1]), + *empty_sequence_if_attr_false([VOCAB.COORD_POST], self, "post"), + ] + + +class EdgeGroupings(__TokenizerElementNamespace): + """Namespace for `EdgeGrouping` subclass hierarchy used by `AdjListTokenizer`.""" + + key = "edge_grouping" + + class _GroupingTokenParams(TypedDict): + """A uniform private hyperparameter interface used by `AdjListTokenizer`.""" + + connection_token_ordinal: Literal[0, 1, 2] + intra: bool + grouped: bool + + @serializable_dataclass(frozen=True, kw_only=True) + class _EdgeGrouping(_TokenizerElement, abc.ABC): + """Specifies if/how multiple coord-coord connections are grouped together in a token subsequence called a edge grouping.""" + + @classmethod + def attribute_key(cls) -> str: + return EdgeGroupings.key + + def is_valid(self) -> bool: + return True + + @abc.abstractmethod + def _group_edges(self, edges: ConnectionArray) -> Sequence[ConnectionArray]: + """Divides a ConnectionArray into groups of edges. + Shuffles/sequences within each group if applicable. + """ + pass + + @abc.abstractmethod + def _token_params(self) -> "EdgeGroupings._GroupingTokenParams": + """Returns the tok.nization hyperparameters necessary for an `AdjListTokenizer` to tokenize. + + These hyperparameters are not used by `_EdgeGrouping` internally. + They are located in `_EdgeGrouping` rather than in `AdjListTokenizer` + since the hyperparameter space is a function of the `_EdgeGrouping` subclass. + This function resolves the `_EdgeGrouping` hyperparameter space which is non-uniform across subclasses + into a uniform private interface used by `AdjListTokenizer`. + """ + pass + + @serializable_dataclass(frozen=True, kw_only=True) + class Ungrouped(_EdgeGrouping): + """No grouping occurs, each edge is tokenized individually. + + # Parameters + - `connection_token_ordinal`: At which index in the edge tokenization the connector (or wall) token appears. + Edge tokenizations contain 3 parts: a leading coord, a connector (or wall) token, and either a second coord or cardinal direction tokenization. + """ + + connection_token_ordinal: Literal[0, 1, 2] = serializable_field( + default=1, assert_type=False + ) + + def _token_params(self) -> "EdgeGroupings._GroupingTokenParams": + return EdgeGroupings._GroupingTokenParams( + connection_token_ordinal=self.connection_token_ordinal, + intra=False, + grouped=False, + ) + + def _group_edges(self, edges: ConnectionList) -> Sequence[ConnectionList]: + return np.expand_dims(edges, 1) + + @serializable_dataclass(frozen=True, kw_only=True) + @mark_as_unsupported(lambda self_: False) + class ByLeadingCoord(_EdgeGrouping): + """All edges with the same leading coord are grouped together. + + # Parameters + - `intra`: Whether all edge groupings include a delimiter token between individual edge representations. + Note that each edge representation will already always include a connector token (`VOCAB.CONNECTOR`, or possibly `) + - `shuffle_group`: Whether the sequence of edges within the group should be shuffled or appear in a fixed order. + If false, the fixed order is lexicographical by (row, col). + In effect, lexicographical sorting sorts edges by their cardinal direction in the sequence NORTH, WEST, EAST, SOUTH, where the directions indicate the position of the trailing coord relative to the leading coord. + - `connection_token_ordinal`: At which index in token sequence representing a single edge the connector (or wall) token appears. + Edge tokenizations contain 2 parts: a connector (or wall) token and a coord or cardinal tokenization. + """ + + intra: bool = serializable_field(default=True) + shuffle_group: bool = serializable_field(default=True) + connection_token_ordinal: Literal[0, 1] = serializable_field( + default=0, assert_type=False + ) + + def _token_params(self) -> "EdgeGroupings._GroupingTokenParams": + return EdgeGroupings._GroupingTokenParams( + connection_token_ordinal=self.connection_token_ordinal, + intra=self.intra, + grouped=True, + ) + + def _group_edges(self, edges: ConnectionArray) -> Sequence[ConnectionArray]: + # Adapted from: https://stackoverflow.com/questions/38013778/is-there-any-numpy-group-by-function + index_array: Int[np.ndarray, "sort_indices=edges"] = np.lexsort( + (edges[:, 1, 1], edges[:, 1, 0], edges[:, 0, 1], edges[:, 0, 0]) + ) + sorted_edges: ConnectionArray = edges[index_array, ...] + groups: list[ConnectionArray] = np.split( + sorted_edges, + np.unique(sorted_edges[:, 0, :], return_index=True, axis=0)[1][1:], + ) + if self.shuffle_group: + [numpy_rng.shuffle(g, axis=0) for g in groups] + return groups + + +class EdgePermuters(__TokenizerElementNamespace): + """Namespace for `EdgePermuter` subclass hierarchy used by `AdjListTokenizer`.""" + + key = "edge_permuter" + + @serializable_dataclass(frozen=True, kw_only=True) + class _EdgePermuter(_TokenizerElement, abc.ABC): + """Specifies how to sequence the two coords that encode a lattice edge.""" + + @classmethod + def attribute_key(cls) -> str: + return EdgePermuters.key + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + @staticmethod + @abc.abstractmethod + def _permute(lattice_edges: ConnectionArray) -> ConnectionArray: + """ + Executes a permutation. + Warning: Caller should be aware that `lattice_edges` may be modified in-place depending on the subclass's implementation. + + # Parameters + - `lattice_edges`: Array of lattice edges. + The two coords in shape[1] must be adjacent in the lattice. + + # Returns + - Array of lattice edges with entries along shape[1] systematically permuted. + - shape[0] of the returned array is NOT guaranteed to match `lattice_edges.shape[1]`. + """ + pass + + @serializable_dataclass(frozen=True, kw_only=True) + class SortedCoords(_EdgePermuter): + """returns a sorted representation. useful for checking consistency""" + + @staticmethod + def _permute(lattice_edges: ConnectionArray) -> ConnectionArray: + return lattice_edges[ + np.lexsort( + ( + lattice_edges[:, 1, 1], + lattice_edges[:, 1, 0], + lattice_edges[:, 0, 1], + lattice_edges[:, 0, 0], + ) + ), + ..., + ] + + @serializable_dataclass(frozen=True, kw_only=True) + class RandomCoords(_EdgePermuter): + """Permutes each edge randomly.""" + + @staticmethod + def _permute(lattice_edges: ConnectionArray) -> ConnectionArray: + numpy_rng.permuted(lattice_edges, axis=1, out=lattice_edges) + return lattice_edges + + @serializable_dataclass(frozen=True, kw_only=True) + class BothCoords(_EdgePermuter): + """Includes both possible permutations of every edge in the output. + Since input ConnectionList has only 1 instance of each edge, + a call to `BothCoords._permute` will modify `lattice_edges` in-place, doubling `shape[0]`. + """ + + @staticmethod + def _permute(lattice_edges: ConnectionArray) -> ConnectionArray: + return np.append(lattice_edges, np.flip(lattice_edges, axis=1), axis=0) + + +class EdgeSubsets(__TokenizerElementNamespace): + """ + Namespace for `EdgeSubset` subclass hierarchy used by `AdjListTokenizer`. + """ + + key = "edge_subset" + + @serializable_dataclass(frozen=True, kw_only=True) + class _EdgeSubset(_TokenizerElement, abc.ABC): + """ + Component of an `AdjListTokenizers._AdjListTokenizer` which specifies the subset of lattice edges to be tokenized. + """ + + @classmethod + def attribute_key(cls) -> str: + return EdgeSubsets.key + + def is_valid(self) -> bool: + return True + + @abc.abstractmethod + def _get_edges(self, maze: LatticeMaze) -> ConnectionArray: + """ + Returns the set of lattice edges to be tokenized. + """ + pass + + @serializable_dataclass(frozen=True, kw_only=True) + class AllLatticeEdges(_EdgeSubset): + """ + All 2n**2-2n edges of the lattice are tokenized. + If a wall exists on that edge, the edge is tokenized in the same manner, using `VOCAB.ADJLIST_WALL` in place of `VOCAB.CONNECTOR`. + """ + + def _get_edges(self, maze: LatticeMaze) -> ConnectionArray: + return lattice_connection_array(maze.grid_n) + + @serializable_dataclass(frozen=True, kw_only=True) + class ConnectionEdges(_EdgeSubset): + """ + Only edges which contain a connection are tokenized. + Alternatively, only edges which contain a wall are tokenized. + + # Parameters + - `walls`: Whether wall edges or connection edges are tokenized. + If true, `VOCAB.ADJLIST_WALL` is used in place of `VOCAB.CONNECTOR`. + """ + + walls: bool = serializable_field(default=False) + + def _get_edges(self, maze: LatticeMaze) -> ConnectionArray: + conn_list: ConnectionList = maze.connection_list + if self.walls: + conn_list = np.logical_not(conn_list) + conn_list[0, -1, :] = False + conn_list[1, :, -1] = False + return connection_list_to_adj_list( + conn_list, shuffle_d0=False, shuffle_d1=False + ) + + +class AdjListTokenizers(__TokenizerElementNamespace): + key = "adj_list_tokenizer" + + @serializable_dataclass(frozen=True, kw_only=True) + @mark_as_unsupported(lambda self_: self_.pre is False) + class _AdjListTokenizer(_TokenizerElement, abc.ABC): + """ + Specifies how the adjacency list is tokenized. + Tokenization behavior is decomposed into specification of edge subsets, groupings, and permutations. + See documentation of `EdgeSubset` and `EdgeGrouping` classes for more details. + + # Parameters + - `pre`: Whether all edge groupings include a preceding delimiter token + - `post`: Whether all edge groupings include a following delimiter token + - `shuffle_d0`: Specifies how to sequence the edge groupings. + If true, groupings are shuffled randomly. If false, groupings are sorted by the leading coord of each group. + - `edge_grouping`: Specifies if/how multiple coord-coord connections are grouped together in a token subsequence called an edge grouping. + - `edge_subset`: Specifies the subset of lattice edges to be tokenized. + - `edge_permuter`: Specifies, in each edge tokenization, which coord either: + 1. Appears first in the tokenization, for `AdjListCoord`. + 2. Is tokenized directly as a coord, for `AdjListCardinal`. + - `shuffle`: For each edge, the leading coord is selected randomly. + - `all`: Each edge appears twice in the tokenization, appearing with both leading coords. + - `evens`, `odds`: The leading coord is the one belonging to that coord subset. See `EdgeSubsets.ChessboardSublattice` for details. + """ + + pre: bool = serializable_field(default=False, assert_type=False) + post: bool = serializable_field(default=True) + shuffle_d0: bool = serializable_field(default=True) + edge_grouping: EdgeGroupings._EdgeGrouping = serializable_field( + default=EdgeGroupings.Ungrouped(), + loading_fn=lambda x: _load_tokenizer_element(x, EdgeGroupings), + ) + edge_subset: EdgeSubsets._EdgeSubset = serializable_field( + default=EdgeSubsets.ConnectionEdges(), + loading_fn=lambda x: _load_tokenizer_element(x, EdgeSubsets), + ) + edge_permuter: EdgePermuters._EdgePermuter = serializable_field( + default=EdgePermuters.RandomCoords(), + loading_fn=lambda x: _load_tokenizer_element(x, EdgePermuters), + ) + + @classmethod + def attribute_key(cls) -> str: + return AdjListTokenizers.key + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + @abc.abstractmethod + def _tokenization_callables( + self, + edges: ConnectionArray, + is_conn: Bool[np.ndarray, "edges"], + coord_tokenizer: CoordTokenizers._CoordTokenizer, + *args, + **kwargs, + ): + """ + Returns a sequence of callables which take an index in `edges` and return parts of that edge tokenization. + + # Returns + - `[0]`: leading coord tokens + - `[1]`: connector tokens + - `[2]`: trailing coord tokens + """ + pass + + def _tokenize_edge_grouping( + self, + edges: ConnectionArray, + maze: LatticeMaze, + coord_tokenizer: CoordTokenizers._CoordTokenizer, + group_params: EdgeGroupings._GroupingTokenParams, + ) -> Sequence[str]: + """ + Tokenizes a single edge grouping. + """ + cxn_ord: int = group_params["connection_token_ordinal"] + is_conn: Bool[np.ndarray, "edges"] = is_connection( + edges, maze.connection_list + ) + tokenize_callables = self._tokenization_callables( + edges, is_conn, coord_tokenizer + ) + + if group_params["grouped"]: + # If grouped + callable_permutation: list[int] = [1, 2] if cxn_ord == 0 else [2, 1] + repeated_callables = [ + tokenize_callables[i] for i in callable_permutation + ] + return flatten( + [ + tokenize_callables[0](0), + [ + [ + *[ + tok_callable(i) + for tok_callable in repeated_callables + ], + *( + (VOCAB.ADJLIST_INTRA,) + if group_params["intra"] + else () + ), + ] + for i in range(edges.shape[0]) + ], + ] + ) + else: + # If ungrouped + callable_permutation = [0, 2] + callable_permutation.insert(cxn_ord, 1) + tokenize_callables = [ + tokenize_callables[i] for i in callable_permutation + ] + + return flatten( + [ + [ + [ + *[ + tok_callable(i) + for tok_callable in tokenize_callables + ], + *empty_sequence_if_attr_false( + (VOCAB.ADJLIST_INTRA,), group_params, "intra" + ), + ] + for i in range(edges.shape[0]) + ] + ] + ) + + def to_tokens( + self, maze: LatticeMaze, coord_tokenizer: CoordTokenizers._CoordTokenizer + ) -> list[str]: + # Get the set of edges to be tokenized + edges: ConnectionArray = self.edge_subset._get_edges(maze) + # Systematically permute the leading coord of each edge + edges: ConnectionArray = self.edge_permuter._permute(edges) + group_params: EdgeGroupings._GroupingTokenParams = ( + self.edge_grouping._token_params() + ) + # then, we need to group the edges + groups: Sequence[ConnectionArray] = self.edge_grouping._group_edges(edges) + # shuffle the groups if specified + if self.shuffle_d0: + if isinstance(groups, np.ndarray): + numpy_rng.shuffle(groups, axis=0) + elif isinstance(groups, list): + random.shuffle(groups) + else: + raise TypeError( + f"`groups` is an unexpected type {type(groups)}. Only types `list` and `np.ndarray` are currently supported." + ) + # Tokenize each group with optional delimiters + tokens: list[str] = list( + flatten( + [ + [ + *empty_sequence_if_attr_false( + (VOCAB.ADJLIST_PRE,), self, "pre" + ), + *self._tokenize_edge_grouping( + group, maze, coord_tokenizer, group_params + ), + *empty_sequence_if_attr_false( + (VOCAB.ADJACENCY_ENDLINE,), self, "post" + ), + ] + for group in groups + ] + ) + ) + return tokens + + @serializable_dataclass(frozen=True, kw_only=True) + class AdjListCoord(_AdjListTokenizer): + """Represents an edge group as tokens for the leading coord followed by coord tokens for the other group members.""" + + edge_permuter: EdgePermuters._EdgePermuter = serializable_field( + default=EdgePermuters.RandomCoords(), + loading_fn=lambda x: _load_tokenizer_element(x, EdgePermuters), + ) + + def _tokenization_callables( + self, + edges: ConnectionArray, + is_conn: Bool[np.ndarray, "edges"], + coord_tokenizer: CoordTokenizers._CoordTokenizer, + *args, + **kwargs, + ): + # Map from `is_conn` to the tokens which represent connections and walls + conn_token_map: dict[bool, str] = { + True: VOCAB.CONNECTOR, + False: VOCAB.ADJLIST_WALL, + } + return [ + lambda i: coord_tokenizer.to_tokens(edges[i, 0]), + lambda i: conn_token_map[is_conn[i]], + lambda i: coord_tokenizer.to_tokens(edges[i, 1]), + ] + + @serializable_dataclass(frozen=True, kw_only=True) + class AdjListCardinal(_AdjListTokenizer): + """Represents an edge group as coord tokens for the leading coord and cardinal tokens relative to the leading coord for the other group members. + + # Parameters + - `coord_first`: Whether the leading coord token(s) should come before or after the sequence of cardinal tokens. + """ + + edge_permuter: EdgePermuters._EdgePermuter = serializable_field( + default=EdgePermuters.BothCoords(), + loading_fn=lambda x: _load_tokenizer_element(x, EdgePermuters), + ) + + def _tokenization_callables( + self, + edges: ConnectionArray, + is_conn: Bool[np.ndarray, "edges"], + coord_tokenizer: CoordTokenizers._CoordTokenizer, + *args, + **kwargs, + ): + # Map from `is_conn` to the tokens which represent connections and walls + conn_token_map: dict[bool, str] = { + True: VOCAB.CONNECTOR, + False: VOCAB.ADJLIST_WALL, + } + return [ + lambda i: coord_tokenizer.to_tokens(edges[i, 0]), + lambda i: conn_token_map[is_conn[i]], + lambda i: get_cardinal_direction(edges[i]), + ] + + +class TargetTokenizers(__TokenizerElementNamespace): + key = "target_tokenizer" + + @serializable_dataclass(frozen=True, kw_only=True) + class _TargetTokenizer(_TokenizerElement, abc.ABC): + """Superclass of tokenizers for maze targets.""" + + @abc.abstractmethod + def to_tokens( + self, + targets: Sequence[Coord], + coord_tokenizer: CoordTokenizers._CoordTokenizer, + ) -> list[str]: + """Returns tokens representing the target.""" + pass + + @classmethod + def attribute_key(cls) -> str: + return TargetTokenizers.key + + @serializable_dataclass(frozen=True, kw_only=True) + class Unlabeled(_TargetTokenizer): + """Targets are simply listed as coord tokens. + - `post`: Whether all coords include an integral following delimiter token + """ + + post: bool = serializable_field(default=False) + + def to_tokens( + self, + targets: Sequence[Coord], + coord_tokenizer: CoordTokenizers._CoordTokenizer, + ) -> list[str]: + return list( + flatten( + [ + [ + *coord_tokenizer.to_tokens(target), + *empty_sequence_if_attr_false( + [VOCAB.TARGET_POST], self, "post" + ), + ] + for target in targets + ] + ) + ) + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + +class StepSizes(__TokenizerElementNamespace): + key = "step_size" + + @serializable_dataclass(frozen=True, kw_only=True) + class _StepSize(_TokenizerElement, abc.ABC): + """ + Specifies which coords in `maze.solution` are used to represent the path. + """ + + @classmethod + def attribute_key(cls) -> str: + return StepSizes.key + + @abc.abstractmethod # TODO: make this a static/class method, allowing ForksAndStraightaways to skip object construction at every call + def _step_single_indices(self, maze: SolvedMaze) -> list[int]: + """Returns the indices of `maze.solution` corresponding to the steps to be tokenized.""" + raise NotImplementedError( + "Subclasses must implement `StepSize.step_indices." + ) + + def step_start_end_indices(self, maze) -> list[tuple[int, int]]: + """Returns steps as tuples of starting and ending positions for each step.""" + indices: list[int] = self._step_single_indices(maze) + return [(start, end) for start, end in zip(indices[:-1], indices[1:])] + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + @serializable_dataclass(frozen=True, kw_only=True) + class Singles(_StepSize): + """ + Every coord in `maze.solution` is represented. + Legacy tokenizers all use this behavior. + """ + + def _step_single_indices(self, maze: SolvedMaze) -> list[int]: + """Returns the indices of `maze.solution` corresponding to the steps to be tokenized.""" + return list(range(maze.solution.shape[0])) + + @serializable_dataclass(frozen=True, kw_only=True) + @mark_as_unsupported(lambda self_: False) + class Straightaways(_StepSize): + """ + Only coords where the path turns are represented in the path. + I.e., the path is represented as a sequence of straightaways, + specified by the coords at the turns. + """ + + def _step_single_indices(self, maze: SolvedMaze) -> list[int]: + """Returns the indices of `maze.solution` corresponding to the steps to be tokenized.""" + last_turn_coord: Coord = maze.solution[0, ...] + indices: list[int] = [0] + for i, coord in enumerate(maze.solution): + if coord[0] != last_turn_coord[0] and coord[1] != last_turn_coord[1]: + indices.append(i - 1) + last_turn_coord = maze.solution[i - 1, ...] + indices.append(i) + return indices + + @serializable_dataclass(frozen=True, kw_only=True) + class Forks(_StepSize): + """ + Only coords at forks, where the path has >=2 options for the next step are included. + Excludes the option of backtracking. + The starting and ending coords are always included. + """ + + def _step_single_indices(self, maze: SolvedMaze) -> list[int]: + """Returns the indices of `maze.solution` corresponding to the steps to be tokenized.""" + return maze.get_solution_forking_points(always_include_endpoints=True)[0] + + @serializable_dataclass(frozen=True, kw_only=True) + @mark_as_unsupported(lambda self_: False) + class ForksAndStraightaways(_StepSize): + """ + Includes the union of the coords included by `Forks` and `Straightaways`. + See documentation for those classes for details. + """ + + def _step_single_indices(self, maze: SolvedMaze) -> list[int]: + """Returns the indices of `maze.solution` corresponding to the steps to be tokenized.""" + return list( + np.unique( + np.concatenate( + ( + StepSizes.Straightaways()._step_single_indices(maze), + StepSizes.Forks()._step_single_indices(maze), + ) + ) + ) + ) + + +class StepTokenizers(__TokenizerElementNamespace): + key = "step_tokenizers" + + @serializable_dataclass(frozen=True, kw_only=True) + class _StepTokenizer(_TokenizerElement, abc.ABC): + """ + Specifies how a single step (as specified by an instance of `_StepSize`) is tokenized. + """ + + @classmethod + def attribute_key(cls) -> str: + return StepTokenizers.key + + @abc.abstractmethod + def to_tokens( + self, + maze: SolvedMaze, + start_index: int, + end_index: int, + **kwargs, + ) -> list[str]: + """Tokenizes a single step in the solution. + + # Parameters + - `maze`: Maze to be tokenized + - `start_index`: The index of the Coord in `maze.solution` at which the current step starts + - `end_index`: The index of the Coord in `maze.solution` at which the current step ends + """ + raise NotImplementedError( + "Subclasses must implement `StepTokenizer.to_tokens." + ) + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + @serializable_dataclass(frozen=True, kw_only=True) + class Coord(_StepTokenizer): + """ + A direct tokenization of the end position coord represents the step. + """ + + def to_tokens( + self, + maze: SolvedMaze, + start_index: int, + end_index: int, + coord_tokenizer: CoordTokenizers._CoordTokenizer, + ) -> list[str]: + return coord_tokenizer.to_tokens(maze.solution[end_index, ...]) + + @serializable_dataclass(frozen=True, kw_only=True) + class Cardinal(_StepTokenizer): + """ + A step is tokenized with a cardinal direction token. + It is the direction of the step from the starting position along the solution. + """ + + def to_tokens( + self, maze: SolvedMaze, start_index: int, end_index: int, **kwargs + ) -> list[str]: + return [ + get_cardinal_direction(maze.solution[start_index : start_index + 2]) + ] + + @serializable_dataclass(frozen=True, kw_only=True) + class Relative(_StepTokenizer): + """Tokenizes a solution step using relative first-person directions (right, left, forward, etc.). + To simplify the indeterminacy, at the start of a solution the "agent" solving the maze is assumed to be facing NORTH. + Similarly to `Cardinal`, the direction is that of the step from the starting position. + """ + + def to_tokens( + self, maze: SolvedMaze, start_index: int, end_index: int, **kwargs + ) -> list[str]: + if start_index == 0: + start = maze.solution[0] + previous = start + np.array([1, 0]) + return [ + get_relative_direction( + np.concatenate( + ( + np.expand_dims(previous, 0), + maze.solution[start_index : start_index + 2], + ), + axis=0, + ) + ) + ] + return [ + get_relative_direction(maze.solution[start_index - 1 : start_index + 2]) + ] + + @serializable_dataclass(frozen=True, kw_only=True) + class Distance(_StepTokenizer): + """ + A count of the number of individual steps from the starting point to the end point. + Contains no information about directionality, only the distance traveled in the step. + `Distance` must be combined with at least one other `_StepTokenizer` in a `StepTokenizerPermutation`. + This constraint is enforced in `_PathTokenizer.is_valid`. + """ + + def to_tokens( + self, maze: SolvedMaze, start_index: int, end_index: int, **kwargs + ) -> list[str]: + d: int = end_index - start_index + return [getattr(VOCAB, f"I_{d:03}")] + + """ + `StepTokenizerPermutation` + A sequence of unique `_StepTokenizer`s. + This type exists mostly just for the clarity and convenience of `_PathTokenizer` code. + """ + StepTokenizerPermutation: type = ( + tuple[_StepTokenizer] + | tuple[_StepTokenizer, _StepTokenizer] + | tuple[_StepTokenizer, _StepTokenizer, _StepTokenizer] + | tuple[_StepTokenizer, _StepTokenizer, _StepTokenizer, _StepTokenizer] + ) + + +class PathTokenizers(__TokenizerElementNamespace): + key = "path_tokenizer" + + @serializable_dataclass(frozen=True, kw_only=True) + class _PathTokenizer(_TokenizerElement, abc.ABC): + """Superclass of tokenizers for maze solution paths.""" + + @abc.abstractmethod + def to_tokens( + self, maze: SolvedMaze, coord_tokenizer: CoordTokenizers._CoordTokenizer + ) -> list[str]: + """Returns tokens representing the solution path.""" + pass + + @classmethod + def attribute_key(cls) -> str: + return PathTokenizers.key + + @serializable_dataclass(frozen=True, kw_only=True) + class StepSequence(_PathTokenizer, abc.ABC): + """Any `PathTokenizer` where the tokenization may be assembled from token subsequences, each of which represents a step along the path. + Allows for a sequence of leading and trailing tokens which don't fit the step pattern. + + # Parameters + - `step_size`: Selects the size of a single step in the sequence + - `step_tokenizers`: Selects the combination and permutation of tokens + - `pre`: Whether all steps include an integral preceding delimiter token + - `intra`: Whether all steps include a delimiter token after each individual `_StepTokenizer` tokenization. + - `post`: Whether all steps include an integral following delimiter token + """ + + step_size: StepSizes._StepSize = serializable_field( + default=StepSizes.Singles(), + loading_fn=lambda x: _load_tokenizer_element(x, StepSizes), + ) + step_tokenizers: StepTokenizers.StepTokenizerPermutation = serializable_field( + default=(StepTokenizers.Coord(),), + serialization_fn=lambda x: [y.serialize() for y in x], + loading_fn=lambda x: tuple(x[StepTokenizers.key]), + ) + pre: bool = serializable_field(default=False) + intra: bool = serializable_field(default=False) + post: bool = serializable_field(default=False) + + def to_tokens( + self, maze: SolvedMaze, coord_tokenizer: CoordTokenizers._CoordTokenizer + ) -> list[str]: + return [ + *self._leading_tokens(maze, coord_tokenizer), + *flatten( + [ + self._single_step_tokens(maze, start, end, coord_tokenizer) + for start, end in self.step_size.step_start_end_indices(maze) + ] + ), + *self._trailing_tokens(maze, coord_tokenizer), + ] + + def _single_step_tokens( + self, + maze: SolvedMaze, + i: int, + j: int, + coord_tokenizer: CoordTokenizers._CoordTokenizer, + ) -> list[str]: + """Returns the token sequence representing a single step along the path.""" + step_rep_tokens: list[list[str]] = [ + step_tokenizer.to_tokens(maze, i, j, coord_tokenizer=coord_tokenizer) + for step_tokenizer in self.step_tokenizers + ] + if self.intra: + step_rep_tokens_and_intra: list[str] = [None] * ( + len(step_rep_tokens) * 2 + ) + step_rep_tokens_and_intra[::2] = step_rep_tokens + step_rep_tokens_and_intra[1::2] = [VOCAB.PATH_INTRA] * len( + step_rep_tokens + ) + step_rep_tokens = list(flatten(step_rep_tokens_and_intra)) + all_tokens: list[str] = [ + *empty_sequence_if_attr_false((VOCAB.PATH_PRE,), self, "pre"), + *flatten(step_rep_tokens), + *empty_sequence_if_attr_false((VOCAB.PATH_POST,), self, "post"), + ] + return all_tokens + + def _leading_tokens( + self, maze: SolvedMaze, coord_tokenizer: CoordTokenizers._CoordTokenizer + ) -> list[str]: + """Returns tokens preceding those from the sequence from `_single_step_tokens`. + Since the for loop in `to_tokens` iterates `len(path)-1` times, a fencepost problem exists with `StepTokenizers.Coord`. + should NOT be included. + """ + if StepTokenizers.Coord() in self.step_tokenizers: + return [ + *empty_sequence_if_attr_false((VOCAB.PATH_PRE,), self, "pre"), + *coord_tokenizer.to_tokens(maze.solution[0, ...]), + *empty_sequence_if_attr_false((VOCAB.PATH_INTRA,), self, "intra"), + ] + return [] + + def _trailing_tokens( + self, c: Coord, coord_tokenizer: CoordTokenizers._CoordTokenizer + ) -> list[str]: + """Returns tokens following those from the sequence from `_single_step_tokens`. + should NOT be included. + """ + return [] + + def is_valid(self) -> bool: + if len(set(self.step_tokenizers)) != len(self.step_tokenizers): + # Uninteresting: repeated elements are not useful + return False + + if len(self.step_tokenizers) == 1 and isinstance( + self.step_tokenizers[0], StepTokenizers.Distance + ): + # Untrainable: `Distance` alone cannot encode a path. >=1 `StepTokenizer` which indicates direction/location is required. + return False + else: + return True + + +class PromptSequencers(__TokenizerElementNamespace): + key = "prompt_sequencer" + + @serializable_dataclass(frozen=True, kw_only=True) + class _PromptSequencer(_TokenizerElement, abc.ABC): + """ + Sequences token regions into a complete maze tokenization. + + # Parameters + - `coord_tokenizer`: Tokenizer element which tokenizes a single `Coord` aka maze position. + - `adj_list_tokenizer`: Tokenizer element which tokenizes the adjacency list of a `LatticeMaze`. + Uses `coord_tokenizer` to tokenize coords if needed in other `TokenizerElement`s. + """ + + coord_tokenizer: CoordTokenizers._CoordTokenizer = serializable_field( + default=CoordTokenizers.UT(), + loading_fn=lambda x: _load_tokenizer_element(x, CoordTokenizers), + ) + adj_list_tokenizer: AdjListTokenizers._AdjListTokenizer = serializable_field( + default=AdjListTokenizers.AdjListCoord(), + loading_fn=lambda x: _load_tokenizer_element(x, AdjListTokenizers), + ) + + @classmethod + def attribute_key(cls) -> str: + return PromptSequencers.key + + @staticmethod + def _trim_if_unsolved_maze( + untrimmed: list[str], is_untargeted: bool = False, is_unsolved: bool = False + ): + """Trims a full `SolvedMaze` prompt if the maze data reflects an unsolved or untargeted maze. + + # Development + This implementation should function for `AOTP`, `AOP`, and other concrete classes using any subsequence of AOTP. + It is not located in `token_utils.py` because it may need to be overridden in more exotic `PromptSequencer` subclasses. + """ + if is_untargeted: + return tokens_between( + untrimmed, + VOCAB.ADJLIST_START, + VOCAB.ADJLIST_END, + include_start=True, + include_end=True, + ) + if is_unsolved: + if VOCAB.TARGET_END in untrimmed: + return tokens_between( + untrimmed, + VOCAB.ADJLIST_START, + VOCAB.TARGET_END, + include_start=True, + include_end=True, + ) + else: + return tokens_between( + untrimmed, + VOCAB.ADJLIST_START, + VOCAB.ORIGIN_END, + include_start=True, + include_end=True, + ) + return untrimmed + + def to_tokens( + self, + maze: LatticeMaze, + *args, + **kwargs, + ) -> list[str]: + """Returns a complete list of tokens for a given set of maze elements.""" + untrimmed: list[str] = self._sequence_tokens( + *self._get_prompt_regions(maze) + ) + return self._trim_if_unsolved_maze( + untrimmed, not hasattr(maze, "start_pos"), not hasattr(maze, "solution") + ) + + def _get_prompt_regions( + self, + maze: LatticeMaze, + *args, + **kwargs, + ) -> list[list[str]]: + """Gets the prompt regions of a maze in a fixed sequence. + + This method is NOT responsible for including/excluding any prompt regions. + Always return according to the API described under Returns. + This implementation is expected to be suitable for most `PromptSequencer` subclasses. + Subclasses may override this method if needed for special behavior. + + # Returns + - [0]: list[str] Adjacency list tokens + - [1]: list[str] Origin tokens + - [2]: list[str] Target tokens + - [3]: list[str] Path tokens + + # `None`-valued Args + If one or more of `origin`, `target`, or `path` are `None`, that indicates that an unsolved or untargeted maze is being tokenized. + To ensure unpackability in `_sequence_tokens`, these `None` values are substituted for empty iterables. + """ + origin: Coord | None = getattr(maze, "start_pos", None) + target: list[Coord] | None = [ + getattr(maze, "end_pos", None) + ] # TargetTokenizer requires target: Sequence[Coord] + + return [ + ( + self.adj_list_tokenizer.to_tokens( + maze, coord_tokenizer=self.coord_tokenizer + ) + if hasattr(self, "adj_list_tokenizer") + else [] + ), + self.coord_tokenizer.to_tokens(origin) if origin is not None else [], + ( + self.target_tokenizer.to_tokens( + target, coord_tokenizer=self.coord_tokenizer + ) + if target[0] is not None and hasattr(self, "target_tokenizer") + else [] + ), + ( + self.path_tokenizer.to_tokens( + maze, coord_tokenizer=self.coord_tokenizer + ) + if hasattr(maze, "solution") and hasattr(self, "path_tokenizer") + else [] + ), + ] + + @abc.abstractmethod + def _sequence_tokens( + self, + adj_list: list[str], + origin: list[str] | None, + target: list[str] | None, + path: list[str] | None, + ) -> list[str]: + """Sequences token regions into a complete prompt. + Includes any boundary tokens in `constants.SPECIAL_TOKENS` such as , , etc. + # Parameters + - `adj_list`: Tokens representing the adjacency list + - `origin`: Tokens representing the origin + - `target`: Tokens representing the target + - `path`: Tokens representing the path + """ + pass + + def is_valid(self) -> bool: + # No invalid instances possible within data member type hint bounds + return True + + @serializable_dataclass(frozen=True, kw_only=True) + class AOTP(_PromptSequencer): + """ + Sequences a prompt as [adjacency list, origin, target, path]. + + # Parameters + - `target_tokenizer`: Tokenizer element which tokenizes the target(s) of a `TargetedLatticeMaze`. + Uses `coord_tokenizer` to tokenize coords if that is part of the design of that `TargetTokenizer`. + - `path_tokenizer`: Tokenizer element which tokenizes the solution path of a `SolvedMaze`. + Uses `coord_tokenizer` to tokenize coords if that is part of the design of that `PathTokenizer`. + + """ + + target_tokenizer: TargetTokenizers._TargetTokenizer = serializable_field( + default=TargetTokenizers.Unlabeled(), + loading_fn=lambda x: _load_tokenizer_element(x, TargetTokenizers), + ) + path_tokenizer: PathTokenizers._PathTokenizer = serializable_field( + default=PathTokenizers.StepSequence(), + loading_fn=lambda x: _load_tokenizer_element(x, PathTokenizers), + ) + + def _sequence_tokens( + self, + adj_list: list[str], + origin: list[str], + target: list[str], + path: list[str], + ) -> list[str]: + return [ + VOCAB.ADJLIST_START, + *adj_list, + VOCAB.ADJLIST_END, + VOCAB.ORIGIN_START, + *origin, + VOCAB.ORIGIN_END, + VOCAB.TARGET_START, + *target, + VOCAB.TARGET_END, + VOCAB.PATH_START, + *path, + VOCAB.PATH_END, + ] + + @serializable_dataclass(frozen=True, kw_only=True) + class AOP(_PromptSequencer): + """Sequences a prompt as [adjacency list, origin, path]. + Still includes "" and "" tokens, but no representation of the target itself. + + # Parameters + - `path_tokenizer`: Tokenizer element which tokenizes the solution path of a `SolvedMaze`. + Uses `coord_tokenizer` to tokenize coords if that is part of the design of that `PathTokenizer`. + """ + + path_tokenizer: PathTokenizers._PathTokenizer = serializable_field( + default=PathTokenizers.StepSequence(), + loading_fn=lambda x: _load_tokenizer_element(x, PathTokenizers), + ) + + def _sequence_tokens( + self, + adj_list: list[str], + origin: list[str], + target: list[str], + path: list[str], + ) -> list[str]: + return [ + VOCAB.ADJLIST_START, + *adj_list, + VOCAB.ADJLIST_END, + VOCAB.ORIGIN_START, + *origin, + VOCAB.ORIGIN_END, + VOCAB.TARGET_START, + VOCAB.TARGET_END, + VOCAB.PATH_START, + *path, + VOCAB.PATH_END, + ] + + +@serializable_dataclass( + frozen=True, + kw_only=True, + properties_to_serialize=["tokenizer_element_tree_concrete", "name"], +) +class MazeTokenizerModular(SerializableDataclass): + """Tokenizer for mazes + + # Parameters + - `prompt_sequencer`: Tokenizer element which assembles token regions (adjacency list, origin, target, path) into a complete prompt. + + # Development + - To ensure backwards compatibility, the default constructor must always return a tokenizer equivalent to the legacy `TokenizationMode.AOTP_UT_Uniform`. + - Furthermore, the mapping reflected in `from_legacy` must also be maintained. + - Updates to `MazeTokenizerModular` or the `_TokenizerElement` hierarchy must maintain that behavior. + """ + + prompt_sequencer: PromptSequencers._PromptSequencer = serializable_field( + default=PromptSequencers.AOTP(), + loading_fn=lambda x: _load_tokenizer_element(x, PromptSequencers), + ) + + def __hash__(self): + "Stable hash to identify unique `MazeTokenizerModular` instances. uses name" + return int.from_bytes( + hashlib.blake2b(self.name.encode("utf-8")).digest(), + byteorder="big", + ) + + # Information Querying Methods + + @cached_property + def tokenizer_elements(self) -> list[_TokenizerElement]: + return [self.prompt_sequencer, *self.prompt_sequencer.tokenizer_elements()] + + def tokenizer_element_tree(self, abstract: bool = False) -> str: + """ + Returns a string representation of the tree of tokenizer elements contained in `self`. + + # Parameters + - `abstract: bool`: Whether to print the name of the abstract base class or the concrete class for each `_TokenizerElement` instance. + """ + + return "\n".join( + [ + type(self).__name__, + self.prompt_sequencer.tokenizer_element_tree( + abstract=abstract, depth=1 + ), + ] + ) + + @property + def tokenizer_element_tree_concrete(self): + """ + Property wrapper for `tokenizer_element_tree` so that it can be used in `properties_to_serialize`. + """ + return self.tokenizer_element_tree() + + def tokenizer_element_dict(self) -> dict: + """ + Nested dictionary of the internal `TokenizerElement`s. + """ + return {type(self).__name__: self.prompt_sequencer.tokenizer_element_dict()} + + @property + def name(self) -> str: + """Serializes MazeTokenizer into a key for encoding in zanj""" + return "-".join([type(self).__name__, self.prompt_sequencer.name]) + + def summary(self) -> dict[str, str]: + """ + Single-level dictionary of the internal `TokenizerElement`s. + """ + return { + # "prompt_sequencer": self.prompt_sequencer.name, + **{elem.attribute_key(): elem.name for elem in self.tokenizer_elements} + } + + @staticmethod + def _type_check(obj: any) -> None: + """Helper method for `has_element`""" + if not ( + isinstance(obj, _TokenizerElement) + or (isinstance(obj, type) and issubclass(obj, _TokenizerElement)) + ): + raise TypeError(f"{obj} is not a `_TokenizerElement` instance or subclass.") + + def _has_element_singular(self, el: type[_TokenizerElement] | _TokenizerElement): + """Helper method for `has_element`""" + self._type_check(el) + if isinstance(el, type): + return any([isinstance(e, el) for e in self.tokenizer_elements]) + else: + return el in self.tokenizer_elements + + def has_element( + self, + *elements: Sequence[type[_TokenizerElement] | _TokenizerElement], + ) -> bool: + """Returns True if the `MazeTokenizerModular` instance contains ALL of the items specified in `elements`. + + Querying with a partial subset of `_TokenizerElement` fields is not currently supported. + To do such a query, assemble multiple calls to `has_elements`. + + # Parameters + - `elements`: Singleton or iterable of `_TokenizerElement` instances or classes. + If an instance is provided, then comparison is done via instance equality. + If a class is provided, then comparison isdone via `isinstance`. I.e., any instance of that class is accepted. + """ + if len(elements) == 1 and isinstance(elements[0], Iterable): + elements = elements[0] + return all([self._has_element_singular(e) for e in elements]) + + def is_valid(self): + """ + Returns `True` if `self` is a valid tokenizer. + Evaluates the validity of all of `self.tokenizer_elements` according to each one's method. + """ + return all([el.is_valid() for el in self.tokenizer_elements]) + + def is_legacy_equivalent(self) -> bool: + """Returns if `self` has identical stringification behavior as any legacy `MazeTokenizer`.""" + return any( + [ + self == MazeTokenizerModular.from_legacy(tok_mode) + for tok_mode in TokenizationMode + ] + ) + + def is_tested_tokenizer(self) -> bool: + """ + Returns if the tokenizer is returned by `all_tokenizers._get_all_tokenizers`, the set of tested and reliable tokenizers. + Since evaluating `all_tokenizers._get_all_tokenizers` is expensive, + instead checks for membership of `self`'s hash in `get_all_tokenizer_hashes()`. + """ + all_tokenizer_hashes: Int64[np.ndarray, "n_tokenizers"] = ( + get_all_tokenizer_hashes() + ) + hash_index: int = np.searchsorted(all_tokenizer_hashes, hash(self)) + return ( + hash_index < len(all_tokenizer_hashes) + and all_tokenizer_hashes[hash_index] == hash(self) + and self.is_valid() + ) + + def is_AOTP(self) -> bool: + return self.has_element(PromptSequencers.AOTP) + + def is_UT(self) -> bool: + return self.has_element(CoordTokenizers.UT) + + # Alternate Constructors + # ====================== + + @classmethod + def from_legacy( + cls, legacy_maze_tokenizer: MazeTokenizer | TokenizationMode + ) -> "MazeTokenizerModular": + """Maps a legacy `MazeTokenizer` or `TokenizationMode` to its equivalent `MazeTokenizerModular` instance.""" + if isinstance(legacy_maze_tokenizer, MazeTokenizer): + legacy_maze_tokenizer = legacy_maze_tokenizer.tokenization_mode + return { + TokenizationMode.AOTP_UT_uniform: MazeTokenizerModular(), + TokenizationMode.AOTP_UT_rasterized: MazeTokenizerModular(), + TokenizationMode.AOTP_CTT_indexed: MazeTokenizerModular( + prompt_sequencer=PromptSequencers.AOTP( + coord_tokenizer=CoordTokenizers.CTT() + ) + ), + }[legacy_maze_tokenizer] + + # Simple properties + # ================= + @classmethod + def from_tokens( + cls, + tokens: str | list[str], + ) -> "MazeTokenizerModular": + """ + Infers most `MazeTokenizerModular` parameters from a full sequence of tokens. + """ + raise NotImplementedError( + "Recovering tokenizer objects from MazeTokenizerModular-produced strings is not supported" + ) + + @property + def token_arr(self) -> list[str] | None: + """map from index to token""" + return VOCAB_LIST + + @property + def tokenizer_map(self) -> dict[str, int]: + """map from token to index""" + return VOCAB_TOKEN_TO_INDEX + + @property + def vocab_size(self) -> int: + """Number of tokens in the static vocab""" + return len(VOCAB_LIST) + + @property + def n_tokens(self) -> int: + raise NameError( + "`MazeTokenizerModular.n_tokens` has been removed. Use `len(maze_dataset.VOCAB_LIST)` instead." + ) + + @property + def padding_token_index(self) -> int: + return VOCAB_TOKEN_TO_INDEX[VOCAB.PADDING] + + # conversion functions + # ============================================================ + + def to_tokens( + self, + maze: LatticeMaze, + ) -> list[str]: + """Converts maze into a list of tokens.""" + return self.prompt_sequencer.to_tokens(maze) + + def coords_to_strings(self, coords: list[CoordTup | Coord]) -> list[str]: + return list( + flatten( + [self.prompt_sequencer.coord_tokenizer.to_tokens(c) for c in coords] + ) + ) + + @staticmethod + def strings_to_coords( + text: str, + when_noncoord: WhenMissing = "skip", + ) -> list[str | CoordTup]: + warnings.warn( + "`MazeTokenizerModular.strings_to_coords` only supports legacy UT strings.", + TokenizerPendingDeprecationWarning, + ) + return strings_to_coords(text=text, when_noncoord=when_noncoord) + + @staticmethod + def encode(text: str | list[str]) -> list[int]: + """encode a string or list of strings into a list of tokens""" + try: + if isinstance(text, str): + text = text.split() + return [VOCAB_TOKEN_TO_INDEX[token] for token in text] + except KeyError as e: + raise TokenError( + f"Token {e} not found", + f"in `VOCAB`.", + ) from e + + @staticmethod + def decode( + token_ids: Sequence[int], joined_tokens: bool = False + ) -> list[str] | str: + """decode a list of tokens into a string or list of strings""" + try: + output: list[str] = [VOCAB_LIST[token_id] for token_id in token_ids] + except IndexError as e: + raise TokenError(f"Token index '{e}' not found in `VOCAB`.") from e + if joined_tokens: + return " ".join(output) + else: + return output + + +def _load_tokenizer_hashes() -> Int64[np.ndarray, "n_tokenizers"]: + """Loads the sorted list of `all_tokenizers.ALL_TOKENIZERS` hashes from disk.""" + try: + path: Path = Path(__file__).parent / "MazeTokenizerModular_hashes.npz" + return np.load(path)["hashes"] + except FileNotFoundError as e: + raise FileNotFoundError( + "Tokenizers hashes cannot be loaded. To fix this:", + "\n- install the package with the `tokenizers` extra: `pip install maze-dataset[tokenizers]` (recommended)", + "\n- run `python -m maze-dataset.tokenization.save_hashes` (not recommended, might break depending on how `maze-dataset` is installed)", + ) from e + + +_ALL_TOKENIZER_HASHES: Int64[np.ndarray, "n_tokenizers"] + + +def get_all_tokenizer_hashes() -> Int64[np.ndarray, "n_tokenizers"]: + global _ALL_TOKENIZER_HASHES + try: + got_tokenizers: bool = len(_ALL_TOKENIZER_HASHES) > 0 + if got_tokenizers: + return _ALL_TOKENIZER_HASHES + else: + _ALL_TOKENIZER_HASHES = _load_tokenizer_hashes() + except NameError: + _ALL_TOKENIZER_HASHES = _load_tokenizer_hashes() + + return _ALL_TOKENIZER_HASHES diff --git a/maze_dataset/tokenization/save_hashes.py b/maze_dataset/tokenization/save_hashes.py new file mode 100644 index 00000000..b128953b --- /dev/null +++ b/maze_dataset/tokenization/save_hashes.py @@ -0,0 +1,109 @@ +"""generate and save the hashes of all supported tokenizers + +calls `maze_dataset.tokenization.all_tokenizers.save_hashes()` + +Usage: + +To save to the default location (inside package, `maze_dataset/tokenization/MazeTokenizerModular_hashes.npy`): +```bash +python -m maze_dataset.tokenization.save_hashes +``` + +to save to a custom location: +```bash +python -m maze_dataset.tokenization.save_hashes /path/to/save/to.npy +``` + +to check hashes shipped with the package: +```bash +python -m maze_dataset.tokenization.save_hashes --check +``` + +""" + +from pathlib import Path + +import numpy as np +from muutils.spinner import SpinnerContext + +import maze_dataset.tokenization.all_tokenizers as all_tokenizers +from maze_dataset.tokenization.maze_tokenizer import ( + _load_tokenizer_hashes, + get_all_tokenizer_hashes, +) + +if __name__ == "__main__": + # parse args + # ================================================== + import argparse + + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="generate and save the hashes of all supported tokenizers" + ) + + parser.add_argument("path", type=str, nargs="?", help="path to save the hashes to") + parser.add_argument( + "--quiet", "-q", action="store_true", help="disable progress bar and spinner" + ) + parser.add_argument( + "--parallelize", "-p", action="store_true", help="parallelize the computation" + ) + parser.add_argument( + "--check", + "-c", + action="store_true", + help="save to temp location, then compare to existing", + ) + + args: argparse.Namespace = parser.parse_args() + + if not args.check: + # write new hashes + # ================================================== + all_tokenizers.save_hashes( + path=args.path, + verbose=not args.quiet, + parallelize=args.parallelize, + ) + + else: + # check hashes only + # ================================================== + + # set up path + if args.path is not None: + raise ValueError("cannot use --check with a custom path") + temp_path: Path = Path("tests/_temp/tok_hashes.npz") + temp_path.parent.mkdir(parents=True, exist_ok=True) + + # generate and save to temp location + returned_hashes: np.ndarray = all_tokenizers.save_hashes( + path=temp_path, + verbose=not args.quiet, + parallelize=args.parallelize, + ) + + # load saved hashes + with SpinnerContext( + spinner_chars="square_dot", + update_interval=0.5, + message="loading saved hashes...", + ): + read_hashes: np.ndarray = np.load(temp_path)["hashes"] + read_hashes_pkg: np.ndarray = _load_tokenizer_hashes() + read_hashes_wrapped: np.ndarray = get_all_tokenizer_hashes() + + # compare + with SpinnerContext( + spinner_chars="square_dot", + update_interval=0.01, + message="checking hashes: ", + format_string="\r{spinner} ({elapsed_time:.2f}s) {message}{value} ", + format_string_when_updated=True, + ) as sp: + sp.update_value("returned vs read") + assert np.array_equal(returned_hashes, read_hashes) + sp.update_value("returned vs _load_tokenizer_hashes") + assert np.array_equal(returned_hashes, read_hashes_pkg) + sp.update_value("returned vs get_all_tokenizer_hashes()") + assert np.array_equal(read_hashes, read_hashes_wrapped) diff --git a/maze_dataset/tokenization/token_utils.py b/maze_dataset/tokenization/token_utils.py deleted file mode 100644 index e18a2024..00000000 --- a/maze_dataset/tokenization/token_utils.py +++ /dev/null @@ -1,123 +0,0 @@ -"""a whole bunch of utilities for tokenization""" - -import warnings - -from maze_dataset.constants import SPECIAL_TOKENS -from maze_dataset.tokenization.maze_tokenizer import TokenizationMode, is_UT - -# filtering things from a prompt or generated text -# ================================================== - - -def remove_padding_from_token_str(token_str: str) -> str: - token_str = token_str.replace(f"{SPECIAL_TOKENS.PADDING} ", "") - token_str = token_str.replace(f"{SPECIAL_TOKENS.PADDING}", "") - return token_str - - -def tokens_between( - tokens: list[str], - start_value: str, - end_value: str, - include_start: bool = False, - include_end: bool = False, - except_when_tokens_not_unique: bool = False, -) -> list[str]: - if start_value == end_value: - raise ValueError( - f"start_value and end_value cannot be the same: {start_value = } {end_value = }" - ) - if except_when_tokens_not_unique: - if (tokens.count(start_value) != 1) or (tokens.count(end_value) != 1): - raise ValueError( - "start_value or end_value is not unique in the input tokens:", - f"{tokens.count(start_value) = } {tokens.count(end_value) = }" - f"{start_value = } {end_value = }", - f"{tokens = }", - ) - else: - if (tokens.count(start_value) < 1) or (tokens.count(end_value) < 1): - raise ValueError( - "start_value or end_value is not present in the input tokens:", - f"{tokens.count(start_value) = } {tokens.count(end_value) = }", - f"{start_value = } {end_value = }", - f"{tokens = }", - ) - - start_idx: int = tokens.index(start_value) + int(not include_start) - end_idx: int = tokens.index(end_value) + int(include_end) - - assert start_idx < end_idx, "Start must come before end" - - return tokens[start_idx:end_idx] - - -def get_adj_list_tokens(tokens: list[str]) -> list[str]: - return tokens_between( - tokens, SPECIAL_TOKENS.ADJLIST_START, SPECIAL_TOKENS.ADJLIST_END - ) - - -def get_path_tokens(tokens: list[str], trim_end: bool = False) -> list[str]: - """The path is considered everything from the first path coord to the path_end token, if it exists.""" - if SPECIAL_TOKENS.PATH_START not in tokens: - raise ValueError( - f"Path start token {SPECIAL_TOKENS.PATH_START} not found in tokens:\n{tokens}" - ) - start_idx: int = tokens.index(SPECIAL_TOKENS.PATH_START) + int(trim_end) - end_idx: int | None = None - if trim_end and (SPECIAL_TOKENS.PATH_END in tokens): - end_idx = tokens.index(SPECIAL_TOKENS.PATH_END) - return tokens[start_idx:end_idx] - - -def get_context_tokens(tokens: list[str]) -> list[str]: - return tokens_between( - tokens, - SPECIAL_TOKENS.ADJLIST_START, - SPECIAL_TOKENS.PATH_START, - include_start=True, - include_end=True, - ) - - -def get_origin_tokens(tokens: list[str]) -> list[str]: - return tokens_between( - tokens, - SPECIAL_TOKENS.ORIGIN_START, - SPECIAL_TOKENS.ORIGIN_END, - include_start=False, - include_end=False, - ) - - -def get_target_tokens(tokens: list[str]) -> list[str]: - return tokens_between( - tokens, - SPECIAL_TOKENS.TARGET_START, - SPECIAL_TOKENS.TARGET_END, - include_start=False, - include_end=False, - ) - - -def get_tokens_up_to_path_start( - tokens: list[str], - include_start_coord: bool = True, - tokenization_mode: TokenizationMode = TokenizationMode.AOTP_UT_uniform, -) -> list[str]: - warnings.warn( - "`get_tokens_up_to_path_start` assumes a unique token (UT) type tokenizer when `include_start_coord=True`. " - "This method is deprecated for a tokenizer-agnostic function in a future release.", - PendingDeprecationWarning, - ) - path_start_idx: int = tokens.index(SPECIAL_TOKENS.PATH_START) + 1 - if include_start_coord: - if is_UT(tokenization_mode): - return tokens[: path_start_idx + 1] - elif tokenization_mode == TokenizationMode.AOTP_CTT_indexed: - return tokens[: path_start_idx + 5] - else: - raise ValueError(f"Invalid tokenization mode: {tokenization_mode}") - else: - return tokens[:path_start_idx] diff --git a/maze_dataset/tokenization/util.py b/maze_dataset/tokenization/util.py deleted file mode 100644 index 10b66d06..00000000 --- a/maze_dataset/tokenization/util.py +++ /dev/null @@ -1,148 +0,0 @@ -"""utilities required by MazeTokenizer""" - -import re -import typing -from typing import Callable - -import numpy as np -from muutils.misc import list_join - -from maze_dataset.constants import CoordTup -from maze_dataset.utils import WhenMissing - -# coordinate to strings -# ================================================== - - -def _coord_to_strings_UT(coord: typing.Sequence[int]) -> list[str]: - """convert a coordinate to a string: `(i,j)`->"(i,j)" - always returns a list of length 1""" - return [f"({','.join(str(c) for c in coord)})"] - - -def _coord_to_strings_indexed(coord: typing.Sequence[int]) -> list[str]: - """convert a coordinate to a list of indexed strings: `(i,j)`->"(", "i", ",", "j", ")" """ - return [ - "(", - *list_join([str(c) for c in coord], lambda: ","), - ")", - ] - - -# string to coordinate representation -# ================================================== - - -def str_is_coord(coord_str: str, allow_whitespace: bool = True) -> bool: - """return True if the string represents a coordinate, False otherwise""" - - strip_func: Callable[[str], str] = lambda x: x.strip() if allow_whitespace else x - - coord_str = strip_func(coord_str) - - return all( - [ - coord_str.startswith("("), - coord_str.endswith(")"), - "," in coord_str, - all( - [ - strip_func(x).isdigit() - for x in strip_func(coord_str.lstrip("(").rstrip(")")).split(",") - ] - ), - ] - ) - - -def coord_str_to_tuple( - coord_str: str, allow_whitespace: bool = True -) -> tuple[int, ...]: - """convert a coordinate string to a tuple""" - strip_func: Callable[[str], str] = lambda x: x.strip() if allow_whitespace else x - coord_str = strip_func(coord_str) - stripped: str = strip_func(coord_str.lstrip("(").rstrip(")")) - return tuple(int(strip_func(x)) for x in stripped.split(",")) - - -def coord_str_to_coord_np(coord_str: str, allow_whitespace: bool = True) -> np.ndarray: - """convert a coordinate string to a numpy array""" - return np.array(coord_str_to_tuple(coord_str, allow_whitespace=allow_whitespace)) - - -def coord_str_to_tuple_noneable(coord_str: str) -> CoordTup | None: - """convert a coordinate string to a tuple, or None if the string is not a coordinate string""" - if not str_is_coord(coord_str): - return None - return coord_str_to_tuple(coord_str) - - -def coords_string_split_UT(coords: str) -> list[str]: - """Splits a string of tokens into a list containing the UT tokens for each coordinate. - - Not capable of producing indexed tokens ("(", "1", ",", "2", ")"), only unique tokens ("(1,2)"). - Non-whitespace portions of the input string not matched are preserved in the same list: - "(1,2) (5,6)" -> ["(1,2)", "", "(5,6)"] - """ - # ty gpt4 - return re.findall(r"\([^)]*\)|\S+", coords) - - -# back and forth in wrapped form -# ================================================== -def strings_to_coords( - text: str | list[str], - when_noncoord: WhenMissing = "skip", -) -> list[str | CoordTup]: - """converts a list of tokens to a list of coordinates - - returns list[CoordTup] if `when_noncoord` is "skip" or "error" - returns list[str | CoordTup] if `when_noncoord` is "include" - """ - tokens_joined: str = text if isinstance(text, str) else " ".join(text) - tokens_processed: list[str] = coords_string_split_UT(tokens_joined) - result: list[str] = list() - for token in tokens_processed: - coord: CoordTup | None = coord_str_to_tuple_noneable(token) - if coord is None: - if when_noncoord == "skip": - continue - elif when_noncoord == "error": - raise ValueError( - f"Invalid non-coordinate token '{token}' in text: '{text}'" - ) - elif when_noncoord == "include": - result.append(token) - else: - raise ValueError(f"Invalid when_noncoord value '{when_noncoord}'") - else: - result.append(coord) - return result - - -def coords_to_strings( - coords: list[str | CoordTup], - coord_to_strings_func: Callable[[CoordTup], list[str]], - when_noncoord: WhenMissing = "skip", -) -> list[str]: - """converts a list of coordinates to a list of strings (tokens) - - expects list[CoordTup] if `when_noncoord` is "error" - expects list[str | CoordTup] if `when_noncoord` is "include" or "skip" - """ - result: list[str] = list() - for coord in coords: - if isinstance(coord, str): - if when_noncoord == "skip": - continue - elif when_noncoord == "error": - raise ValueError( - f"Invalid non-coordinate '{coord}' in list of coords: '{coords}'" - ) - elif when_noncoord == "include": - result.append(coord) - else: - raise ValueError(f"Invalid when_noncoord value '{when_noncoord}'") - else: - result.extend(coord_to_strings_func(coord)) - return result diff --git a/maze_dataset/utils.py b/maze_dataset/utils.py index a6861211..37e43b63 100644 --- a/maze_dataset/utils.py +++ b/maze_dataset/utils.py @@ -1,15 +1,16 @@ -import cProfile +import enum +import itertools import math -import pstats -import timeit import typing -from typing import Any, Callable, Iterable, Literal, Mapping, NamedTuple, TypeVar +from dataclasses import Field +from functools import cache, wraps +from types import UnionType +from typing import Callable, Generator, Iterable, Literal, TypeVar, get_args, get_origin +import frozendict import numpy as np -from jaxtyping import Bool -from muutils.statcounter import StatCounter - -WhenMissing = Literal["except", "skip", "include"] +from jaxtyping import Bool, Int, Int8 +from muutils.misc import IsDataclass, flatten, is_abstract def bool_array_from_string( @@ -89,6 +90,81 @@ def corner_first_ndindex(n: int, ndim: int = 2) -> list[tuple]: """ +def manhattan_distance( + edges: ( + Int[np.ndarray, "edges coord=2 row_col=2"] + | Int[np.ndarray, "coord=2 row_col=2"] + ), +) -> Int[np.ndarray, "edges"] | Int[np.ndarray, ""]: + """Returns the Manhattan distance between two coords.""" + if len(edges.shape) == 3: + return np.linalg.norm(edges[:, 0, :] - edges[:, 1, :], axis=1, ord=1).astype( + np.int8 + ) + elif len(edges.shape) == 2: + return np.linalg.norm(edges[0, :] - edges[1, :], ord=1).astype(np.int8) + else: + raise ValueError( + f"{edges} has shape {edges.shape}, but must be match the shape in the type hints." + ) + + +def lattice_max_degrees(n: int) -> Int8[np.ndarray, "row col"]: + """ + Returns an array with the maximum possible degree for each coord. + """ + out = np.full((n, n), 2) + out[1:-1, :] += 1 + out[:, 1:-1] += 1 + return out + + +def lattice_connection_array( + n: int, +) -> Int8[np.ndarray, "edges=2*n*(n-1) leading_trailing_coord=2 row_col=2"]: + """ + Returns a 3D NumPy array containing all the edges in a 2D square lattice of size n x n. + Thanks Claude. + + # Parameters + - `n`: The size of the square lattice. + + # Returns + np.ndarray: A 3D NumPy array of shape containing the coordinates of the edges in the 2D square lattice. + In each pair, the coord with the smaller sum always comes first. + """ + row_coords, col_coords = np.meshgrid( + np.arange(n, dtype=np.int8), + np.arange(n, dtype=np.int8), + indexing="ij", + ) + + # Horizontal edges + horiz_edges = np.column_stack( + ( + row_coords[:, :-1].ravel(), + col_coords[:, :-1].ravel(), + row_coords[:, 1:].ravel(), + col_coords[:, 1:].ravel(), + ) + ) + + # Vertical edges + vert_edges = np.column_stack( + ( + row_coords[:-1, :].ravel(), + col_coords[:-1, :].ravel(), + row_coords[1:, :].ravel(), + col_coords[1:, :].ravel(), + ) + ) + + return np.concatenate( + (horiz_edges.reshape(n**2 - n, 2, 2), vert_edges.reshape(n**2 - n, 2, 2)), + axis=0, + ) + + def adj_list_to_nested_set(adj_list: list) -> set: """Used for comparison of adj_lists @@ -101,137 +177,221 @@ def adj_list_to_nested_set(adj_list: list) -> set: } -_AM_K = typing.TypeVar("_AM_K") -_AM_V = typing.TypeVar("_AM_V") - - -def apply_mapping( - mapping: Mapping[_AM_K, _AM_V], - iter: Iterable[_AM_K], - when_missing: WhenMissing = "skip", -) -> list[_AM_V]: - """Given an and a mapping, apply the mapping to the iterable with certain options""" - output: list[_AM_V] = list() - item: _AM_K - for item in iter: - if item in mapping: - output.append(mapping[item]) - continue - match when_missing: - case "skip": - continue - case "include": - output.append(item) - case "except": - raise ValueError(f"item {item} is missing from mapping {mapping}") - case _: - raise ValueError(f"invalid value for {when_missing = }") - return output - - -def apply_mapping_chain( - mapping: Mapping[_AM_K, Iterable[_AM_V]], - iter: Iterable[_AM_K], - when_missing: WhenMissing = "skip", -) -> list[_AM_V]: - """Given a list and a mapping, apply the mapping to the list""" - output: list[_AM_V] = list() - item: _AM_K - for item in iter: - if item in mapping: - output.extend(mapping[item]) - continue - match when_missing: - case "skip": - continue - case "include": - output.append(item) - case "except": - raise ValueError(f"item {item} is missing from mapping {mapping}") - case _: - raise ValueError(f"invalid value for {when_missing = }") - return output - - -T = TypeVar("T") - -FancyTimeitResult = NamedTuple( - "FancyTimeitResult", - [ - ("timings", StatCounter), - ("return_value", T | None), - ("profile", pstats.Stats | None), - ], -) - - -def timeit_fancy( - cmd: Callable[[], T] | str, - setup: str = lambda: None, - repeats: int = 5, - namespace: dict[str, Any] | None = None, - get_return: bool = True, - do_profiling: bool = False, -) -> FancyTimeitResult: +FiniteValued = TypeVar("FiniteValued", bound=bool | IsDataclass | enum.Enum) +""" +# `FiniteValued` +The details of this type are not possible to fully define via the Python 3.10 typing library. +This custom generic type is a generic domain of many types which have a finite, discrete, and well-defined range space. +`FiniteValued` defines the domain of supported types for the `all_instances` function, since that function relies heavily on static typing. +These types may be nested in an arbitrarily deep tree via Container Types and Superclass Types (see below). +The leaves of the tree must always be Primitive Types. + +# `FiniteValued` Subtypes +*: Indicates that this subtype is not yet supported by `all_instances` + +## Non-`FiniteValued` (Unbounded) Types +These are NOT valid subtypes, and are listed for illustrative purposes only. +This list is not comprehensive. +While the finite and discrete nature of digital computers means that the cardinality of these types is technically finite, +they are considered unbounded types in this context. +- No Container subtype may contain any of these unbounded subtypes. +- `int` +- `float` +- `str` +- `list` +- `set`: Set types without a `FiniteValued` argument are unbounded +- `tuple`: Tuple types without a fixed length are unbounded + +## Primitive Types +Primitive types are non-nested types which resolve directly to a concrete range of values +- `bool`: has 2 possible values +- *`enum.Enum`: The range of a concrete `Enum` subclass is its set of enum members +- `typing.Literal`: Every type constructed using `Literal` has a finite set of possible literal values in its definition. +This is the preferred way to include limited ranges of non-`FiniteValued` types such as `int` or `str` in a `FiniteValued` hierarchy. + +## Container Types +Container types are types which contain zero or more fields of `FiniteValued` type. +The range of a container type is the cartesian product of their field types, except for `set[FiniteValued]`. +- `tuple[FiniteValued]`: Tuples of fixed length whose elements are each `FiniteValued`. +- `IsDataclass`: Concrete dataclasses whose fields are `FiniteValued`. +- *Standard concrete class: Regular classes could be supported just like dataclasses if all their data members are `FiniteValued`-typed. +- *`set[FiniteValued]`: Sets of fixed length of a `FiniteValued` type. + +## Superclass Types +Superclass types don't directly contain data members like container types. +Their range is the union of the ranges of their subtypes. +- Abstract dataclasses: Abstract dataclasses whose subclasses are all `FiniteValued` superclass or container types +- *`IsDataclass`: Concrete dataclasses which also have their own subclasses. +- *Standard abstract classes: Abstract dataclasses whose subclasses are all `FiniteValued` superclass or container types +- `UnionType`: Any union of `FiniteValued` types, e.g., bool | Literal[2, 3] +""" + + +def _apply_validation_func( + type_: FiniteValued, + vals: Generator[FiniteValued, None, None], + validation_funcs: ( + frozendict.frozendict[FiniteValued, Callable[[FiniteValued], bool]] | None + ) = None, +) -> Generator[FiniteValued, None, None]: """ - Wrapper for `timeit` to get the fastest run of a callable with more customization options. - - Approximates the functionality of the %timeit magic or command line interface in a Python callable. + Helper function for `all_instances`. + Filters `vals` according to `validation_funcs`. + If `type_` is a regular type, searches in MRO order in `validation_funcs` and applies the first match, if any. + Handles generic types supported by `all_instances` with special `if` clauses. # Parameters - - `cmd: Callable[[], T] | str` - The callable to time. If a string, it will be passed to `timeit.Timer` as the `stmt` argument. - - `setup: str` - The setup code to run before `cmd`. If a string, it will be passed to `timeit.Timer` as the `setup` argument. - - `repeats: int` - The number of times to run `cmd` to get a reliable measurement. - - `namespace: dict[str, Any]` - Passed to `timeit.Timer` constructor. - If `cmd` or `setup` use local or global variables, they must be passed here. See `timeit` documentation for details. - - `get_return: bool` - Whether to pass the value returned from `cmd`. If True, the return value will be appended in a tuple with execution time. - This is for speed and convenience so that `cmd` doesn't need to be run again in the calling scope if the return values are needed. - (default: `False`) - - `do_profiling: bool` - Whether to return a `pstats.Stats` object in addition to the time and return value. - (default: `False`) - - # Returns - `FancyTimeitResult`, which is a NamedTuple with the following fields: - - `time: float` - The time in seconds it took to run `cmd` the minimum number of times to get a reliable measurement. - - `return_value: T|None` - The return value of `cmd` if `get_return` is `True`, otherwise `None`. - - `profile: pstats.Stats|None` - A `pstats.Stats` object if `do_profiling` is `True`, otherwise `None`. + - `type_: FiniteValued`: A type + - `vals: Generator[FiniteValued, None, None]`: Instances of `type_` + - `validation_funcs: dict`: Collection of types mapped to filtering validation functions """ - timer: timeit.Timer = timeit.Timer(cmd, setup, globals=namespace) - - # Perform the timing - times: list[float] = timer.repeat(repeats, 1) - - # Optionally capture the return value - return_value: T | None = None - profile: pstats.Stats | None = None - - if get_return or do_profiling: - # Optionally perform profiling - if do_profiling: - profiler = cProfile.Profile() - profiler.enable() + if validation_funcs is None: + return vals + if type_ in validation_funcs: # Only possible catch of UnionTypes + return filter(validation_funcs[type_], vals) + elif hasattr( + type_, "__mro__" + ): # Generic types like UnionType, Literal don't have `__mro__` + for superclass in type_.__mro__: + if superclass not in validation_funcs: + continue + vals = filter(validation_funcs[superclass], vals) + break # Only the first validation function hit in the mro is applied + elif get_origin(type_) == Literal: + return flatten( + ( + _apply_validation_func(type(v), [v], validation_funcs) + for v in get_args(type_) + ), + levels_to_flatten=1, + ) + return vals - return_value: T = cmd() - if do_profiling: - profiler.disable() - profile = pstats.Stats(profiler).strip_dirs().sort_stats("cumulative") +def _all_instances_wrapper(f): + """ + Converts dicts to frozendicts to allow caching and applies `_apply_validation_func`. + """ - # reset the return value if it wasn't requested - if not get_return: - return_value = None + @wraps(f) + def wrapper(*args, **kwargs): + @cache + def cached_wrapper( + type_: type, + all_instances_func: Callable, + validation_funcs: ( + frozendict.frozendict[FiniteValued, Callable[[FiniteValued], bool]] + | None + ), + ): + return _apply_validation_func( + type_, all_instances_func(type_, validation_funcs), validation_funcs + ) + + if len(args) >= 2 and args[1] is not None: + validation_funcs: frozendict.frozendict = frozendict.frozendict(args[1]) + elif "validation_funcs" in kwargs and kwargs["validation_funcs"] is not None: + validation_funcs: frozendict.frozendict = frozendict.frozendict( + kwargs["validation_funcs"] + ) + else: + validation_funcs = None + return cached_wrapper(args[0], f, validation_funcs) + + return wrapper + + +@_all_instances_wrapper +def all_instances( + type_: FiniteValued, + validation_funcs: dict[FiniteValued, Callable[[FiniteValued], bool]] | None = None, +) -> Generator[FiniteValued, None, None]: + """ + Returns all possible values of an instance of `type_` if finite instances exist. + Uses type hinting to construct the possible values. + All nested elements of `type_` must themselves be typed. + Do not use with types whose members contain circular references. + Function is susceptible to infinite recursion if `type_` is a dataclass whose member tree includes another instance of `type_`. - return FancyTimeitResult( - timings=StatCounter(times), - return_value=return_value, - profile=profile, - ) + # Parameters + - `type_: FiniteValued` + A finite-valued type. See docstring on `FiniteValued` for full details. + - `validation_funcs: dict[FiniteValued, Callable[[FiniteValued], bool]] | None` + A mapping of types to auxiliary functions to validate instances of that type. + This optional argument can provide an additional, more precise layer of validation for the instances generated beyond what type hinting alone can provide. + See `validation_funcs` Details section below. + (default: `None`) + + ## Supported `type_` Values + See docstring on `FiniteValued` for full details. + `type_` may be: + - `FiniteValued` + - A finite-valued, fixed-length Generic tuple type. + E.g., `tuple[bool]`, `tuple[bool, MyEnum]` are OK. + `tuple[bool, ...]` is NOT supported, since the length of the tuple is not fixed. + - Nested versions of any of the types in this list + - A `UnionType` of any of the types in this list + + ## `validation_funcs` Details + - `validation_funcs` is applied after all instances have been generated according to type hints. + - If `type_` is in `validation_funcs`, then the list of instances is filtered by `validation_funcs[type_](instance)`. + - `validation_funcs` is passed down for all recursive calls of `all_instances`. + - This allows for improved performance through maximal pruning of the exponential tree. + - `validation_funcs` supports subclass checking. + - If `type_` is not found in `validation_funcs`, then the search is performed iteratively in mro order. + - If a superclass of `type_` is found while searching in mro order, that validation function is applied and the list is returned. + - If no superclass of `type_` is found, then no filter is applied. + """ + if type_ == bool: + yield from [True, False] + elif hasattr(type_, "__dataclass_fields__"): + if is_abstract(type_): + # Abstract dataclass: call `all_instances` on each subclass + yield from flatten( + ( + all_instances(sub, validation_funcs) + for sub in type_.__subclasses__() + ), + levels_to_flatten=1, + ) + else: + # Concrete dataclass: construct dataclass instances with all possible combinations of fields + fields: list[Field] = type_.__dataclass_fields__ + fields_to_types: dict[str, type] = {f: fields[f].type for f in fields} + all_arg_sequences: Iterable = itertools.product( + *[ + all_instances(arg_type, validation_funcs) + for arg_type in fields_to_types.values() + ] + ) + yield from ( + type_(**{fld: arg for fld, arg in zip(fields_to_types.keys(), args)}) + for args in all_arg_sequences + ) + else: + type_origin = get_origin(type_) + if type_origin == tuple: + # Only matches Generic type tuple since regular tuple is not finite-valued + # Generic tuple: Similar to concrete dataclass. Construct all possible combinations of tuple fields. + yield from ( + tuple(combo) + for combo in itertools.product( + *( + all_instances(tup_item, validation_funcs) + for tup_item in get_args(type_) + ) + ) + ) + elif type_origin in (UnionType, typing.Union): + # Union: call `all_instances` for each type in the Union + yield from flatten( + [all_instances(sub, validation_funcs) for sub in get_args(type_)], + levels_to_flatten=1, + ) + elif type_origin is Literal: + # Literal: return all Literal arguments + yield from get_args(type_) + else: + raise TypeError( + f"Type {type_} either has unbounded possible values or is not supported (Enum is not supported)." + ) diff --git a/notebooks/demo_mazetokenizermodular.ipynb b/notebooks/demo_mazetokenizermodular.ipynb new file mode 100644 index 00000000..35e182a9 --- /dev/null +++ b/notebooks/demo_mazetokenizermodular.ipynb @@ -0,0 +1,1655 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "from typing import Callable, Any\n", + "import json\n", + "import random\n", + "\n", + "import yaml\n", + "from tqdm import tqdm\n", + "from muutils.errormode import ErrorMode\n", + "from muutils.misc import shorten_numerical_to_str\n", + "\n", + "from maze_dataset.tokenization import (\n", + " MazeTokenizerModular,\n", + " _TokenizerElement,\n", + " MazeTokenizer,\n", + " TokenizationMode,\n", + " CoordTokenizers,\n", + " PromptSequencers,\n", + " AdjListTokenizers,\n", + " PathTokenizers,\n", + " TargetTokenizers,\n", + " StepSizes,\n", + " StepTokenizers,\n", + " EdgePermuters,\n", + " EdgeGroupings,\n", + " EdgeSubsets,\n", + ")\n", + "\n", + "import maze_dataset.tokenization as md_tokenization\n", + "\n", + "from maze_dataset import (\n", + " VOCAB,\n", + " VOCAB_LIST,\n", + " VOCAB_TOKEN_TO_INDEX,\n", + " LatticeMazeGenerators,\n", + " MazeDataset,\n", + " MazeDatasetConfig,\n", + " SolvedMaze,\n", + ")\n", + "\n", + "from maze_dataset.plotting import MazePlot\n", + "\n", + "from maze_dataset.token_utils import equal_except_adj_list_sequence\n", + "\n", + "from maze_dataset.utils import all_instances\n", + "\n", + "from maze_dataset.tokenization.all_tokenizers import (\n", + " get_all_tokenizers, \n", + " MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# magic autoreload\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# `MazeTokenizerModular` Initialization and Structure\n", + "\n", + "Initialiation can be done vai the default constructor or via `MazeTokenizerModular.from_legacy`. The latter is useful for converting a legacy `MazeTokenizer` into its equivalent `MazeTokenizerModular`.\n", + "\n", + "Most of the API for these tokenizers is contained in the `MazeTokenizerModular` class. The only time when users need to interact with the internal components of a `MazeTokenizerModular` is when initializing a non-default tokenizer." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "mt_default: MazeTokenizerModular = MazeTokenizerModular()\n", + "mt_ctt: MazeTokenizerModular = MazeTokenizerModular.from_legacy(TokenizationMode.AOTP_CTT_indexed)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The objects composing `MazeTokenizerModular` are all instances of `_TokenizerElement`. " + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ] + } + ], + "source": [ + "print(\"\\n\".join([str(elem) for elem in _TokenizerElement.__subclasses__()]))\n", + "assert all(issubclass(elem, _TokenizerElement) for elem in _TokenizerElement.__subclasses__())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Within a tokenizer, these `_TokenizerElement`s are structured in a nested dataclass tree. The tree is slightly different depending on the particular options selected. Below are shown 3 different tree representations of `mt_default`." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "AOTP `_TokenizerElement` Structure:\n", + "\n", + "MazeTokenizerModular\n", + "\t_PromptSequencer\n", + "\t\t_CoordTokenizer\n", + "\t\t_AdjListTokenizer\n", + "\t\t\t_EdgeGrouping\n", + "\t\t\t_EdgeSubset\n", + "\t\t\t_EdgePermuter\n", + "\t\t_TargetTokenizer\n", + "\t\t_PathTokenizer\n", + "\t\t\t_StepSize\n", + "\t\t\t_StepTokenizer\n", + "\n", + "Default tokenizer elements:\n", + "\n", + "MazeTokenizerModular\n", + "\tAOTP\n", + "\t\tUT\n", + "\t\tAdjListCoord\n", + "\t\t\tUngrouped\n", + "\t\t\tConnectionEdges\n", + "\t\t\tRandomCoords\n", + "\t\tUnlabeled\n", + "\t\tStepSequence\n", + "\t\t\tSingles\n", + "\t\t\tCoord\n", + "\n", + "`MazeTokenizerModular` structure with all fields:\n", + "\n", + "MazeTokenizerModular:\n", + " AOTP:\n", + " adj_list_tokenizer:\n", + " AdjListCoord:\n", + " edge_grouping:\n", + " Ungrouped:\n", + " connection_token_ordinal: 1\n", + " edge_permuter:\n", + " RandomCoords: {}\n", + " edge_subset:\n", + " ConnectionEdges:\n", + " walls: false\n", + " post: true\n", + " pre: false\n", + " shuffle_d0: true\n", + " coord_tokenizer:\n", + " UT: {}\n", + " path_tokenizer:\n", + " StepSequence:\n", + " intra: false\n", + " post: false\n", + " pre: false\n", + " step_size:\n", + " Singles: {}\n", + " step_tokenizers:\n", + " - Coord: {}\n", + " target_tokenizer:\n", + " Unlabeled:\n", + " post: false\n", + "\n" + ] + } + ], + "source": [ + "print('\\nAOTP `_TokenizerElement` Structure:\\n')\n", + "print(mt_default.tokenizer_element_tree(abstract=True))\n", + "print(f'Default tokenizer elements:\\n')\n", + "print(mt_default.tokenizer_element_tree())\n", + "print(f'`MazeTokenizerModular` structure with all fields:\\n')\n", + "print(yaml.dump(mt_default.tokenizer_element_dict()))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are currently no other constructor methods. To construct a `MazeTokenizerModular` with other `TokenizerElement`s besides those available via `from_legacy`, the standard constructor with all parent `TokenizerElement`s in the tree must be used. Some `TokenizerElement`s also contain their own initialization arguments, most of which are `boolean`-typed. The most common arguments across all `TokenizerElement`s are named `pre`, `intra`, and `post`, which all control the option to add delimiter tokens to that part of the output. Other args are more specialized; see the class docstrings for more details." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Vocabulary\n", + "\n", + "All instances of `MazeTokenizerModular` uses a static vocabulary `VOCAB`, which is one of the main functional differences from `MazeTokenizer`. Direct access to the static vocabulary can be made through 3 constants:\n", + "- `VOCAB`\n", + " - Extension of the `SPECIAL_TOKENS` dataclass\n", + " - Supports direct property attribution\n", + "- `VOCAB_LIST: list[str]`\n", + " - Contains the vocabulary in a list\n", + " - Index of a token is its unique ID\n", + "- `VOCAB_TOKEN_TO_INDEX: dict[str, int]`\n", + " - Inverse mapping of `VOCAB_LIST`, maps tokens to unique IDs\n", + "\n", + "The following shows a visualization of the first 5 elements of each constant." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "`VOCAB`: IsDataclass\n", + "\tVOCAB.ADJLIST_START =\t''\n", + "\tVOCAB.ADJLIST_END =\t''\n", + "\tVOCAB.TARGET_START =\t''\n", + "\tVOCAB.TARGET_END =\t''\n", + "\tVOCAB.ORIGIN_START =\t''\n", + "\t...\n", + "\n", + "`VOCAB_LIST`: list[str]\n", + "\t''\n", + "\t''\n", + "\t''\n", + "\t''\n", + "\t''\n", + "\t...\n", + "\n", + "`VOCAB_TOKEN_TO_INDEX`: dict[str, int]\n", + "\t'': \t0\n", + "\t'': \t1\n", + "\t'': \t2\n", + "\t'': \t3\n", + "\t'': \t4\n", + "\t...\n" + ] + } + ], + "source": [ + "print(\"`VOCAB`: IsDataclass\")\n", + "for i, t in enumerate(VOCAB):\n", + " if i >= 5: break\n", + " print(f\"\\tVOCAB.{t} =\\t'{getattr(VOCAB, t)}'\")\n", + "print('\\t...')\n", + "\n", + "print(\"\\n`VOCAB_LIST`: list[str]\")\n", + "for t in VOCAB_LIST[:5]:\n", + " print(f\"\\t'{t}'\")\n", + "print('\\t...')\n", + " \n", + "print(\"\\n`VOCAB_TOKEN_TO_INDEX`: dict[str, int]\")\n", + "for t in VOCAB_TOKEN_TO_INDEX:\n", + " if VOCAB_TOKEN_TO_INDEX[t] >= 5: break\n", + " print(f\"\\t'{t}': \\t{VOCAB_TOKEN_TO_INDEX[t]}\")\n", + "print('\\t...')\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Considerations of Static Vocabulary\n", + "\n", + "- No more rasterized vs uniform indexing, it's all fixed as uniform now\n", + "- Fixed max grid size\n", + " - There is now a fixed maximum maze size which is supported.\n", + " - Unique tokens (`CoordTokenizers.UT`): 50x50\n", + " - Coordinate tuple tokens (`CoordTokenizers.CTT`): 128x128\n", + " - Mazes larger than these sizes are not supported\n", + " - There should be fewer compatibility issues with tokenizers using different `max_grid_size` parameters\n", + "- Vocabulary access\n", + " - Since maze-dataset 1.0, there is no need to pass around a tokenizer object or any data structure to access its custom vocabulary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Refactoring your code from legacy `MazeTokenizer` and `TokenizationMode`\n", + "Since `MazeTokenizerModular` uses a static vocabulary, it is not backwards compatible with any models trained using a legacy `MazeTokenizer`. The `maze-transformer` library is updated in vX.X.X to use `MazeTokenizerModular` by default. \n", + "\n", + "If you've manually specified a `MazeTokenizer` or `TokenizationMode` in your research code, the easiest way to refactor is using `MazeTokenizerModular.from_legacy`, which will convert a `MazeTokenizer` or `TokenizationMode` to its corresponding `MazeTokenizerModular` instance. Note that this correspondence means only that the stringification of mazes are equivalent; the encodings of strings to integer vocabulary indices are not." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "MazeTokenizer(tokenization_mode=, max_grid_size=None) \n", + " MazeTokenizerModular(prompt_sequencer=PromptSequencers.AOTP(coord_tokenizer=CoordTokenizers.UT(), adj_list_tokenizer=AdjListTokenizers.AdjListCoord(pre=False, post=True, shuffle_d0=True, edge_grouping=EdgeGroupings.Ungrouped(connection_token_ordinal=1), edge_subset=EdgeSubsets.ConnectionEdges(walls=False), edge_permuter=EdgePermuters.RandomCoords()), target_tokenizer=TargetTokenizers.Unlabeled(post=False), path_tokenizer=PathTokenizers.StepSequence(step_size=StepSizes.Singles(), step_tokenizers=(StepTokenizers.Coord(),), pre=False, intra=False, post=False)))\n" + ] + } + ], + "source": [ + "legacy_maze_tokenizer: MazeTokenizer = TokenizationMode.AOTP_UT_uniform.to_legacy_tokenizer()\n", + "modular_tokenizer_equivalent: MazeTokenizerModular = MazeTokenizerModular.from_legacy(legacy_maze_tokenizer)\n", + "print(legacy_maze_tokenizer, '\\n', modular_tokenizer_equivalent)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## `get_all_tokenizers`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Most combinations of `TokenizerElement`s and their arguments will produce a valid and unique `MazeTokenizerModular`. However, it is not guaranteed that every possible `MazeTokenizerModular` that can be constructed will make practical sense or have been put through testing.\n", + "\n", + "`get_all_tokenizers` constructs and caches all the tested tokenizers at once. For research investigating many different tokenization schemes, one practical way to access them is by looping through/sampling from `get_all_tokenizers()`. Be aware that the indexing of specific tokenizers may change without notice." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "5878656 or 5.9M tokenizers found.\n" + ] + } + ], + "source": [ + "all_tokenizers = get_all_tokenizers()\n", + "print(f\"{len(all_tokenizers)} or {shorten_numerical_to_str(len(all_tokenizers))} tokenizers found.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Other possible tokenizers which aren't in `get_all_tokenizers` are not guaranteed to function. Instead of running the expensive call to `get_all_tokenizers` yourself, you can check if a tokenizer is tested using `MazeTokenizerModular.is_tested_tokenizer` or `MazeTokenizerModular.is_valid`." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "assert mt_default.is_tested_tokenizer()\n", + "assert mt_default.is_valid()\n", + "assert mt_ctt.is_tested_tokenizer()\n", + "assert mt_ctt.is_valid()\n", + "\n", + "custom_untested_tokenizer = MazeTokenizerModular(\n", + " prompt_sequencer=PromptSequencers.AOP(\n", + " path_tokenizer=PathTokenizers.StepSequence(\n", + " step_tokenizers=(StepTokenizers.Distance(),) \n", + " ),\n", + " )\n", + ")\n", + "\n", + "assert not custom_untested_tokenizer.is_tested_tokenizer()\n", + "assert not custom_untested_tokenizer.is_valid()\n", + "# Danger, use this tokenizer at your own risk!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Filtering Tokenizer Collections\n", + "\n", + "There are a several practical ways to filter down a collection of tokenizers, or alternatively, generate a new collection with a filter.\n", + "\n", + "**WARNING: Applying `filter` to the output of `get_all_tokenizers` is extremely slow due to the size of the initial population. Only use the first 3 methods for filtering much smaller collections of tokenizers. To generate a new collection based on filters, always use `utils.all_instances`**\n", + "\n", + "In order of increasing speed, power and decreasing syntactic concision:\n", + "\n", + "1. `MazeTokenizerModular.has_element`\n", + " - Use case: Use with `filter` for concise, basic filtering on an existing collection\n", + "1. `MazeTokenizerModular.tokenizer_elements`\n", + " - Use case: Use with `filter` for more precise filtering on an existing collection\n", + "1. `MazeTokenizerModular.summary`\n", + " - Use case: Use with `filter` for more precise filtering on an existing collection\n", + "1. `utils.all_instances`\n", + " - Use case: Generate a new collection with filter(s).\n", + " - Anytime you don't already have a small collection of tokenizers as the starting population.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "len_all = len(get_all_tokenizers())" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "filtered 1: 13824 tokenizers / 5878656 tokenizers\n", + "filtered 2: 27216 tokenizers / 5878656 tokenizers\n", + "filtered 3: 979776 tokenizers / 5878656 tokenizers\n" + ] + } + ], + "source": [ + "filtered_1: list[MazeTokenizerModular] = list(\n", + " all_instances(\n", + " MazeTokenizerModular,\n", + " {\n", + " **MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS, # Always include this as the first item in the dict whenever calling `all_instances` with `MazeTokenizerModular` or any `_TokenizerElement`\n", + " CoordTokenizers._CoordTokenizer: lambda x: isinstance(x, CoordTokenizers.UT),\n", + " StepTokenizers.StepTokenizerPermutation: lambda x: x[0] == StepTokenizers.Cardinal() and len(x) < 3,\n", + " AdjListTokenizers._AdjListTokenizer: lambda x: isinstance(x, AdjListTokenizers.AdjListCardinal),\n", + " EdgeSubsets._EdgeSubset: lambda x: x == EdgeSubsets.ConnectionEdges(walls=False),\n", + " }\n", + " )\n", + ")\n", + "filtered_2: list[MazeTokenizerModular] = list(\n", + " all_instances(\n", + " MazeTokenizerModular,\n", + " {\n", + " **MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS, # Always include this as the first item in the dict whenever calling`all_instances` with `MazeTokenizerModular` or any `_TokenizerElement`\n", + " _TokenizerElement: lambda x: x.is_valid() and not getattr(x, \"pre\", False) and not getattr(x, \"intra\", False) and not getattr(x, \"post\", False), # Minimal delimiters everywhere...\n", + " CoordTokenizers.CTT: lambda x: x.pre and x.intra and x.post, # ...except for the coord tokens\n", + " }\n", + " )\n", + ")\n", + "filtered_3: list[MazeTokenizerModular] = list(\n", + " all_instances(\n", + " MazeTokenizerModular,\n", + " {\n", + " **MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS, # Always include this as the first item in the dict whenever calling `all_instances` with `MazeTokenizerModular` or any `_TokenizerElement`\n", + " PromptSequencers._PromptSequencer: lambda x: isinstance(x, PromptSequencers.AOTP),\n", + " TargetTokenizers._TargetTokenizer: lambda x: x == TargetTokenizers.Unlabeled(),\n", + " StepSizes.Singles: lambda x: False,\n", + "\n", + " }\n", + " )\n", + ")\n", + "print(f\"filtered 1: {len(filtered_1)} tokenizers / {len_all} tokenizers\")\n", + "print(f\"filtered 2: {len(filtered_2)} tokenizers / {len_all} tokenizers\")\n", + "print(f\"filtered 3: {len(filtered_3)} tokenizers / {len_all} tokenizers\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The examples below show equivalent methods of filtering one of the smaller collections above using options 1-3." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "filtered: 4608 tokenizers / 5878656 tokenizers\n", + "set(filtered_has_element).symmetric_difference(set(filtered_summary)) = set()\n" + ] + } + ], + "source": [ + "filtered_has_element: list[MazeTokenizerModular] = list(filter(\n", + " lambda x: x.has_element(EdgePermuters.BothCoords())\n", + ", filtered_1))\n", + "\n", + "filtered_tokenizer_elements: list[MazeTokenizerModular] = list(filter(\n", + " lambda x: EdgePermuters.BothCoords() in x.tokenizer_elements\n", + ", filtered_1))\n", + "\n", + "filtered_summary: list[MazeTokenizerModular] = list(filter(\n", + " lambda x: x.summary()[\"edge_permuter\"] == EdgePermuters.BothCoords().name\n", + ", filtered_1))\n", + "\n", + "print(f\"filtered: {len(filtered_has_element)} tokenizers / {len_all} tokenizers\")\n", + "\n", + "assert set(filtered_has_element) == set(filtered_tokenizer_elements)\n", + "print(f\"{set(filtered_has_element).symmetric_difference(set(filtered_summary)) = }\")\n", + "assert set(filtered_has_element) == set(filtered_summary)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# TokenizerElement Behavior Reference\n", + "\n", + "For each primary `TokenizerElement`, tokenizations and encodings derived from the below maze are logged in DataFrames for reference." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "trying to get the dataset 'test-g3-n1-a_dfs-h50097'\n", + "generating dataset...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "generating & solving mazes: 100%|██████████| 1/1 [00:00<00:00, 500.04maze/s]" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got dataset test with 1 items. output.cfg.to_fname() = 'test-g3-n1-a_dfs-h50097'\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "cfg: MazeDatasetConfig = MazeDatasetConfig(\n", + " name=\"test\",\n", + " grid_n=3,\n", + " n_mazes=1,\n", + " maze_ctor=LatticeMazeGenerators.gen_dfs,\n", + ")\n", + "dataset: MazeDataset = MazeDataset.from_config(\n", + " cfg,\n", + " do_download=False,\n", + " load_local=False,\n", + " do_generate=True,\n", + " save_local=False,\n", + " verbose=True,\n", + " gen_parallel=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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w8qQDOXl5dqwYYM2bdqkkSNH9lxrzs7OVlZWluPqbGLpuodVV1drzZo1amxs1BVXXKGf//znmj17tuuykAQ1NTW6/vrre+0vKyvTs88+O/AFIanO9BH269ev16JFiwa2GI8grAAA5nHNCgBgHmEFADCPsAIAmEdYAQDMI6wAAOYRVgAA8wgrAIB5hBUAwDzCCvCwRYsW8aGMGBQIKwCAeYQVAMA8wgpwLBgM6vHHH9cXv/hF+f1+jRs3To899pgk6a233tJXvvIVZWVlKTc3V3fddZdaW1sdVwwMPMIKcGzp0qVavXq1li1bpv3792vDhg3Kz89XW1ub5s+fr9GjR+uf//ynfv/732vLli2qqKhwXTIw4HjqOuDQyZMnNWbMGFVXV+vOO++Meu3pp5/WAw88oCNHjmjEiBGSpM2bN+umm25SQ0OD8vPztWjRIjU3N+vFF190UD0wcBhZAQ7961//UkdHh7761a/2+VpxcXFPUElSaWmpgsGgDhw4MJBlAs4RVoBDfCosEBvCCnBowoQJysrK0tatW3u9dtlll2nPnj1qa2vr2ff6668rIyNDkyZNGsgyAecIK8ChQCCgBx54QPfff7+ee+45HTp0SG+++abWrVunW2+9VYFAQGVlZdq3b59effVVLV68WN/73veUn5/vunRgQA1xXQAw2C1btkxDhgzR8uXL1dDQoIsuukh33323hg8frpdffln33nuvSkpKNHz4cN1yyy168sknXZcMDDhWAwIAzGMaEABgHmEFADCPsAIAmEdYAQDMI6wAAOYRVgAA8wgrAIB5hBUAwDzCCgBgHmEFADCPsAIAmPf/yZovFkjSjFwAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "pd.set_option('display.max_colwidth', None)\n", + "mz: SolvedMaze = dataset[0]\n", + "MazePlot(mz).plot()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "def all_elements_df(\n", + " elem_type: type[_TokenizerElement], \n", + " encoding=True, \n", + " **to_tokens_kwargs\n", + " ) -> pd.DataFrame:\n", + " columns = [\"_TokenizerElement\", \"tokens\"]\n", + " if encoding:\n", + " columns.append(\"encoding\")\n", + " tokenizers: pd.DataFrame = pd.DataFrame(columns=columns)\n", + "\n", + " tokenizers[\"_TokenizerElement\"] = list(all_instances(elem_type, validation_funcs=MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS))\n", + " tokenizers[\"tokens\"] = tokenizers[\"_TokenizerElement\"].apply(lambda x: \" \".join(x.to_tokens(**to_tokens_kwargs)))\n", + " if encoding:\n", + " tokenizers[\"encoding\"] = tokenizers[\"tokens\"].apply(lambda x: MazeTokenizerModular.encode(x))\n", + " return tokenizers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## `CoordTokenizers`" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_TokenizerElementtokensencoding
0UT()(1,2)[1602]
1CTT(pre=T, intra=T, post=T)( 1 , 2 )[11, 321, 12, 322, 13]
2CTT(pre=T, intra=T, post=F)( 1 , 2[11, 321, 12, 322]
3CTT(pre=T, intra=F, post=T)( 1 2 )[11, 321, 322, 13]
4CTT(pre=T, intra=F, post=F)( 1 2[11, 321, 322]
5CTT(pre=F, intra=T, post=T)1 , 2 )[321, 12, 322, 13]
6CTT(pre=F, intra=T, post=F)1 , 2[321, 12, 322]
7CTT(pre=F, intra=F, post=T)1 2 )[321, 322, 13]
8CTT(pre=F, intra=F, post=F)1 2[321, 322]
\n", + "
" + ], + "text/plain": [ + " _TokenizerElement tokens encoding\n", + "0 UT() (1,2) [1602]\n", + "1 CTT(pre=T, intra=T, post=T) ( 1 , 2 ) [11, 321, 12, 322, 13]\n", + "2 CTT(pre=T, intra=T, post=F) ( 1 , 2 [11, 321, 12, 322]\n", + "3 CTT(pre=T, intra=F, post=T) ( 1 2 ) [11, 321, 322, 13]\n", + "4 CTT(pre=T, intra=F, post=F) ( 1 2 [11, 321, 322]\n", + "5 CTT(pre=F, intra=T, post=T) 1 , 2 ) [321, 12, 322, 13]\n", + "6 CTT(pre=F, intra=T, post=F) 1 , 2 [321, 12, 322]\n", + "7 CTT(pre=F, intra=F, post=T) 1 2 ) [321, 322, 13]\n", + "8 CTT(pre=F, intra=F, post=F) 1 2 [321, 322]" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "coord_tokenizers = all_elements_df(CoordTokenizers._CoordTokenizer, coord=mz.solution[0])\n", + "coord_tokenizers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Adjacency List Tokenizers" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_TokenizerElementtokens
0AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), AllLatticeEdges(), SortedCoords())<XX> (0,0) (0,1) ; <--> (1,1) (1,2) ; <--> (1,0) (2,0) ; <--> (1,2) (2,2) ; <--> (2,1) (2,2) ; <--> (0,1) (1,1) ; <XX> (1,0) (1,1) ; <XX> (0,1) (0,2) ; <--> (0,2) (1,2) ; <--> (2,0) (2,1) ; <--> (0,0) (1,0) ; <XX> (1,1) (2,1) ;
1AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), AllLatticeEdges(), RandomCoords())<--> (2,0) (1,0) ; <XX> (1,0) (1,1) ; <--> (1,0) (0,0) ; <XX> (1,1) (2,1) ; <XX> (0,2) (0,1) ; <--> (2,1) (2,0) ; <--> (1,2) (0,2) ; <--> (2,2) (1,2) ; <--> (0,1) (1,1) ; <--> (1,1) (1,2) ; <--> (2,1) (2,2) ; <XX> (0,0) (0,1) ;
2AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), AllLatticeEdges(), BothCoords())<--> (2,0) (2,1) ; <--> (1,0) (0,0) ; <XX> (0,1) (0,0) ; <XX> (2,1) (1,1) ; <--> (2,1) (2,0) ; <--> (1,2) (1,1) ; <--> (0,1) (1,1) ; <--> (2,0) (1,0) ; <XX> (0,2) (0,1) ; <--> (0,2) (1,2) ; <--> (0,0) (1,0) ; <XX> (1,1) (2,1) ; <XX> (0,0) (0,1) ; <--> (2,1) (2,2) ; <XX> (0,1) (0,2) ; <--> (2,2) (2,1) ; <--> (1,0) (2,0) ; <XX> (1,0) (1,1) ; <--> (1,1) (1,2) ; <--> (2,2) (1,2) ; <XX> (1,1) (1,0) ; <--> (1,2) (0,2) ; <--> (1,1) (0,1) ; <--> (1,2) (2,2) ;
3AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), ConnectionEdges(walls=T), SortedCoords())<XX> (0,0) (0,1) ; <XX> (1,0) (1,1) ; <XX> (0,1) (0,2) ; <XX> (1,1) (2,1) ;
4AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), ConnectionEdges(walls=T), RandomCoords())<XX> (0,2) (0,1) ; <XX> (2,1) (1,1) ; <XX> (1,0) (1,1) ; <XX> (0,1) (0,0) ;
.........
211AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=T), RandomCoords())(1,1) SOUTH <XX> (0,0) EAST <XX> (0,1) EAST <XX> (1,1) WEST <XX>
212AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=T), BothCoords())(1,1) SOUTH <XX> (0,0) EAST <XX> (0,1) EAST <XX> (1,0) EAST <XX> (2,1) NORTH <XX> (0,1) WEST <XX> (0,2) WEST <XX> (1,1) WEST <XX>
213AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=F), SortedCoords())(0,0) SOUTH <--> (0,1) SOUTH <--> (0,2) SOUTH <--> (1,0) SOUTH <--> (1,1) EAST <--> (1,2) SOUTH <--> (2,0) EAST <--> (2,1) EAST <-->
214AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=F), RandomCoords())(1,0) NORTH <--> (1,1) NORTH <--> (0,2) SOUTH <--> (2,0) NORTH <--> (1,2) SOUTH <--> (1,1) EAST <--> (2,1) WEST <--> (2,2) WEST <-->
215AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=F), BothCoords())(0,0) SOUTH <--> (0,1) SOUTH <--> (0,2) SOUTH <--> (1,0) SOUTH <--> (1,2) SOUTH <--> (1,1) EAST <--> (2,0) EAST <--> (2,1) EAST <--> (1,0) NORTH <--> (1,1) NORTH <--> (1,2) NORTH <--> (2,0) NORTH <--> (2,2) NORTH <--> (1,2) WEST <--> (2,1) WEST <--> (2,2) WEST <-->
\n", + "

216 rows × 2 columns

\n", + "
" + ], + "text/plain": [ + " _TokenizerElement \\\n", + "0 AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), AllLatticeEdges(), SortedCoords()) \n", + "1 AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), AllLatticeEdges(), RandomCoords()) \n", + "2 AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), AllLatticeEdges(), BothCoords()) \n", + "3 AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), ConnectionEdges(walls=T), SortedCoords()) \n", + "4 AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=0), ConnectionEdges(walls=T), RandomCoords()) \n", + ".. ... \n", + "211 AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=T), RandomCoords()) \n", + "212 AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=T), BothCoords()) \n", + "213 AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=F), SortedCoords()) \n", + "214 AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=F), RandomCoords()) \n", + "215 AdjListCardinal(pre=F, post=F, shuffle_d0=F, Ungrouped(connection_token_ordinal=2), ConnectionEdges(walls=F), BothCoords()) \n", + "\n", + " tokens \n", + "0 (0,0) (0,1) ; <--> (1,1) (1,2) ; <--> (1,0) (2,0) ; <--> (1,2) (2,2) ; <--> (2,1) (2,2) ; <--> (0,1) (1,1) ; (1,0) (1,1) ; (0,1) (0,2) ; <--> (0,2) (1,2) ; <--> (2,0) (2,1) ; <--> (0,0) (1,0) ; (1,1) (2,1) ; \n", + "1 <--> (2,0) (1,0) ; (1,0) (1,1) ; <--> (1,0) (0,0) ; (1,1) (2,1) ; (0,2) (0,1) ; <--> (2,1) (2,0) ; <--> (1,2) (0,2) ; <--> (2,2) (1,2) ; <--> (0,1) (1,1) ; <--> (1,1) (1,2) ; <--> (2,1) (2,2) ; (0,0) (0,1) ; \n", + "2 <--> (2,0) (2,1) ; <--> (1,0) (0,0) ; (0,1) (0,0) ; (2,1) (1,1) ; <--> (2,1) (2,0) ; <--> (1,2) (1,1) ; <--> (0,1) (1,1) ; <--> (2,0) (1,0) ; (0,2) (0,1) ; <--> (0,2) (1,2) ; <--> (0,0) (1,0) ; (1,1) (2,1) ; (0,0) (0,1) ; <--> (2,1) (2,2) ; (0,1) (0,2) ; <--> (2,2) (2,1) ; <--> (1,0) (2,0) ; (1,0) (1,1) ; <--> (1,1) (1,2) ; <--> (2,2) (1,2) ; (1,1) (1,0) ; <--> (1,2) (0,2) ; <--> (1,1) (0,1) ; <--> (1,2) (2,2) ; \n", + "3 (0,0) (0,1) ; (1,0) (1,1) ; (0,1) (0,2) ; (1,1) (2,1) ; \n", + "4 (0,2) (0,1) ; (2,1) (1,1) ; (1,0) (1,1) ; (0,1) (0,0) ; \n", + ".. ... \n", + "211 (1,1) SOUTH (0,0) EAST (0,1) EAST (1,1) WEST \n", + "212 (1,1) SOUTH (0,0) EAST (0,1) EAST (1,0) EAST (2,1) NORTH (0,1) WEST (0,2) WEST (1,1) WEST \n", + "213 (0,0) SOUTH <--> (0,1) SOUTH <--> (0,2) SOUTH <--> (1,0) SOUTH <--> (1,1) EAST <--> (1,2) SOUTH <--> (2,0) EAST <--> (2,1) EAST <--> \n", + "214 (1,0) NORTH <--> (1,1) NORTH <--> (0,2) SOUTH <--> (2,0) NORTH <--> (1,2) SOUTH <--> (1,1) EAST <--> (2,1) WEST <--> (2,2) WEST <--> \n", + "215 (0,0) SOUTH <--> (0,1) SOUTH <--> (0,2) SOUTH <--> (1,0) SOUTH <--> (1,2) SOUTH <--> (1,1) EAST <--> (2,0) EAST <--> (2,1) EAST <--> (1,0) NORTH <--> (1,1) NORTH <--> (1,2) NORTH <--> (2,0) NORTH <--> (2,2) NORTH <--> (1,2) WEST <--> (2,1) WEST <--> (2,2) WEST <--> \n", + "\n", + "[216 rows x 2 columns]" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "adjlist_tokenizers = all_elements_df(AdjListTokenizers._AdjListTokenizer, encoding=False, maze=mz, coord_tokenizer=CoordTokenizers.UT())\n", + "adjlist_tokenizers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Target Tokenizers" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_TokenizerElementtokensencoding
0Unlabeled(post=T)(0,0) ||[1596, 15]
1Unlabeled(post=F)(0,0)[1596]
\n", + "
" + ], + "text/plain": [ + " _TokenizerElement tokens encoding\n", + "0 Unlabeled(post=T) (0,0) || [1596, 15]\n", + "1 Unlabeled(post=F) (0,0) [1596]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "target_tokenizers = all_elements_df(TargetTokenizers._TargetTokenizer, targets=[mz.end_pos], coord_tokenizer=CoordTokenizers.UT())\n", + "target_tokenizers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Path Tokenizers" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_TokenizerElementtokensencoding
0StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=T, post=T)STEP (1,2) : STEP (2,2) : THEN STEP (2,1) : THEN STEP (2,0) : THEN STEP (1,0) : THEN STEP (0,0) : THEN[704, 1602, 16, 704, 1604, 16, 17, 704, 1603, 16, 17, 704, 1601, 16, 17, 704, 1598, 16, 17, 704, 1596, 16, 17]
1StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=T, post=F)STEP (1,2) : STEP (2,2) : STEP (2,1) : STEP (2,0) : STEP (1,0) : STEP (0,0) :[704, 1602, 16, 704, 1604, 16, 704, 1603, 16, 704, 1601, 16, 704, 1598, 16, 704, 1596, 16]
2StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=F, post=T)STEP (1,2) STEP (2,2) THEN STEP (2,1) THEN STEP (2,0) THEN STEP (1,0) THEN STEP (0,0) THEN[704, 1602, 704, 1604, 17, 704, 1603, 17, 704, 1601, 17, 704, 1598, 17, 704, 1596, 17]
3StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=F, post=F)STEP (1,2) STEP (2,2) STEP (2,1) STEP (2,0) STEP (1,0) STEP (0,0)[704, 1602, 704, 1604, 704, 1603, 704, 1601, 704, 1598, 704, 1596]
4StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=F, intra=T, post=T)(1,2) : (2,2) : THEN (2,1) : THEN (2,0) : THEN (1,0) : THEN (0,0) : THEN[1602, 16, 1604, 16, 17, 1603, 16, 17, 1601, 16, 17, 1598, 16, 17, 1596, 16, 17]
............
1003StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=T, intra=F, post=F)STEP (1,2) STEP +5 BACKWARD SOUTH (0,0)[704, 1602, 704, 69, 60, 56, 1596]
1004StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=T, post=T)(1,2) : +5 : BACKWARD : SOUTH : (0,0) : THEN[1602, 16, 69, 16, 60, 16, 56, 16, 1596, 16, 17]
1005StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=T, post=F)(1,2) : +5 : BACKWARD : SOUTH : (0,0) :[1602, 16, 69, 16, 60, 16, 56, 16, 1596, 16]
1006StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=F, post=T)(1,2) +5 BACKWARD SOUTH (0,0) THEN[1602, 69, 60, 56, 1596, 17]
1007StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=F, post=F)(1,2) +5 BACKWARD SOUTH (0,0)[1602, 69, 60, 56, 1596]
\n", + "

1008 rows × 3 columns

\n", + "
" + ], + "text/plain": [ + " _TokenizerElement \\\n", + "0 StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=T, post=T) \n", + "1 StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=T, post=F) \n", + "2 StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=F, post=T) \n", + "3 StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=T, intra=F, post=F) \n", + "4 StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=F, intra=T, post=T) \n", + "... ... \n", + "1003 StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=T, intra=F, post=F) \n", + "1004 StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=T, post=T) \n", + "1005 StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=T, post=F) \n", + "1006 StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=F, post=T) \n", + "1007 StepSequence(Forks(), step_tokenizers=(Distance(), Relative(), Cardinal(), Coord(), ), pre=F, intra=F, post=F) \n", + "\n", + " tokens \\\n", + "0 STEP (1,2) : STEP (2,2) : THEN STEP (2,1) : THEN STEP (2,0) : THEN STEP (1,0) : THEN STEP (0,0) : THEN \n", + "1 STEP (1,2) : STEP (2,2) : STEP (2,1) : STEP (2,0) : STEP (1,0) : STEP (0,0) : \n", + "2 STEP (1,2) STEP (2,2) THEN STEP (2,1) THEN STEP (2,0) THEN STEP (1,0) THEN STEP (0,0) THEN \n", + "3 STEP (1,2) STEP (2,2) STEP (2,1) STEP (2,0) STEP (1,0) STEP (0,0) \n", + "4 (1,2) : (2,2) : THEN (2,1) : THEN (2,0) : THEN (1,0) : THEN (0,0) : THEN \n", + "... ... \n", + "1003 STEP (1,2) STEP +5 BACKWARD SOUTH (0,0) \n", + "1004 (1,2) : +5 : BACKWARD : SOUTH : (0,0) : THEN \n", + "1005 (1,2) : +5 : BACKWARD : SOUTH : (0,0) : \n", + "1006 (1,2) +5 BACKWARD SOUTH (0,0) THEN \n", + "1007 (1,2) +5 BACKWARD SOUTH (0,0) \n", + "\n", + " encoding \n", + "0 [704, 1602, 16, 704, 1604, 16, 17, 704, 1603, 16, 17, 704, 1601, 16, 17, 704, 1598, 16, 17, 704, 1596, 16, 17] \n", + "1 [704, 1602, 16, 704, 1604, 16, 704, 1603, 16, 704, 1601, 16, 704, 1598, 16, 704, 1596, 16] \n", + "2 [704, 1602, 704, 1604, 17, 704, 1603, 17, 704, 1601, 17, 704, 1598, 17, 704, 1596, 17] \n", + "3 [704, 1602, 704, 1604, 704, 1603, 704, 1601, 704, 1598, 704, 1596] \n", + "4 [1602, 16, 1604, 16, 17, 1603, 16, 17, 1601, 16, 17, 1598, 16, 17, 1596, 16, 17] \n", + "... ... \n", + "1003 [704, 1602, 704, 69, 60, 56, 1596] \n", + "1004 [1602, 16, 69, 16, 60, 16, 56, 16, 1596, 16, 17] \n", + "1005 [1602, 16, 69, 16, 60, 16, 56, 16, 1596, 16] \n", + "1006 [1602, 69, 60, 56, 1596, 17] \n", + "1007 [1602, 69, 60, 56, 1596] \n", + "\n", + "[1008 rows x 3 columns]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "path_tokenizers = all_elements_df(PathTokenizers._PathTokenizer, maze=mz, coord_tokenizer=CoordTokenizers.UT())\n", + "path_tokenizers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prompt Sequencers\n", + "\n", + "Currently, the only difference in possible prompt sequencers is the inclusion/exclusion of target tokens." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_TokenizerElementtokens
0AOTP(UT(), AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=1), ConnectionEdges(walls=F), RandomCoords()), Unlabeled(post=F), StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=F, intra=F, post=F))<ADJLIST_START> (0,1) <--> (1,1) ; (2,1) <--> (2,0) ; (2,2) <--> (1,2) ; (2,2) <--> (2,1) ; (0,2) <--> (1,2) ; (0,0) <--> (1,0) ; (1,1) <--> (1,2) ; (1,0) <--> (2,0) ; <ADJLIST_END> <ORIGIN_START> (1,2) <ORIGIN_END> <TARGET_START> (0,0) <TARGET_END> <PATH_START> (1,2) (2,2) (2,1) (2,0) (1,0) (0,0) <PATH_END>
1AOP(UT(), AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=1), ConnectionEdges(walls=F), RandomCoords()), StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=F, intra=F, post=F))<ADJLIST_START> (1,0) <--> (2,0) ; (0,1) <--> (1,1) ; (1,0) <--> (0,0) ; (2,1) <--> (2,2) ; (1,2) <--> (2,2) ; (2,1) <--> (2,0) ; (1,2) <--> (0,2) ; (1,1) <--> (1,2) ; <ADJLIST_END> <ORIGIN_START> (1,2) <ORIGIN_END> <TARGET_START> <TARGET_END> <PATH_START> (1,2) (2,2) (2,1) (2,0) (1,0) (0,0) <PATH_END>
\n", + "
" + ], + "text/plain": [ + " _TokenizerElement \\\n", + "0 AOTP(UT(), AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=1), ConnectionEdges(walls=F), RandomCoords()), Unlabeled(post=F), StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=F, intra=F, post=F)) \n", + "1 AOP(UT(), AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungrouped(connection_token_ordinal=1), ConnectionEdges(walls=F), RandomCoords()), StepSequence(Singles(), step_tokenizers=(Coord(), ), pre=F, intra=F, post=F)) \n", + "\n", + " tokens \n", + "0 (0,1) <--> (1,1) ; (2,1) <--> (2,0) ; (2,2) <--> (1,2) ; (2,2) <--> (2,1) ; (0,2) <--> (1,2) ; (0,0) <--> (1,0) ; (1,1) <--> (1,2) ; (1,0) <--> (2,0) ; (1,2) (0,0) (1,2) (2,2) (2,1) (2,0) (1,0) (0,0) \n", + "1 (1,0) <--> (2,0) ; (0,1) <--> (1,1) ; (1,0) <--> (0,0) ; (2,1) <--> (2,2) ; (1,2) <--> (2,2) ; (2,1) <--> (2,0) ; (1,2) <--> (0,2) ; (1,1) <--> (1,2) ; (1,2) (1,2) (2,2) (2,1) (2,0) (1,0) (0,0) " + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "prompt_sequencers = [PromptSequencers.AOTP(), PromptSequencers.AOP()]\n", + "columns = [\"_TokenizerElement\", \"tokens\"]\n", + "tokenizers: pd.DataFrame = pd.DataFrame(columns=columns)\n", + "\n", + "tokenizers[\"_TokenizerElement\"] = prompt_sequencers\n", + "tokenizers[\"tokens\"] = tokenizers[\"_TokenizerElement\"].apply(lambda x: \" \".join(x.to_tokens(maze=mz)))\n", + "tokenizers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Random Sample of `MazeTokenizerModular`s" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "random_sample_size: int = 1_000\n", + "\n", + "tokenizers: list[MazeTokenizerModular] = random.sample(get_all_tokenizers(), random_sample_size)\n", + "columns = [\"MazeTokenizerModular\", \"tokens\", \"encoding\", *mt_default.summary().keys()]\n", + "df: pd.DataFrame = pd.DataFrame(columns=columns)\n", + "\n", + "df[\"MazeTokenizerModular\"] = tokenizers\n", + "df[\"tokens\"] = df[\"MazeTokenizerModular\"].apply(lambda x: \" \".join(x.to_tokens(maze=mz)))\n", + "df.encoding = df.tokens.apply(MazeTokenizerModular.encode)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Tokenizers: 100%|██████████| 10/10 [00:01<00:00, 9.15it/s]\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
MazeTokenizerModulartokensencodingprompt_sequencercoord_tokenizeradj_list_tokenizeredge_groupingedge_subsetedge_permutertarget_tokenizerpath_tokenizerstep_sizestep_tokenizers
0MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> 1 2 <--> 2 2 ; 2 0 <--> 2 1 ; ...[0, 321, 322, 8, 322, 322, 9, 322, 320, 8, 322...AOP(CTT(pre=F, intra=F, post=F), AdjListCoord(...CTT(pre=F, intra=F, post=F)AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungr...Ungrouped(connection_token_ordinal=1)ConnectionEdges(walls=F)SortedCoords()NoneStepSequence(Forks(), step_tokenizers=(Cardina...Forks()Cardinal()
1MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> <XX> ( 1 1 ) ( 1 0 ) ; <--> ( ...[0, 707, 11, 321, 321, 13, 11, 321, 320, 13, 9...AOP(CTT(pre=T, intra=F, post=T), AdjListCoord(...CTT(pre=T, intra=F, post=T)AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungr...Ungrouped(connection_token_ordinal=0)AllLatticeEdges()BothCoords()NoneStepSequence(Forks(), step_tokenizers=(Cardina...Forks()Relative()
2MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> ( 0 , 0 ) <XX> EAST ; ( 0 , 2 ...[0, 11, 320, 12, 320, 13, 707, 57, 9, 11, 320,...AOTP(CTT(pre=T, intra=T, post=T), AdjListCardi...CTT(pre=T, intra=T, post=T)AdjListCardinal(pre=F, post=T, shuffle_d0=F, U...Ungrouped(connection_token_ordinal=1)AllLatticeEdges()RandomCoords()Unlabeled(post=F)StepSequence(Forks(), step_tokenizers=(Coord()...Forks()Coord()
3MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> <--> ( 1 1 EAST <XX> ( 0 0 EAS...[0, 8, 11, 321, 321, 57, 707, 11, 320, 320, 57...AOP(CTT(pre=T, intra=F, post=F), AdjListCardin...CTT(pre=T, intra=F, post=F)AdjListCardinal(pre=F, post=F, shuffle_d0=T, U...Ungrouped(connection_token_ordinal=0)AllLatticeEdges()SortedCoords()NoneStepSequence(Singles(), step_tokenizers=(Dista...Singles()Coord()
4MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> <XX> 0 0 0 1 ; <--> 0 0 1 0 ; ...[0, 707, 320, 320, 320, 321, 9, 8, 320, 320, 3...AOTP(CTT(pre=F, intra=F, post=F), AdjListCoord...CTT(pre=F, intra=F, post=F)AdjListCoord(pre=F, post=T, shuffle_d0=F, Ungr...Ungrouped(connection_token_ordinal=0)AllLatticeEdges()SortedCoords()Unlabeled(post=F)StepSequence(Singles(), step_tokenizers=(Relat...Singles()Coord()
..........................................
995MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> <--> 1 0 ) SOUTH <XX> 1 1 ) SO...[0, 8, 321, 320, 13, 56, 707, 321, 321, 13, 56...AOTP(CTT(pre=F, intra=F, post=T), AdjListCardi...CTT(pre=F, intra=F, post=T)AdjListCardinal(pre=F, post=F, shuffle_d0=T, U...Ungrouped(connection_token_ordinal=0)AllLatticeEdges()SortedCoords()Unlabeled(post=F)StepSequence(Forks(), step_tokenizers=(Cardina...Forks()Coord()
996MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> 1 , 0 ) 0 , 0 ) <--> ; 0 , 1 )...[0, 321, 12, 320, 13, 320, 12, 320, 13, 8, 9, ...AOTP(CTT(pre=F, intra=T, post=T), AdjListCoord...CTT(pre=F, intra=T, post=T)AdjListCoord(pre=F, post=T, shuffle_d0=F, Ungr...Ungrouped(connection_token_ordinal=2)ConnectionEdges(walls=F)RandomCoords()Unlabeled(post=F)StepSequence(Forks(), step_tokenizers=(Coord()...Forks()Cardinal()
997MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> <--> ( 2 , 0 ) ( 2 , 1 ) <--> ...[0, 8, 11, 322, 12, 320, 13, 11, 322, 12, 321,...AOP(CTT(pre=T, intra=T, post=T), AdjListCoord(...CTT(pre=T, intra=T, post=T)AdjListCoord(pre=F, post=F, shuffle_d0=T, Ungr...Ungrouped(connection_token_ordinal=0)ConnectionEdges(walls=F)SortedCoords()NoneStepSequence(Forks(), step_tokenizers=(Relativ...Forks()Coord()
998MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> ( 2 , 0 <--> NORTH ( 2 , 2 <--...[0, 11, 322, 12, 320, 8, 55, 11, 322, 12, 322,...AOP(CTT(pre=T, intra=T, post=F), AdjListCardin...CTT(pre=T, intra=T, post=F)AdjListCardinal(pre=F, post=F, shuffle_d0=T, U...Ungrouped(connection_token_ordinal=1)AllLatticeEdges()BothCoords()NoneStepSequence(Forks(), step_tokenizers=(Relativ...Forks()Distance()
999MazeTokenizerModular(prompt_sequencer=PromptSe...<ADJLIST_START> <XX> ( 0 , 2 ) ( 0 , 1 ) ; <XX...[0, 707, 11, 320, 12, 322, 13, 11, 320, 12, 32...AOP(CTT(pre=T, intra=T, post=T), AdjListCoord(...CTT(pre=T, intra=T, post=T)AdjListCoord(pre=F, post=T, shuffle_d0=T, Ungr...Ungrouped(connection_token_ordinal=0)ConnectionEdges(walls=T)BothCoords()NoneStepSequence(Singles(), step_tokenizers=(Cardi...Singles()Distance()
\n", + "

1000 rows × 13 columns

\n", + "
" + ], + "text/plain": [ + " MazeTokenizerModular \\\n", + "0 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "1 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "2 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "3 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "4 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + ".. ... \n", + "995 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "996 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "997 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "998 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "999 MazeTokenizerModular(prompt_sequencer=PromptSe... \n", + "\n", + " tokens \\\n", + "0 1 2 <--> 2 2 ; 2 0 <--> 2 1 ; ... \n", + "1 ( 1 1 ) ( 1 0 ) ; <--> ( ... \n", + "2 ( 0 , 0 ) EAST ; ( 0 , 2 ... \n", + "3 <--> ( 1 1 EAST ( 0 0 EAS... \n", + "4 0 0 0 1 ; <--> 0 0 1 0 ; ... \n", + ".. ... \n", + "995 <--> 1 0 ) SOUTH 1 1 ) SO... \n", + "996 1 , 0 ) 0 , 0 ) <--> ; 0 , 1 )... \n", + "997 <--> ( 2 , 0 ) ( 2 , 1 ) <--> ... \n", + "998 ( 2 , 0 <--> NORTH ( 2 , 2 <--... \n", + "999 ( 0 , 2 ) ( 0 , 1 ) ; , maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 1, 'serialize_full': 0.001988600008189678, 'serialize_minimal': 0.0021772999316453934, 'serialize_minimal_soln_cat': 0.002378899953328073, 'load_full': 0.002351999981328845, 'load_minimal': 0.0012380999978631735, 'load_minimal_soln_cat': 0.004195999936200678, 'save': 0.02134229999501258, 'read': 0.010948900016956031, 'save_minimal': 0.021853100042790174, 'read_minimal': 0.0077757000690326095}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=1, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 2/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[], grid_n=10, n_mazes=3, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 3, 'serialize_full': 0.0040244999108836055, 'serialize_minimal': 0.002215999993495643, 'serialize_minimal_soln_cat': 0.0027842000126838684, 'load_full': 0.0013717999681830406, 'load_minimal': 0.0010489999549463391, 'load_minimal_soln_cat': 0.0029203000012785196, 'save': 0.025130500085651875, 'read': 0.01522189995739609, 'save_minimal': 0.0183852999471128, 'read_minimal': 0.007356400019489229}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=3, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 3/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[], grid_n=10, n_mazes=10, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 10, 'serialize_full': 0.002029099967330694, 'serialize_minimal': 0.0033373000333085656, 'serialize_minimal_soln_cat': 0.0038735000416636467, 'load_full': 0.022445300011895597, 'load_minimal': 0.0016599999507889152, 'load_minimal_soln_cat': 0.00458700000308454, 'save': 0.04590440005995333, 'read': 0.03264600003603846, 'save_minimal': 0.019865899928845465, 'read_minimal': 0.01097559998743236}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=10, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 4/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[], grid_n=10, n_mazes=31, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 31, 'serialize_full': 0.0020833000307902694, 'serialize_minimal': 0.0067968000657856464, 'serialize_minimal_soln_cat': 0.007813800009898841, 'load_full': 0.07286169996950775, 'load_minimal': 0.0014121000422164798, 'load_minimal_soln_cat': 0.003298899973742664, 'save': 0.09832919994369149, 'read': 0.07949670008383691, 'save_minimal': 0.01897090009879321, 'read_minimal': 0.0076274999883025885}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}, {'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=31, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 5/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': [], 'kwargs': {}}], grid_n=10, n_mazes=100, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 100, 'serialize_full': 0.0033178000012412667, 'serialize_minimal': 0.0019642000552266836, 'serialize_minimal_soln_cat': 0.002348399953916669, 'load_full': 0.011457700049504638, 'load_minimal': 0.0023580999113619328, 'load_minimal_soln_cat': 0.00430549995508045, 'save': 0.10298119997605681, 'read': 0.02772969997022301, 'save_minimal': 0.021856300067156553, 'read_minimal': 0.008771799970418215}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': [], 'kwargs': {}}], grid_n=10, n_mazes=100, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 6/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': [], 'kwargs': {}}], grid_n=10, n_mazes=316, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 316, 'serialize_full': 0.0033614999847486615, 'serialize_minimal': 0.002898499951697886, 'serialize_minimal_soln_cat': 0.004402699996717274, 'load_full': 0.03508079994935542, 'load_minimal': 0.004440300050191581, 'load_minimal_soln_cat': 0.006955699995160103, 'save': 0.0607445000205189, 'read': 0.06687710003461689, 'save_minimal': 0.037657000008039176, 'read_minimal': 0.015283499960787594}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': [], 'kwargs': {}}], grid_n=10, n_mazes=316, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 7/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': [], 'kwargs': {}}], grid_n=10, n_mazes=1000, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 1000, 'serialize_full': 0.007448500022292137, 'serialize_minimal': 0.004398599965497851, 'serialize_minimal_soln_cat': 0.005521100014448166, 'load_full': 0.10773430007975549, 'load_minimal': 0.013339900062419474, 'load_minimal_soln_cat': 0.01814209995791316, 'save': 0.1529555000597611, 'read': 0.19769990001805127, 'save_minimal': 0.06099189992528409, 'read_minimal': 0.024608899955637753}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': [], 'kwargs': {}}], grid_n=10, n_mazes=1000, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 8/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=3162, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 3162, 'serialize_full': 0.018301300005987287, 'serialize_minimal': 0.007376400055363774, 'serialize_minimal_soln_cat': 0.012505099992267787, 'load_full': 0.3155438000103459, 'load_minimal': 0.03919269994366914, 'load_minimal_soln_cat': 0.04947179998271167, 'save': 0.4433165000518784, 'read': 0.5045625999337062, 'save_minimal': 0.15782690001651645, 'read_minimal': 0.052560399984940886}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=3162, maze_ctor=, maze_ctor_kwargs={})\n", - "Profiling 9/9:\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=10000, maze_ctor=, maze_ctor_kwargs={})\n", - "\t{'grid_n': 10, 'n_mazes': 10000, 'serialize_full': 0.049999000038951635, 'serialize_minimal': 0.02071439998690039, 'serialize_minimal_soln_cat': 0.04813159990590066, 'load_full': 0.9990234000142664, 'load_minimal': 0.11658990010619164, 'load_minimal_soln_cat': 0.1261283999774605, 'save': 1.360802499926649, 'read': 1.5814258999889717, 'save_minimal': 0.41468789998907596, 'read_minimal': 0.14149269997142255}\n", - "\tMazeDatasetConfig(name='test', seq_len_min=1, seq_len_max=512, seed=42, applied_filters=[{'name': 'collect_generation_meta', 'args': (), 'kwargs': {}}], grid_n=10, n_mazes=10000, maze_ctor=, maze_ctor_kwargs={})\n" - ] - } - ], + "outputs": [], "source": [ "for i, d in enumerate(datasets):\n", " print(f\"Profiling {i+1}/{len(datasets)}:\\t{d.cfg}\")\n", @@ -311,383 +296,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
grid_nn_mazesserialize_fullserialize_full:statsserialize_full:profilingserialize_minimalserialize_minimal:statsserialize_minimal:profilingserialize_minimal_soln_catserialize_minimal_soln_cat:stats...save:profilingreadread:statsread:profilingsave_minimalsave_minimal:statssave_minimal:profilingread_minimalread_minimal:statsread_minimal:profiling
01010.001989{0.001988600008189678: 1}<pstats.Stats object at 0x000002045788DB90>0.002177{0.0021772999316453934: 1}<pstats.Stats object at 0x0000020457880CD0>0.002379{0.002378899953328073: 1}...<pstats.Stats object at 0x0000020457895BD0>0.010949{0.010948900016956031: 1}<pstats.Stats object at 0x000002044B915250>0.021853{0.021853100042790174: 1}<pstats.Stats object at 0x0000020457858F10>0.007776{0.0077757000690326095: 1}<pstats.Stats object at 0x00000204577F5A10>
11030.004024{0.0040244999108836055: 1}<pstats.Stats object at 0x0000020457881DD0>0.002216{0.002215999993495643: 1}<pstats.Stats object at 0x0000020457823BD0>0.002784{0.0027842000126838684: 1}...<pstats.Stats object at 0x000002045783C550>0.015222{0.01522189995739609: 1}<pstats.Stats object at 0x00000204577F2A50>0.018385{0.0183852999471128: 1}<pstats.Stats object at 0x0000020457874B50>0.007356{0.007356400019489229: 1}<pstats.Stats object at 0x0000020456797410>
210100.002029{0.002029099967330694: 1}<pstats.Stats object at 0x00000204578A7AD0>0.003337{0.0033373000333085656: 1}<pstats.Stats object at 0x00000204578A8250>0.003874{0.0038735000416636467: 1}...<pstats.Stats object at 0x00000204567C0C90>0.032646{0.03264600003603846: 1}<pstats.Stats object at 0x0000020457883010>0.019866{0.019865899928845465: 1}<pstats.Stats object at 0x0000020457866750>0.010976{0.01097559998743236: 1}<pstats.Stats object at 0x0000020456770CD0>
310310.002083{0.0020833000307902694: 1}<pstats.Stats object at 0x00000204566D5E90>0.006797{0.0067968000657856464: 1}<pstats.Stats object at 0x00000204578A5710>0.007814{0.007813800009898841: 1}...<pstats.Stats object at 0x00000204566CD690>0.079497{0.07949670008383691: 1}<pstats.Stats object at 0x00000204577A2E50>0.018971{0.01897090009879321: 1}<pstats.Stats object at 0x000002044CD5CB50>0.007627{0.0076274999883025885: 1}<pstats.Stats object at 0x000002044B914690>
4101000.003318{0.0033178000012412667: 1}<pstats.Stats object at 0x00000204566DEB50>0.001964{0.0019642000552266836: 1}<pstats.Stats object at 0x00000204567892D0>0.002348{0.002348399953916669: 1}...<pstats.Stats object at 0x0000020457838F50>0.027730{0.02772969997022301: 1}<pstats.Stats object at 0x000002044CD97C50>0.021856{0.021856300067156553: 1}<pstats.Stats object at 0x000002044CD2CE50>0.008772{0.008771799970418215: 1}<pstats.Stats object at 0x000002045780D410>
5103160.003361{0.0033614999847486615: 1}<pstats.Stats object at 0x00000204565A3F90>0.002898{0.002898499951697886: 1}<pstats.Stats object at 0x0000020456662950>0.004403{0.004402699996717274: 1}...<pstats.Stats object at 0x000002045672E490>0.066877{0.06687710003461689: 1}<pstats.Stats object at 0x00000204564B0DD0>0.037657{0.037657000008039176: 1}<pstats.Stats object at 0x00000204564F4390>0.015283{0.015283499960787594: 1}<pstats.Stats object at 0x0000020456568D10>
61010000.007449{0.007448500022292137: 1}<pstats.Stats object at 0x00000204564E5950>0.004399{0.004398599965497851: 1}<pstats.Stats object at 0x000002044A839710>0.005521{0.005521100014448166: 1}...<pstats.Stats object at 0x00000204566E9CD0>0.197700{0.19769990001805127: 1}<pstats.Stats object at 0x0000020455F17410>0.060992{0.06099189992528409: 1}<pstats.Stats object at 0x000002045632D9D0>0.024609{0.024608899955637753: 1}<pstats.Stats object at 0x0000020456715F50>
71031620.018301{0.018301300005987287: 1}<pstats.Stats object at 0x000002045623D950>0.007376{0.007376400055363774: 1}<pstats.Stats object at 0x000002045788FF10>0.012505{0.012505099992267787: 1}...<pstats.Stats object at 0x0000020456595E10>0.504563{0.5045625999337062: 1}<pstats.Stats object at 0x000002045425A690>0.157827{0.15782690001651645: 1}<pstats.Stats object at 0x00000204577B8AD0>0.052560{0.052560399984940886: 1}<pstats.Stats object at 0x0000020454304B10>
810100000.049999{0.049999000038951635: 1}<pstats.Stats object at 0x00000204564F8710>0.020714{0.02071439998690039: 1}<pstats.Stats object at 0x00000204542BF990>0.048132{0.04813159990590066: 1}...<pstats.Stats object at 0x000002045664D9D0>1.581426{1.5814258999889717: 1}<pstats.Stats object at 0x000002045663FE10>0.414688{0.41468789998907596: 1}<pstats.Stats object at 0x000002044D7E5B90>0.141493{0.14149269997142255: 1}<pstats.Stats object at 0x000002044D803510>
\n", - "

9 rows × 32 columns

\n", - "
" - ], - "text/plain": [ - " grid_n n_mazes serialize_full serialize_full:stats \\\n", - "0 10 1 0.001989 {0.001988600008189678: 1} \n", - "1 10 3 0.004024 {0.0040244999108836055: 1} \n", - "2 10 10 0.002029 {0.002029099967330694: 1} \n", - "3 10 31 0.002083 {0.0020833000307902694: 1} \n", - "4 10 100 0.003318 {0.0033178000012412667: 1} \n", - "5 10 316 0.003361 {0.0033614999847486615: 1} \n", - "6 10 1000 0.007449 {0.007448500022292137: 1} \n", - "7 10 3162 0.018301 {0.018301300005987287: 1} \n", - "8 10 10000 0.049999 {0.049999000038951635: 1} \n", - "\n", - " serialize_full:profiling serialize_minimal \\\n", - "0 0.002177 \n", - "1 0.002216 \n", - "2 0.003337 \n", - "3 0.006797 \n", - "4 0.001964 \n", - "5 0.002898 \n", - "6 0.004399 \n", - "7 0.007376 \n", - "8 0.020714 \n", - "\n", - " serialize_minimal:stats serialize_minimal:profiling \\\n", - "0 {0.0021772999316453934: 1} \n", - "1 {0.002215999993495643: 1} \n", - "2 {0.0033373000333085656: 1} \n", - "3 {0.0067968000657856464: 1} \n", - "4 {0.0019642000552266836: 1} \n", - "5 {0.002898499951697886: 1} \n", - "6 {0.004398599965497851: 1} \n", - "7 {0.007376400055363774: 1} \n", - "8 {0.02071439998690039: 1} \n", - "\n", - " serialize_minimal_soln_cat serialize_minimal_soln_cat:stats ... \\\n", - "0 0.002379 {0.002378899953328073: 1} ... \n", - "1 0.002784 {0.0027842000126838684: 1} ... \n", - "2 0.003874 {0.0038735000416636467: 1} ... \n", - "3 0.007814 {0.007813800009898841: 1} ... \n", - "4 0.002348 {0.002348399953916669: 1} ... \n", - "5 0.004403 {0.004402699996717274: 1} ... \n", - "6 0.005521 {0.005521100014448166: 1} ... \n", - "7 0.012505 {0.012505099992267787: 1} ... \n", - "8 0.048132 {0.04813159990590066: 1} ... \n", - "\n", - " save:profiling read \\\n", - "0 0.010949 \n", - "1 0.015222 \n", - "2 0.032646 \n", - "3 0.079497 \n", - "4 0.027730 \n", - "5 0.066877 \n", - "6 0.197700 \n", - "7 0.504563 \n", - "8 1.581426 \n", - "\n", - " read:stats read:profiling \\\n", - "0 {0.010948900016956031: 1} \n", - "1 {0.01522189995739609: 1} \n", - "2 {0.03264600003603846: 1} \n", - "3 {0.07949670008383691: 1} \n", - "4 {0.02772969997022301: 1} \n", - "5 {0.06687710003461689: 1} \n", - "6 {0.19769990001805127: 1} \n", - "7 {0.5045625999337062: 1} \n", - "8 {1.5814258999889717: 1} \n", - "\n", - " save_minimal save_minimal:stats \\\n", - "0 0.021853 {0.021853100042790174: 1} \n", - "1 0.018385 {0.0183852999471128: 1} \n", - "2 0.019866 {0.019865899928845465: 1} \n", - "3 0.018971 {0.01897090009879321: 1} \n", - "4 0.021856 {0.021856300067156553: 1} \n", - "5 0.037657 {0.037657000008039176: 1} \n", - "6 0.060992 {0.06099189992528409: 1} \n", - "7 0.157827 {0.15782690001651645: 1} \n", - "8 0.414688 {0.41468789998907596: 1} \n", - "\n", - " save_minimal:profiling read_minimal \\\n", - "0 0.007776 \n", - "1 0.007356 \n", - "2 0.010976 \n", - "3 0.007627 \n", - "4 0.008772 \n", - "5 0.015283 \n", - "6 0.024609 \n", - "7 0.052560 \n", - "8 0.141493 \n", - "\n", - " read_minimal:stats read_minimal:profiling \n", - "0 {0.0077757000690326095: 1} \n", - "1 {0.007356400019489229: 1} \n", - "2 {0.01097559998743236: 1} \n", - "3 {0.0076274999883025885: 1} \n", - "4 {0.008771799970418215: 1} \n", - "5 {0.015283499960787594: 1} \n", - "6 {0.024608899955637753: 1} \n", - "7 {0.052560399984940886: 1} \n", - "8 {0.14149269997142255: 1} \n", - "\n", - "[9 rows x 32 columns]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "SPEEDS: pd.DataFrame = pd.DataFrame(speeds_data)\n", "\n", @@ -696,7 +307,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -716,309 +327,62 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
grid_nn_mazesserialize_fullserialize_minimalserialize_minimal_soln_catload_fullload_minimalload_minimal_soln_catsavereadsave_minimalread_minimalserialize/speedupload/speedupsave/speedupread/speedup
01010.0019890.0021770.0023790.0023520.0012380.0041960.0213420.0109490.0218530.0077760.9133331.8996850.9766261.408092
11030.0040240.0022160.0027840.0013720.0010490.0029200.0251310.0152220.0183850.0073561.8161101.3077221.3668802.069205
210100.0020290.0033370.0038740.0224450.0016600.0045870.0459040.0326460.0198660.0109760.60800613.5212652.3107132.974416
310310.0020830.0067970.0078140.0728620.0014120.0032990.0983290.0794970.0189710.0076270.30651251.5981155.18315910.422380
4101000.0033180.0019640.0023480.0114580.0023580.0043050.1029810.0277300.0218560.0087721.6891354.8588704.7117403.161233
5103160.0033610.0028980.0044030.0350810.0044400.0069560.0607450.0668770.0376570.0152831.1597387.9005471.6131004.375771
61010000.0074490.0043990.0055210.1077340.0133400.0181420.1529560.1977000.0609920.0246091.6933808.0760952.5078008.033675
71031620.0183010.0073760.0125050.3155440.0391930.0494720.4433170.5045630.1578270.0525602.4810618.0510862.8088789.599672
810100000.0499990.0207140.0481320.9990230.1165900.1261281.3608021.5814260.4146880.1414932.4137328.5686963.28151011.176731
\n", - "
" - ], - "text/plain": [ - " grid_n n_mazes serialize_full serialize_minimal \\\n", - "0 10 1 0.001989 0.002177 \n", - "1 10 3 0.004024 0.002216 \n", - "2 10 10 0.002029 0.003337 \n", - "3 10 31 0.002083 0.006797 \n", - "4 10 100 0.003318 0.001964 \n", - "5 10 316 0.003361 0.002898 \n", - "6 10 1000 0.007449 0.004399 \n", - "7 10 3162 0.018301 0.007376 \n", - "8 10 10000 0.049999 0.020714 \n", - "\n", - " serialize_minimal_soln_cat load_full load_minimal load_minimal_soln_cat \\\n", - "0 0.002379 0.002352 0.001238 0.004196 \n", - "1 0.002784 0.001372 0.001049 0.002920 \n", - "2 0.003874 0.022445 0.001660 0.004587 \n", - "3 0.007814 0.072862 0.001412 0.003299 \n", - "4 0.002348 0.011458 0.002358 0.004305 \n", - "5 0.004403 0.035081 0.004440 0.006956 \n", - "6 0.005521 0.107734 0.013340 0.018142 \n", - "7 0.012505 0.315544 0.039193 0.049472 \n", - "8 0.048132 0.999023 0.116590 0.126128 \n", - "\n", - " save read save_minimal read_minimal serialize/speedup \\\n", - "0 0.021342 0.010949 0.021853 0.007776 0.913333 \n", - "1 0.025131 0.015222 0.018385 0.007356 1.816110 \n", - "2 0.045904 0.032646 0.019866 0.010976 0.608006 \n", - "3 0.098329 0.079497 0.018971 0.007627 0.306512 \n", - "4 0.102981 0.027730 0.021856 0.008772 1.689135 \n", - "5 0.060745 0.066877 0.037657 0.015283 1.159738 \n", - "6 0.152956 0.197700 0.060992 0.024609 1.693380 \n", - "7 0.443317 0.504563 0.157827 0.052560 2.481061 \n", - "8 1.360802 1.581426 0.414688 0.141493 2.413732 \n", - "\n", - " load/speedup save/speedup read/speedup \n", - "0 1.899685 0.976626 1.408092 \n", - "1 1.307722 1.366880 2.069205 \n", - "2 13.521265 2.310713 2.974416 \n", - "3 51.598115 5.183159 10.422380 \n", - "4 4.858870 4.711740 3.161233 \n", - "5 7.900547 1.613100 4.375771 \n", - "6 8.076095 2.507800 8.033675 \n", - "7 8.051086 2.808878 9.599672 \n", - "8 8.568696 3.281510 11.176731 " - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], + "source": [ + "SPEEDS: pd.DataFrame = pd.DataFrame(speeds_data)\n", + "\n", + "# SPEEDS.loc[:,\"load_legacy\":\"load_minimal_soln_cat:profiling\"]\n", + "SPEEDS.loc[:,\"read_legacy\":\"read:profiling\"]\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "SPEEDS.columns" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "\n", + "def compute_speedups(speeds: pd.DataFrame) -> pd.DataFrame:\n", + " # for prefix in column_measurement_prefixes:\n", + " # speeds[f'{prefix}_speedup'] = speeds[f'{prefix}_full'] / speeds[f'{prefix}_minimal']\n", + " speeds['serialize/speedup'] = speeds['serialize_full'] / speeds['serialize_minimal']\n", + " speeds['load_minimal/speedup'] = speeds['load_legacy'] / speeds['load_minimal']\n", + " speeds['load/speedup'] = speeds['load_legacy'] / speeds['load_full']\n", + " speeds['save/speedup'] = speeds['save'] / speeds['save_minimal']\n", + " speeds['read_minimal/speedup'] = speeds['read_legacy'] / speeds['read_minimal']\n", + " speeds['read/speedup'] = speeds['read_legacy'] / speeds['read']\n", + "\n", + " return speeds\n", + "\n", + "SPEEDS = compute_speedups(SPEEDS)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "SPEEDS[[c for c in SPEEDS.columns if \":\" not in c]]" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Plotting serialize timings and speedups\n", - "Plotting grid_n=10\n", - "Plotting load timings and speedups\n", - "Plotting grid_n=10\n", - "Plotting save timings and speedups\n", - "Plotting grid_n=10\n", - "Plotting read timings and speedups\n", - "Plotting grid_n=10\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "def plot_speeds(\n", " speeds: pd.DataFrame,\n", @@ -1060,6 +424,11 @@ "\n", " # speedups\n", " ax_speedups = axs[1, i]\n", + " col_name: str = f'{prefix}' if prefix in ('serialize','save') else f'{prefix}_minimal'\n", + " ax_speedups.plot(x_n_mazes, speeds_masked[f\"{col_name}/speedup\"], \"x-\", label=f'grid_n={grid_n}')\n", + "\n", + " # Setting multiple properties with `set` for ax_speedups\n", + " ax_speedups.set(xscale='log', yscale='log', xlabel='Number of mazes', ylabel='Speedup', title=f'{col_name} speedups')\n", " ax_speedups.plot(\n", " x_n_mazes,\n", " speeds_masked[f\"{prefix}/speedup\"],\n", @@ -1085,244 +454,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Comparing rows 2 and 4, it appears that the `grid_n` has a relatively small effect on `serialize` and `load` runtimes. Those functions appear to run in $O(n_{\\mathrm{mazes}})$ time. `grid_n` does impact `save` and `read`, but not their `_minimal` counterparts as much.\n", - "\n", - "To compare the speed of analogous procedures vs `n_mazes`, the plots below show data from `speeds.loc[3:,:]`." + "Speedups plotted on the bottom set of axes all show the `_minimal` compared to the legacy performance. `serialize_full` and `save` are unchanged from the legacy version, so speedups are plotted relative to those vectors." ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
grid_nn_mazesserialize_minimal_soln_cat:profiling
0101<pstats.Stats object at 0x0000020457872C90>
1103<pstats.Stats object at 0x00000204577D7410>
21010<pstats.Stats object at 0x0000020456764590>
31031<pstats.Stats object at 0x00000204566509D0>
410100<pstats.Stats object at 0x000002044CE3A310>
510316<pstats.Stats object at 0x00000204566C67D0>
6101000<pstats.Stats object at 0x00000204564D6A90>
7103162<pstats.Stats object at 0x00000204565D2310>
81010000<pstats.Stats object at 0x000002044A88F010>
\n", - "
" - ], - "text/plain": [ - " grid_n n_mazes serialize_minimal_soln_cat:profiling\n", - "0 10 1 \n", - "1 10 3 \n", - "2 10 10 \n", - "3 10 31 \n", - "4 10 100 \n", - "5 10 316 \n", - "6 10 1000 \n", - "7 10 3162 \n", - "8 10 10000 " - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "SPEEDS[[\"grid_n\", \"n_mazes\", \"serialize_minimal_soln_cat:profiling\"]]" + "SPEEDS[['grid_n', 'n_mazes', 'serialize_minimal:profiling']]" ] }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 16044 function calls (15819 primitive calls) in 0.044 seconds\n", - "\n", - " Ordered by: internal time\n", - "\n", - " ncalls tottime percall cumtime percall filename:lineno(function)\n", - " 1 0.026 0.026 0.044 0.044 maze_dataset.py:407(_serialize_minimal_soln_cat)\n", - " 10001 0.010 0.000 0.010 0.000 {built-in method numpy.array}\n", - " 724 0.002 0.000 0.004 0.000 tokenize.py:433(_tokenize)\n", - " 1 0.002 0.002 0.002 0.002 maze_dataset.py:414()\n", - " 671 0.001 0.000 0.001 0.000 {method 'match' of 're.Pattern' objects}\n", - " 1 0.001 0.001 0.005 0.005 inspect.py:1224(getblock)\n", - " 723 0.000 0.000 0.000 0.000 inspect.py:1181(tokeneater)\n", - " 723 0.000 0.000 0.001 0.000 :1()\n", - " 195/2 0.000 0.000 0.006 0.003 json_serialize.py:231(json_serialize)\n", - " 723 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x00007FFB8BC98F90}\n", - " 8/1 0.000 0.000 0.000 0.000 json_serialize.py:109()\n", - " 647 0.000 0.000 0.000 0.000 {method 'span' of 're.Match' objects}\n", - " 2 0.000 0.000 0.000 0.000 {built-in method nt.stat}\n", - " 195 0.000 0.000 0.000 0.000 json_serialize.py:101()\n", - " 526 0.000 0.000 0.000 0.000 {method 'isidentifier' of 'str' objects}\n", - " 262/244 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance}\n", - " 218 0.000 0.000 0.000 0.000 {built-in method builtins.len}\n", - " 2 0.000 0.000 0.000 0.000 {method 'splitlines' of 'str' objects}\n", - " 1 0.000 0.000 0.005 0.005 serializable_dataclass.py:356(serialize)\n", - " 186 0.000 0.000 0.000 0.000 json_serialize.py:104()\n", - " 3 0.000 0.000 0.000 0.000 {built-in method numpy.empty}\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:1055(findsource)\n", - " 1 0.000 0.000 0.000 0.000 {method 'reduce' of 'numpy.ufunc' objects}\n", - " 1 0.000 0.000 0.005 0.005 inspect.py:1235(getsourcelines)\n", - " 9 0.000 0.000 0.000 0.000 typing.py:1572(__subclasscheck__)\n", - " 1 0.000 0.000 0.000 0.000 dataclasses.py:1233(fields)\n", - " 8/1 0.000 0.000 0.000 0.000 json_serialize.py:109()\n", - " 9 0.000 0.000 0.000 0.000 typing.py:1297(__instancecheck__)\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:936(getsourcefile)\n", - " 1 0.000 0.000 0.000 0.000 fromnumeric.py:71(_wrapreduction)\n", - " 1 0.000 0.000 0.005 0.005 maze_dataset.py:71()\n", - " 9 0.000 0.000 0.000 0.000 {built-in method builtins.issubclass}\n", - " 16 0.000 0.000 0.000 0.000 {method 'rstrip' of 'str' objects}\n", - " 1 0.000 0.000 0.000 0.000 fromnumeric.py:2177(sum)\n", - " 1 0.000 0.000 0.000 0.000 linecache.py:52(checkcache)\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:896(getfile)\n", - " 9 0.000 0.000 0.000 0.000 :121(__subclasscheck__)\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:735(unwrap)\n", - " 2 0.000 0.000 0.006 0.003 json_serialize.py:274(json_serialize)\n", - " 9 0.000 0.000 0.000 0.000 :117(__instancecheck__)\n", - " 11 0.000 0.000 0.000 0.000 {built-in method builtins.getattr}\n", - " 1 0.000 0.000 0.005 0.005 util.py:122(safe_getsource)\n", - " 9 0.000 0.000 0.000 0.000 {built-in method _abc._abc_instancecheck}\n", - " 1 0.000 0.000 0.005 0.005 inspect.py:1256(getsource)\n", - " 10 0.000 0.000 0.000 0.000 dataclasses.py:1248()\n", - " 1 0.000 0.000 0.000 0.000 {method 'join' of 'str' objects}\n", - " 9 0.000 0.000 0.000 0.000 json_serialize.py:108()\n", - " 9 0.000 0.000 0.000 0.000 {built-in method _abc._abc_subclasscheck}\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:973(getmodule)\n", - " 9 0.000 0.000 0.000 0.000 {method 'endswith' of 'str' objects}\n", - " 2 0.000 0.000 0.000 0.000 util.py:111(string_as_lines)\n", - " 14 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr}\n", - " 13 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects}\n", - " 1 0.000 0.000 0.000 0.000 json_serialize.py:138()\n", - " 3 0.000 0.000 0.000 0.000 inspect.py:943()\n", - " 1 0.000 0.000 0.000 0.000 package_importer.py:695(_patched_getfile)\n", - " 1 0.000 0.000 0.000 0.000 :16(exists)\n", - " 2 0.000 0.000 0.000 0.000 {built-in method builtins.any}\n", - " 1 0.000 0.000 0.000 0.000 __init__.py:272(_compile)\n", - " 4 0.000 0.000 0.000 0.000 inspect.py:283(ismodule)\n", - " 1 0.000 0.000 0.005 0.005 json_serialize.py:124(_serialize_override_serialize_func)\n", - " 1 0.000 0.000 0.000 0.000 fromnumeric.py:72()\n", - " 9 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects}\n", - " 3 0.000 0.000 0.000 0.000 inspect.py:292(isclass)\n", - " 3 0.000 0.000 0.000 0.000 inspect.py:946()\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:1172(__init__)\n", - " 3 0.000 0.000 0.000 0.000 inspect.py:456(istraceback)\n", - " 1 0.000 0.000 0.000 0.000 __init__.py:225(compile)\n", - " 1 0.000 0.000 0.000 0.000 linecache.py:36(getlines)\n", - " 2 0.000 0.000 0.000 0.000 inspect.py:300(ismethod)\n", - " 1 0.000 0.000 0.000 0.000 tokenize.py:616(generate_tokens)\n", - " 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}\n", - " 3 0.000 0.000 0.000 0.000 inspect.py:466(isframe)\n", - " 1 0.000 0.000 0.000 0.000 json_serialize.py:115()\n", - " 2 0.000 0.000 0.000 0.000 inspect.py:480(iscode)\n", - " 2 0.000 0.000 0.000 0.000 inspect.py:378(isfunction)\n", - " 1 0.000 0.000 0.000 0.000 {method 'get' of 'dict' objects}\n", - " 1 0.000 0.000 0.000 0.000 {built-in method builtins.id}\n", - " 1 0.000 0.000 0.000 0.000 inspect.py:752(_is_wrapper)\n", - " 1 0.000 0.000 0.000 0.000 maze_dataset.py:91(grid_shape)\n", - " 1 0.000 0.000 0.000 0.000 {built-in method builtins.callable}\n", - " 1 0.000 0.000 0.000 0.000 fromnumeric.py:2172(_sum_dispatcher)\n", - " 1 0.000 0.000 0.000 0.000 {method 'end' of 're.Match' objects}\n", - " 1 0.000 0.000 0.000 0.000 {method 'values' of 'dict' objects}\n", - " 1 0.000 0.000 0.000 0.000 {built-in method sys.getrecursionlimit}\n", - " 1 0.000 0.000 0.000 0.000 {built-in method builtins.iter}\n", - " 1 0.000 0.000 0.000 0.000 maze_dataset.py:82()\n", - "\n", - "\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "SPEEDS[\"serialize_minimal_soln_cat:profiling\"][len(SPEEDS) - 1].sort_stats(\n", - " \"tottime\"\n", - ").print_stats()" + "SPEEDS['load_minimal:profiling'][len(SPEEDS)-1].sort_stats('tottime').print_stats()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/poetry.lock b/poetry.lock index 6add1d8d..43bfe5a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -143,32 +143,32 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "23.2.0" +version = "24.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, - {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, ] [package.extras] -cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[tests]", "pre-commit"] -docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] -tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] -tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "babel" -version = "2.15.0" +version = "2.16.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ - {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, - {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, + {file = "babel-2.16.0-py3-none-any.whl", hash = "sha256:368b5b98b37c06b7daf6696391c3240c938b37767d4584413e8438c5c435fa8b"}, + {file = "babel-2.16.0.tar.gz", hash = "sha256:d1f3554ca26605fe173f3de0c65f750f5a42f924499bf134de6423582298e316"}, ] [package.extras] @@ -197,33 +197,33 @@ lxml = ["lxml"] [[package]] name = "black" -version = "24.4.2" +version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [package.dependencies] @@ -272,63 +272,78 @@ files = [ [[package]] name = "cffi" -version = "1.16.0" +version = "1.17.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" files = [ - {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, - {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, - {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, - {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, - {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, - {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, - {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, - {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, - {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, - {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, - {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, - {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, - {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, - {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, - {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, - {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, - {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, - {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, - {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, - {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, - {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, - {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, - {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, - {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, - {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, - {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, ] [package.dependencies] @@ -477,126 +492,166 @@ test = ["pytest"] [[package]] name = "contourpy" -version = "1.2.1" +version = "1.3.0" description = "Python library for calculating contours of 2D quadrilateral grids" optional = false python-versions = ">=3.9" files = [ - {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, - {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, - {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, - {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, - {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, - {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, - {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, - {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, - {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, - {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, - {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, - {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, - {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, - {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, - {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, - {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, - {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, - {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, - {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, - {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, - {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, - {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, - {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, - {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, - {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, - {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, - {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, - {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, - {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, - {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, - {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, - {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, -] - -[package.dependencies] -numpy = ">=1.20" + {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"}, + {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"}, + {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"}, + {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"}, + {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"}, + {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"}, + {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"}, + {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"}, + {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"}, + {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"}, + {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"}, + {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"}, + {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"}, + {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"}, + {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"}, + {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"}, + {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"}, + {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"}, + {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"}, + {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"}, + {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"}, + {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"}, + {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"}, + {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"}, + {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"}, + {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"}, + {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"}, + {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"}, + {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"}, + {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"}, + {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"}, + {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"}, + {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"}, + {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"}, + {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"}, + {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"}, + {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"}, + {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"}, + {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"}, + {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"}, + {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"}, + {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"}, +] + +[package.dependencies] +numpy = ">=1.23" [package.extras] bokeh = ["bokeh", "selenium"] docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"] test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.6.0" +version = "7.6.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dff044f661f59dace805eedb4a7404c573b6ff0cdba4a524141bc63d7be5c7fd"}, - {file = "coverage-7.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a8659fd33ee9e6ca03950cfdcdf271d645cf681609153f218826dd9805ab585c"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7792f0ab20df8071d669d929c75c97fecfa6bcab82c10ee4adb91c7a54055463"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b3cd1ca7cd73d229487fa5caca9e4bc1f0bca96526b922d61053ea751fe791"}, - {file = "coverage-7.6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7e128f85c0b419907d1f38e616c4f1e9f1d1b37a7949f44df9a73d5da5cd53c"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a94925102c89247530ae1dab7dc02c690942566f22e189cbd53579b0693c0783"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:dcd070b5b585b50e6617e8972f3fbbee786afca71b1936ac06257f7e178f00f6"}, - {file = "coverage-7.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d50a252b23b9b4dfeefc1f663c568a221092cbaded20a05a11665d0dbec9b8fb"}, - {file = "coverage-7.6.0-cp310-cp310-win32.whl", hash = "sha256:0e7b27d04131c46e6894f23a4ae186a6a2207209a05df5b6ad4caee6d54a222c"}, - {file = "coverage-7.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:54dece71673b3187c86226c3ca793c5f891f9fc3d8aa183f2e3653da18566169"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c7b525ab52ce18c57ae232ba6f7010297a87ced82a2383b1afd238849c1ff933"}, - {file = "coverage-7.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bea27c4269234e06f621f3fac3925f56ff34bc14521484b8f66a580aacc2e7d"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed8d1d1821ba5fc88d4a4f45387b65de52382fa3ef1f0115a4f7a20cdfab0e94"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01c322ef2bbe15057bc4bf132b525b7e3f7206f071799eb8aa6ad1940bcf5fb1"}, - {file = "coverage-7.6.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03cafe82c1b32b770a29fd6de923625ccac3185a54a5e66606da26d105f37dac"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d1b923fc4a40c5832be4f35a5dab0e5ff89cddf83bb4174499e02ea089daf57"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4b03741e70fb811d1a9a1d75355cf391f274ed85847f4b78e35459899f57af4d"}, - {file = "coverage-7.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a73d18625f6a8a1cbb11eadc1d03929f9510f4131879288e3f7922097a429f63"}, - {file = "coverage-7.6.0-cp311-cp311-win32.whl", hash = "sha256:65fa405b837060db569a61ec368b74688f429b32fa47a8929a7a2f9b47183713"}, - {file = "coverage-7.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6379688fb4cfa921ae349c76eb1a9ab26b65f32b03d46bb0eed841fd4cb6afb1"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f7db0b6ae1f96ae41afe626095149ecd1b212b424626175a6633c2999eaad45b"}, - {file = "coverage-7.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbdf9a72403110a3bdae77948b8011f644571311c2fb35ee15f0f10a8fc082e8"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc44bf0315268e253bf563f3560e6c004efe38f76db03a1558274a6e04bf5d5"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da8549d17489cd52f85a9829d0e1d91059359b3c54a26f28bec2c5d369524807"}, - {file = "coverage-7.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0086cd4fc71b7d485ac93ca4239c8f75732c2ae3ba83f6be1c9be59d9e2c6382"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fad32ee9b27350687035cb5fdf9145bc9cf0a094a9577d43e909948ebcfa27b"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:044a0985a4f25b335882b0966625270a8d9db3d3409ddc49a4eb00b0ef5e8cee"}, - {file = "coverage-7.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76d5f82213aa78098b9b964ea89de4617e70e0d43e97900c2778a50856dac605"}, - {file = "coverage-7.6.0-cp312-cp312-win32.whl", hash = "sha256:3c59105f8d58ce500f348c5b56163a4113a440dad6daa2294b5052a10db866da"}, - {file = "coverage-7.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca5d79cfdae420a1d52bf177de4bc2289c321d6c961ae321503b2ca59c17ae67"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d39bd10f0ae453554798b125d2f39884290c480f56e8a02ba7a6ed552005243b"}, - {file = "coverage-7.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beb08e8508e53a568811016e59f3234d29c2583f6b6e28572f0954a6b4f7e03d"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2e16f4cd2bc4d88ba30ca2d3bbf2f21f00f382cf4e1ce3b1ddc96c634bc48ca"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6616d1c9bf1e3faea78711ee42a8b972367d82ceae233ec0ac61cc7fec09fa6b"}, - {file = "coverage-7.6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad4567d6c334c46046d1c4c20024de2a1c3abc626817ae21ae3da600f5779b44"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d17c6a415d68cfe1091d3296ba5749d3d8696e42c37fca5d4860c5bf7b729f03"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9146579352d7b5f6412735d0f203bbd8d00113a680b66565e205bc605ef81bc6"}, - {file = "coverage-7.6.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cdab02a0a941af190df8782aafc591ef3ad08824f97850b015c8c6a8b3877b0b"}, - {file = "coverage-7.6.0-cp38-cp38-win32.whl", hash = "sha256:df423f351b162a702c053d5dddc0fc0ef9a9e27ea3f449781ace5f906b664428"}, - {file = "coverage-7.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:f2501d60d7497fd55e391f423f965bbe9e650e9ffc3c627d5f0ac516026000b8"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7221f9ac9dad9492cecab6f676b3eaf9185141539d5c9689d13fd6b0d7de840c"}, - {file = "coverage-7.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ddaaa91bfc4477d2871442bbf30a125e8fe6b05da8a0015507bfbf4718228ab2"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4cbe651f3904e28f3a55d6f371203049034b4ddbce65a54527a3f189ca3b390"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:831b476d79408ab6ccfadaaf199906c833f02fdb32c9ab907b1d4aa0713cfa3b"}, - {file = "coverage-7.6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46c3d091059ad0b9c59d1034de74a7f36dcfa7f6d3bde782c49deb42438f2450"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4d5fae0a22dc86259dee66f2cc6c1d3e490c4a1214d7daa2a93d07491c5c04b6"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:07ed352205574aad067482e53dd606926afebcb5590653121063fbf4e2175166"}, - {file = "coverage-7.6.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:49c76cdfa13015c4560702574bad67f0e15ca5a2872c6a125f6327ead2b731dd"}, - {file = "coverage-7.6.0-cp39-cp39-win32.whl", hash = "sha256:482855914928c8175735a2a59c8dc5806cf7d8f032e4820d52e845d1f731dca2"}, - {file = "coverage-7.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:543ef9179bc55edfd895154a51792b01c017c87af0ebaae092720152e19e42ca"}, - {file = "coverage-7.6.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:6fe885135c8a479d3e37a7aae61cbd3a0fb2deccb4dda3c25f92a49189f766d6"}, - {file = "coverage-7.6.0.tar.gz", hash = "sha256:289cc803fa1dc901f84701ac10c9ee873619320f2f9aff38794db4a4a0268d51"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, ] [package.dependencies] @@ -607,17 +662,18 @@ toml = ["tomli"] [[package]] name = "coverage-badge" -version = "1.1.1" +version = "1.1.2" description = "Generate coverage badges for Coverage.py." optional = false python-versions = "*" files = [ - {file = "coverage-badge-1.1.1.tar.gz", hash = "sha256:42252df917404af6147380861228a4ace3d9a29804df8fc2d34a22b2bc4f45b6"}, - {file = "coverage_badge-1.1.1-py2.py3-none-any.whl", hash = "sha256:1d8e566ad47c37910fa2bbc74ea19972b171b5b4e40624b31b3e2f2d93680266"}, + {file = "coverage_badge-1.1.2-py2.py3-none-any.whl", hash = "sha256:d8413ce51c91043a1692b943616b450868cbeeb0ea6a0c54a32f8318c9c96ff7"}, + {file = "coverage_badge-1.1.2.tar.gz", hash = "sha256:fe7ed58a3b72dad85a553b64a99e963dea3847dcd0b8ddd2b38a00333618642c"}, ] [package.dependencies] coverage = "*" +setuptools = "*" [[package]] name = "cycler" @@ -636,33 +692,33 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "debugpy" -version = "1.8.2" +version = "1.8.5" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:7ee2e1afbf44b138c005e4380097d92532e1001580853a7cb40ed84e0ef1c3d2"}, - {file = "debugpy-1.8.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f8c3f7c53130a070f0fc845a0f2cee8ed88d220d6b04595897b66605df1edd6"}, - {file = "debugpy-1.8.2-cp310-cp310-win32.whl", hash = "sha256:f179af1e1bd4c88b0b9f0fa153569b24f6b6f3de33f94703336363ae62f4bf47"}, - {file = "debugpy-1.8.2-cp310-cp310-win_amd64.whl", hash = "sha256:0600faef1d0b8d0e85c816b8bb0cb90ed94fc611f308d5fde28cb8b3d2ff0fe3"}, - {file = "debugpy-1.8.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:8a13417ccd5978a642e91fb79b871baded925d4fadd4dfafec1928196292aa0a"}, - {file = "debugpy-1.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acdf39855f65c48ac9667b2801234fc64d46778021efac2de7e50907ab90c634"}, - {file = "debugpy-1.8.2-cp311-cp311-win32.whl", hash = "sha256:2cbd4d9a2fc5e7f583ff9bf11f3b7d78dfda8401e8bb6856ad1ed190be4281ad"}, - {file = "debugpy-1.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:d3408fddd76414034c02880e891ea434e9a9cf3a69842098ef92f6e809d09afa"}, - {file = "debugpy-1.8.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:5d3ccd39e4021f2eb86b8d748a96c766058b39443c1f18b2dc52c10ac2757835"}, - {file = "debugpy-1.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62658aefe289598680193ff655ff3940e2a601765259b123dc7f89c0239b8cd3"}, - {file = "debugpy-1.8.2-cp312-cp312-win32.whl", hash = "sha256:bd11fe35d6fd3431f1546d94121322c0ac572e1bfb1f6be0e9b8655fb4ea941e"}, - {file = "debugpy-1.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:15bc2f4b0f5e99bf86c162c91a74c0631dbd9cef3c6a1d1329c946586255e859"}, - {file = "debugpy-1.8.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:5a019d4574afedc6ead1daa22736c530712465c0c4cd44f820d803d937531b2d"}, - {file = "debugpy-1.8.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40f062d6877d2e45b112c0bbade9a17aac507445fd638922b1a5434df34aed02"}, - {file = "debugpy-1.8.2-cp38-cp38-win32.whl", hash = "sha256:c78ba1680f1015c0ca7115671fe347b28b446081dada3fedf54138f44e4ba031"}, - {file = "debugpy-1.8.2-cp38-cp38-win_amd64.whl", hash = "sha256:cf327316ae0c0e7dd81eb92d24ba8b5e88bb4d1b585b5c0d32929274a66a5210"}, - {file = "debugpy-1.8.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:1523bc551e28e15147815d1397afc150ac99dbd3a8e64641d53425dba57b0ff9"}, - {file = "debugpy-1.8.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e24ccb0cd6f8bfaec68d577cb49e9c680621c336f347479b3fce060ba7c09ec1"}, - {file = "debugpy-1.8.2-cp39-cp39-win32.whl", hash = "sha256:7f8d57a98c5a486c5c7824bc0b9f2f11189d08d73635c326abef268f83950326"}, - {file = "debugpy-1.8.2-cp39-cp39-win_amd64.whl", hash = "sha256:16c8dcab02617b75697a0a925a62943e26a0330da076e2a10437edd9f0bf3755"}, - {file = "debugpy-1.8.2-py2.py3-none-any.whl", hash = "sha256:16e16df3a98a35c63c3ab1e4d19be4cbc7fdda92d9ddc059294f18910928e0ca"}, - {file = "debugpy-1.8.2.zip", hash = "sha256:95378ed08ed2089221896b9b3a8d021e642c24edc8fef20e5d4342ca8be65c00"}, + {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, + {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, + {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, + {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, + {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, + {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, + {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, + {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, + {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, + {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, + {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, + {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, + {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, + {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, + {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, + {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, + {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, + {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, + {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, + {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, + {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, + {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, ] [[package]] @@ -701,6 +757,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "executing" version = "2.0.1" @@ -821,6 +891,48 @@ files = [ {file = "fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f"}, ] +[[package]] +name = "frozendict" +version = "2.4.4" +description = "A simple immutable dictionary" +optional = false +python-versions = ">=3.6" +files = [ + {file = "frozendict-2.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4a59578d47b3949437519b5c39a016a6116b9e787bb19289e333faae81462e59"}, + {file = "frozendict-2.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a342e439aef28ccec533f0253ea53d75fe9102bd6ea928ff530e76eac38906"}, + {file = "frozendict-2.4.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f79c26dff10ce11dad3b3627c89bb2e87b9dd5958c2b24325f16a23019b8b94"}, + {file = "frozendict-2.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2bd009cf4fc47972838a91e9b83654dc9a095dc4f2bb3a37c3f3124c8a364543"}, + {file = "frozendict-2.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:87ebcde21565a14fe039672c25550060d6f6d88cf1f339beac094c3b10004eb0"}, + {file = "frozendict-2.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:fefeb700bc7eb8b4c2dc48704e4221860d254c8989fb53488540bc44e44a1ac2"}, + {file = "frozendict-2.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:4297d694eb600efa429769125a6f910ec02b85606f22f178bafbee309e7d3ec7"}, + {file = "frozendict-2.4.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:812ab17522ba13637826e65454115a914c2da538356e85f43ecea069813e4b33"}, + {file = "frozendict-2.4.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fee9420475bb6ff357000092aa9990c2f6182b2bab15764330f4ad7de2eae49"}, + {file = "frozendict-2.4.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:3148062675536724502c6344d7c485dd4667fdf7980ca9bd05e338ccc0c4471e"}, + {file = "frozendict-2.4.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:78c94991944dd33c5376f720228e5b252ee67faf3bac50ef381adc9e51e90d9d"}, + {file = "frozendict-2.4.4-cp36-cp36m-win_amd64.whl", hash = "sha256:1697793b5f62b416c0fc1d94638ec91ed3aa4ab277f6affa3a95216ecb3af170"}, + {file = "frozendict-2.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:199a4d32194f3afed6258de7e317054155bc9519252b568d9cfffde7e4d834e5"}, + {file = "frozendict-2.4.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85375ec6e979e6373bffb4f54576a68bf7497c350861d20686ccae38aab69c0a"}, + {file = "frozendict-2.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2d8536e068d6bf281f23fa835ac07747fb0f8851879dd189e9709f9567408b4d"}, + {file = "frozendict-2.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:259528ba6b56fa051bc996f1c4d8b57e30d6dd3bc2f27441891b04babc4b5e73"}, + {file = "frozendict-2.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:07c3a5dee8bbb84cba770e273cdbf2c87c8e035903af8f781292d72583416801"}, + {file = "frozendict-2.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6874fec816b37b6eb5795b00e0574cba261bf59723e2de607a195d5edaff0786"}, + {file = "frozendict-2.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8f92425686323a950337da4b75b4c17a3327b831df8c881df24038d560640d4"}, + {file = "frozendict-2.4.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d58d9a8d9e49662c6dafbea5e641f97decdb3d6ccd76e55e79818415362ba25"}, + {file = "frozendict-2.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:93a7b19afb429cbf99d56faf436b45ef2fa8fe9aca89c49eb1610c3bd85f1760"}, + {file = "frozendict-2.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b70b431e3a72d410a2cdf1497b3aba2f553635e0c0f657ce311d841bf8273b6"}, + {file = "frozendict-2.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:e1b941132d79ce72d562a13341d38fc217bc1ee24d8c35a20d754e79ff99e038"}, + {file = "frozendict-2.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc2228874eacae390e63fd4f2bb513b3144066a977dc192163c9f6c7f6de6474"}, + {file = "frozendict-2.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63aa49f1919af7d45fb8fd5dec4c0859bc09f46880bd6297c79bb2db2969b63d"}, + {file = "frozendict-2.4.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6bf9260018d653f3cab9bd147bd8592bf98a5c6e338be0491ced3c196c034a3"}, + {file = "frozendict-2.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6eb716e6a6d693c03b1d53280a1947716129f5ef9bcdd061db5c17dea44b80fe"}, + {file = "frozendict-2.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d13b4310db337f4d2103867c5a05090b22bc4d50ca842093779ef541ea9c9eea"}, + {file = "frozendict-2.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:b3b967d5065872e27b06f785a80c0ed0a45d1f7c9b85223da05358e734d858ca"}, + {file = "frozendict-2.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:4ae8d05c8d0b6134bfb6bfb369d5fa0c4df21eabb5ca7f645af95fdc6689678e"}, + {file = "frozendict-2.4.4-py311-none-any.whl", hash = "sha256:705efca8d74d3facbb6ace80ab3afdd28eb8a237bfb4063ed89996b024bc443d"}, + {file = "frozendict-2.4.4-py312-none-any.whl", hash = "sha256:d9647563e76adb05b7cde2172403123380871360a114f546b4ae1704510801e5"}, + {file = "frozendict-2.4.4.tar.gz", hash = "sha256:3f7c031b26e4ee6a3f786ceb5e3abf1181c4ade92dce1f847da26ea2c96008c7"}, +] + [[package]] name = "fsspec" version = "2024.6.1" @@ -894,13 +1006,13 @@ trio = ["trio (>=0.22.0,<0.26.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -915,16 +1027,17 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.7" +version = "3.8" description = "Internationalized Domain Names in Applications (IDNA)" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" files = [ - {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, - {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, ] [[package]] @@ -1011,21 +1124,21 @@ test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "num [[package]] name = "ipywidgets" -version = "8.1.3" +version = "8.1.5" description = "Jupyter interactive widgets" optional = false python-versions = ">=3.7" files = [ - {file = "ipywidgets-8.1.3-py3-none-any.whl", hash = "sha256:efafd18f7a142248f7cb0ba890a68b96abd4d6e88ddbda483c9130d12667eaf2"}, - {file = "ipywidgets-8.1.3.tar.gz", hash = "sha256:f5f9eeaae082b1823ce9eac2575272952f40d748893972956dc09700a6392d9c"}, + {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, + {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, ] [package.dependencies] comm = ">=0.1.3" ipython = ">=6.1.0" -jupyterlab-widgets = ">=3.0.11,<3.1.0" +jupyterlab-widgets = ">=3.0.12,<3.1.0" traitlets = ">=4.3.1" -widgetsnbextension = ">=4.0.11,<4.1.0" +widgetsnbextension = ">=4.0.12,<4.1.0" [package.extras] test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] @@ -1355,13 +1468,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.4" +version = "4.2.5" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.4-py3-none-any.whl", hash = "sha256:807a7ec73637744f879e112060d4b9d9ebe028033b7a429b2d1f4fc523d00245"}, - {file = "jupyterlab-4.2.4.tar.gz", hash = "sha256:343a979fb9582fd08c8511823e320703281cd072a0049bcdafdc7afeda7f2537"}, + {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"}, + {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"}, ] [package.dependencies] @@ -1425,13 +1538,13 @@ test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-v [[package]] name = "jupyterlab-widgets" -version = "3.0.11" +version = "3.0.13" description = "Jupyter interactive widgets for JupyterLab" optional = false python-versions = ">=3.7" files = [ - {file = "jupyterlab_widgets-3.0.11-py3-none-any.whl", hash = "sha256:78287fd86d20744ace330a61625024cf5521e1c012a352ddc0a3cdc2348becd0"}, - {file = "jupyterlab_widgets-3.0.11.tar.gz", hash = "sha256:dd5ac679593c969af29c9bed054c24f26842baa51352114736756bc035deee27"}, + {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, + {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, ] [[package]] @@ -1682,40 +1795,51 @@ files = [ [[package]] name = "matplotlib" -version = "3.9.1" +version = "3.9.2" description = "Python plotting package" optional = false python-versions = ">=3.9" files = [ - {file = "matplotlib-3.9.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ccd6270066feb9a9d8e0705aa027f1ff39f354c72a87efe8fa07632f30fc6bb"}, - {file = "matplotlib-3.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:591d3a88903a30a6d23b040c1e44d1afdd0d778758d07110eb7596f811f31842"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd2a59ff4b83d33bca3b5ec58203cc65985367812cb8c257f3e101632be86d92"}, - {file = "matplotlib-3.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc001516ffcf1a221beb51198b194d9230199d6842c540108e4ce109ac05cc0"}, - {file = "matplotlib-3.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:83c6a792f1465d174c86d06f3ae85a8fe36e6f5964633ae8106312ec0921fdf5"}, - {file = "matplotlib-3.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:421851f4f57350bcf0811edd754a708d2275533e84f52f6760b740766c6747a7"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:b3fce58971b465e01b5c538f9d44915640c20ec5ff31346e963c9e1cd66fa812"}, - {file = "matplotlib-3.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a973c53ad0668c53e0ed76b27d2eeeae8799836fd0d0caaa4ecc66bf4e6676c0"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd5acf8f3ef43f7532c2f230249720f5dc5dd40ecafaf1c60ac8200d46d7eb"}, - {file = "matplotlib-3.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab38a4f3772523179b2f772103d8030215b318fef6360cb40558f585bf3d017f"}, - {file = "matplotlib-3.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2315837485ca6188a4b632c5199900e28d33b481eb083663f6a44cfc8987ded3"}, - {file = "matplotlib-3.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:a0c977c5c382f6696caf0bd277ef4f936da7e2aa202ff66cad5f0ac1428ee15b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:565d572efea2b94f264dd86ef27919515aa6d629252a169b42ce5f570db7f37b"}, - {file = "matplotlib-3.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d397fd8ccc64af2ec0af1f0efc3bacd745ebfb9d507f3f552e8adb689ed730a"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26040c8f5121cd1ad712abffcd4b5222a8aec3a0fe40bc8542c94331deb8780d"}, - {file = "matplotlib-3.9.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d12cb1837cffaac087ad6b44399d5e22b78c729de3cdae4629e252067b705e2b"}, - {file = "matplotlib-3.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0e835c6988edc3d2d08794f73c323cc62483e13df0194719ecb0723b564e0b5c"}, - {file = "matplotlib-3.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:44a21d922f78ce40435cb35b43dd7d573cf2a30138d5c4b709d19f00e3907fd7"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0c584210c755ae921283d21d01f03a49ef46d1afa184134dd0f95b0202ee6f03"}, - {file = "matplotlib-3.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11fed08f34fa682c2b792942f8902e7aefeed400da71f9e5816bea40a7ce28fe"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0000354e32efcfd86bda75729716b92f5c2edd5b947200be9881f0a671565c33"}, - {file = "matplotlib-3.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db17fea0ae3aceb8e9ac69c7e3051bae0b3d083bfec932240f9bf5d0197a049"}, - {file = "matplotlib-3.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:208cbce658b72bf6a8e675058fbbf59f67814057ae78165d8a2f87c45b48d0ff"}, - {file = "matplotlib-3.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:dc23f48ab630474264276be156d0d7710ac6c5a09648ccdf49fef9200d8cbe80"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3fda72d4d472e2ccd1be0e9ccb6bf0d2eaf635e7f8f51d737ed7e465ac020cb3"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:84b3ba8429935a444f1fdc80ed930babbe06725bcf09fbeb5c8757a2cd74af04"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b918770bf3e07845408716e5bbda17eadfc3fcbd9307dc67f37d6cf834bb3d98"}, - {file = "matplotlib-3.9.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f1f2e5d29e9435c97ad4c36fb6668e89aee13d48c75893e25cef064675038ac9"}, - {file = "matplotlib-3.9.1.tar.gz", hash = "sha256:de06b19b8db95dd33d0dc17c926c7c9ebed9f572074b6fac4f65068a6814d010"}, + {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"}, + {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"}, + {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"}, + {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"}, + {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"}, + {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"}, + {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"}, + {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"}, + {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"}, + {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"}, + {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"}, + {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"}, + {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"}, + {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"}, + {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"}, + {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"}, + {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"}, + {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"}, + {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"}, + {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"}, + {file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"}, + {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"}, + {file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"}, + {file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"}, + {file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"}, + {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"}, ] [package.dependencies] @@ -1787,13 +1911,13 @@ tests = ["pytest (>=4.6)"] [[package]] name = "muutils" -version = "0.6.7" +version = "0.6.10" description = "miscellaneous python utilities" optional = false python-versions = "<4.0,>=3.8" files = [ - {file = "muutils-0.6.7-py3-none-any.whl", hash = "sha256:91cc352a16f701f00b476fd53d9d3f858d785cc6275faabd620d34150940712c"}, - {file = "muutils-0.6.7.tar.gz", hash = "sha256:06d40f6e6c5390418b091a7326f7d78bc4470cc5741a13f4a4160c382f8c1eae"}, + {file = "muutils-0.6.10-py3-none-any.whl", hash = "sha256:a7f4ba138d86a3981ff2ca5d2e7a00102ce12fc0f2009bd8041aa577cad642d5"}, + {file = "muutils-0.6.10.tar.gz", hash = "sha256:3aff934606f485c89f32feec11432e74fcb8dfd6e4f18486bbdc32696cf86a6e"}, ] [package.dependencies] @@ -1810,38 +1934,38 @@ zanj = ["zanj (>=0.3.0,<0.4.0)"] [[package]] name = "mypy" -version = "1.11.0" +version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"}, - {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"}, - {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"}, - {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"}, - {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"}, - {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"}, - {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"}, - {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"}, - {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"}, - {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"}, - {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"}, - {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"}, - {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"}, - {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"}, - {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"}, - {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"}, - {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"}, - {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"}, - {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [package.dependencies] @@ -1977,13 +2101,13 @@ test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"] [[package]] name = "notebook" -version = "7.2.1" +version = "7.2.2" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.1-py3-none-any.whl", hash = "sha256:f45489a3995746f2195a137e0773e2130960b51c9ac3ce257dbc2705aab3a6ca"}, - {file = "notebook-7.2.1.tar.gz", hash = "sha256:4287b6da59740b32173d01d641f763d292f49c30e7a51b89c46ba8473126341e"}, + {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"}, + {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"}, ] [package.dependencies] @@ -2461,13 +2585,13 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyparsing" -version = "3.1.2" +version = "3.1.4" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.6.8" files = [ - {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, - {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, + {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, + {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, ] [package.extras] @@ -2530,6 +2654,26 @@ pytest = ">=6.2.5" [package.extras] dev = ["pre-commit", "pytest-asyncio", "tox"] +[[package]] +name = "pytest-xdist" +version = "3.6.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] + +[package.dependencies] +execnet = ">=2.1" +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2606,158 +2750,182 @@ files = [ [[package]] name = "pyyaml" -version = "6.0.1" +version = "6.0.2" description = "YAML parser and emitter for Python" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, - {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, - {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, - {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, - {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, - {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, - {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, - {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, - {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, - {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, - {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, - {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, - {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, - {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, - {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, - {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, - {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, - {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, - {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, - {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, - {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, - {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, - {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, - {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, - {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, - {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, - {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, - {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, - {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, - {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, - {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, - {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, - {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "pyzmq" -version = "26.0.3" +version = "26.2.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" files = [ - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:44dd6fc3034f1eaa72ece33588867df9e006a7303725a12d64c3dff92330f625"}, - {file = "pyzmq-26.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:acb704195a71ac5ea5ecf2811c9ee19ecdc62b91878528302dd0be1b9451cc90"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbb9c997932473a27afa93954bb77a9f9b786b4ccf718d903f35da3232317de"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6bcb34f869d431799c3ee7d516554797f7760cb2198ecaa89c3f176f72d062be"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38ece17ec5f20d7d9b442e5174ae9f020365d01ba7c112205a4d59cf19dc38ee"}, - {file = "pyzmq-26.0.3-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:ba6e5e6588e49139a0979d03a7deb9c734bde647b9a8808f26acf9c547cab1bf"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:3bf8b000a4e2967e6dfdd8656cd0757d18c7e5ce3d16339e550bd462f4857e59"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:2136f64fbb86451dbbf70223635a468272dd20075f988a102bf8a3f194a411dc"}, - {file = "pyzmq-26.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e8918973fbd34e7814f59143c5f600ecd38b8038161239fd1a3d33d5817a38b8"}, - {file = "pyzmq-26.0.3-cp310-cp310-win32.whl", hash = "sha256:0aaf982e68a7ac284377d051c742610220fd06d330dcd4c4dbb4cdd77c22a537"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:f1a9b7d00fdf60b4039f4455afd031fe85ee8305b019334b72dcf73c567edc47"}, - {file = "pyzmq-26.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:80b12f25d805a919d53efc0a5ad7c0c0326f13b4eae981a5d7b7cc343318ebb7"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:a72a84570f84c374b4c287183debc776dc319d3e8ce6b6a0041ce2e400de3f32"}, - {file = "pyzmq-26.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7ca684ee649b55fd8f378127ac8462fb6c85f251c2fb027eb3c887e8ee347bcd"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e222562dc0f38571c8b1ffdae9d7adb866363134299264a1958d077800b193b7"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f17cde1db0754c35a91ac00b22b25c11da6eec5746431d6e5092f0cd31a3fea9"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7c0c0b3244bb2275abe255d4a30c050d541c6cb18b870975553f1fb6f37527"}, - {file = "pyzmq-26.0.3-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ac97a21de3712afe6a6c071abfad40a6224fd14fa6ff0ff8d0c6e6cd4e2f807a"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:88b88282e55fa39dd556d7fc04160bcf39dea015f78e0cecec8ff4f06c1fc2b5"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:72b67f966b57dbd18dcc7efbc1c7fc9f5f983e572db1877081f075004614fcdd"}, - {file = "pyzmq-26.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4b6cecbbf3b7380f3b61de3a7b93cb721125dc125c854c14ddc91225ba52f83"}, - {file = "pyzmq-26.0.3-cp311-cp311-win32.whl", hash = "sha256:eed56b6a39216d31ff8cd2f1d048b5bf1700e4b32a01b14379c3b6dde9ce3aa3"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:3191d312c73e3cfd0f0afdf51df8405aafeb0bad71e7ed8f68b24b63c4f36500"}, - {file = "pyzmq-26.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:b6907da3017ef55139cf0e417c5123a84c7332520e73a6902ff1f79046cd3b94"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:068ca17214038ae986d68f4a7021f97e187ed278ab6dccb79f837d765a54d753"}, - {file = "pyzmq-26.0.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7821d44fe07335bea256b9f1f41474a642ca55fa671dfd9f00af8d68a920c2d4"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eeb438a26d87c123bb318e5f2b3d86a36060b01f22fbdffd8cf247d52f7c9a2b"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69ea9d6d9baa25a4dc9cef5e2b77b8537827b122214f210dd925132e34ae9b12"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7daa3e1369355766dea11f1d8ef829905c3b9da886ea3152788dc25ee6079e02"}, - {file = "pyzmq-26.0.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6ca7a9a06b52d0e38ccf6bca1aeff7be178917893f3883f37b75589d42c4ac20"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1b7d0e124948daa4d9686d421ef5087c0516bc6179fdcf8828b8444f8e461a77"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e746524418b70f38550f2190eeee834db8850088c834d4c8406fbb9bc1ae10b2"}, - {file = "pyzmq-26.0.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:6b3146f9ae6af82c47a5282ac8803523d381b3b21caeae0327ed2f7ecb718798"}, - {file = "pyzmq-26.0.3-cp312-cp312-win32.whl", hash = "sha256:2b291d1230845871c00c8462c50565a9cd6026fe1228e77ca934470bb7d70ea0"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:926838a535c2c1ea21c903f909a9a54e675c2126728c21381a94ddf37c3cbddf"}, - {file = "pyzmq-26.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:5bf6c237f8c681dfb91b17f8435b2735951f0d1fad10cc5dfd96db110243370b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c0991f5a96a8e620f7691e61178cd8f457b49e17b7d9cfa2067e2a0a89fc1d5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dbf012d8fcb9f2cf0643b65df3b355fdd74fc0035d70bb5c845e9e30a3a4654b"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:01fbfbeb8249a68d257f601deb50c70c929dc2dfe683b754659569e502fbd3aa"}, - {file = "pyzmq-26.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c8eb19abe87029c18f226d42b8a2c9efdd139d08f8bf6e085dd9075446db450"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5344b896e79800af86ad643408ca9aa303a017f6ebff8cee5a3163c1e9aec987"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:204e0f176fd1d067671157d049466869b3ae1fc51e354708b0dc41cf94e23a3a"}, - {file = "pyzmq-26.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a42db008d58530efa3b881eeee4991146de0b790e095f7ae43ba5cc612decbc5"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win32.whl", hash = "sha256:8d7a498671ca87e32b54cb47c82a92b40130a26c5197d392720a1bce1b3c77cf"}, - {file = "pyzmq-26.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:3b4032a96410bdc760061b14ed6a33613ffb7f702181ba999df5d16fb96ba16a"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2cc4e280098c1b192c42a849de8de2c8e0f3a84086a76ec5b07bfee29bda7d18"}, - {file = "pyzmq-26.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5bde86a2ed3ce587fa2b207424ce15b9a83a9fa14422dcc1c5356a13aed3df9d"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:34106f68e20e6ff253c9f596ea50397dbd8699828d55e8fa18bd4323d8d966e6"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ebbbd0e728af5db9b04e56389e2299a57ea8b9dd15c9759153ee2455b32be6ad"}, - {file = "pyzmq-26.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b1d1c631e5940cac5a0b22c5379c86e8df6a4ec277c7a856b714021ab6cfad"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e891ce81edd463b3b4c3b885c5603c00141151dd9c6936d98a680c8c72fe5c67"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9b273ecfbc590a1b98f014ae41e5cf723932f3b53ba9367cfb676f838038b32c"}, - {file = "pyzmq-26.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b32bff85fb02a75ea0b68f21e2412255b5731f3f389ed9aecc13a6752f58ac97"}, - {file = "pyzmq-26.0.3-cp38-cp38-win32.whl", hash = "sha256:f6c21c00478a7bea93caaaef9e7629145d4153b15a8653e8bb4609d4bc70dbfc"}, - {file = "pyzmq-26.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:3401613148d93ef0fd9aabdbddb212de3db7a4475367f49f590c837355343972"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:2ed8357f4c6e0daa4f3baf31832df8a33334e0fe5b020a61bc8b345a3db7a606"}, - {file = "pyzmq-26.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c1c8f2a2ca45292084c75bb6d3a25545cff0ed931ed228d3a1810ae3758f975f"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b63731993cdddcc8e087c64e9cf003f909262b359110070183d7f3025d1c56b5"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b3cd31f859b662ac5d7f4226ec7d8bd60384fa037fc02aee6ff0b53ba29a3ba8"}, - {file = "pyzmq-26.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:115f8359402fa527cf47708d6f8a0f8234f0e9ca0cab7c18c9c189c194dbf620"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:715bdf952b9533ba13dfcf1f431a8f49e63cecc31d91d007bc1deb914f47d0e4"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e1258c639e00bf5e8a522fec6c3eaa3e30cf1c23a2f21a586be7e04d50c9acab"}, - {file = "pyzmq-26.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:15c59e780be8f30a60816a9adab900c12a58d79c1ac742b4a8df044ab2a6d920"}, - {file = "pyzmq-26.0.3-cp39-cp39-win32.whl", hash = "sha256:d0cdde3c78d8ab5b46595054e5def32a755fc028685add5ddc7403e9f6de9879"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:ce828058d482ef860746bf532822842e0ff484e27f540ef5c813d516dd8896d2"}, - {file = "pyzmq-26.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:788f15721c64109cf720791714dc14afd0f449d63f3a5487724f024345067381"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c18645ef6294d99b256806e34653e86236eb266278c8ec8112622b61db255de"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e6bc96ebe49604df3ec2c6389cc3876cabe475e6bfc84ced1bf4e630662cb35"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:971e8990c5cc4ddcff26e149398fc7b0f6a042306e82500f5e8db3b10ce69f84"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8416c23161abd94cc7da80c734ad7c9f5dbebdadfdaa77dad78244457448223"}, - {file = "pyzmq-26.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:082a2988364b60bb5de809373098361cf1dbb239623e39e46cb18bc035ed9c0c"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d57dfbf9737763b3a60d26e6800e02e04284926329aee8fb01049635e957fe81"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:77a85dca4c2430ac04dc2a2185c2deb3858a34fe7f403d0a946fa56970cf60a1"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4c82a6d952a1d555bf4be42b6532927d2a5686dd3c3e280e5f63225ab47ac1f5"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4496b1282c70c442809fc1b151977c3d967bfb33e4e17cedbf226d97de18f709"}, - {file = "pyzmq-26.0.3-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:e4946d6bdb7ba972dfda282f9127e5756d4f299028b1566d1245fa0d438847e6"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:03c0ae165e700364b266876d712acb1ac02693acd920afa67da2ebb91a0b3c09"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3e3070e680f79887d60feeda051a58d0ac36622e1759f305a41059eff62c6da7"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6ca08b840fe95d1c2bd9ab92dac5685f949fc6f9ae820ec16193e5ddf603c3b2"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e76654e9dbfb835b3518f9938e565c7806976c07b37c33526b574cc1a1050480"}, - {file = "pyzmq-26.0.3-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:871587bdadd1075b112e697173e946a07d722459d20716ceb3d1bd6c64bd08ce"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d0a2d1bd63a4ad79483049b26514e70fa618ce6115220da9efdff63688808b17"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0270b49b6847f0d106d64b5086e9ad5dc8a902413b5dbbb15d12b60f9c1747a4"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:703c60b9910488d3d0954ca585c34f541e506a091a41930e663a098d3b794c67"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74423631b6be371edfbf7eabb02ab995c2563fee60a80a30829176842e71722a"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4adfbb5451196842a88fda3612e2c0414134874bffb1c2ce83ab4242ec9e027d"}, - {file = "pyzmq-26.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3516119f4f9b8671083a70b6afaa0a070f5683e431ab3dc26e9215620d7ca1ad"}, - {file = "pyzmq-26.0.3.tar.gz", hash = "sha256:dba7d9f2e047dfa2bca3b01f4f84aa5246725203d6284e3790f2ca15fba6b40a"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, + {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a"}, + {file = "pyzmq-26.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282"}, + {file = "pyzmq-26.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea"}, + {file = "pyzmq-26.2.0-cp310-cp310-win32.whl", hash = "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971"}, + {file = "pyzmq-26.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218"}, + {file = "pyzmq-26.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf"}, + {file = "pyzmq-26.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3"}, + {file = "pyzmq-26.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6"}, + {file = "pyzmq-26.2.0-cp311-cp311-win32.whl", hash = "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5"}, + {file = "pyzmq-26.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_15_universal2.whl", hash = "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9"}, + {file = "pyzmq-26.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae"}, + {file = "pyzmq-26.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3"}, + {file = "pyzmq-26.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b"}, + {file = "pyzmq-26.2.0-cp312-cp312-win32.whl", hash = "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a"}, + {file = "pyzmq-26.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726"}, + {file = "pyzmq-26.2.0-cp313-cp313-macosx_10_15_universal2.whl", hash = "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187"}, + {file = "pyzmq-26.2.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115"}, + {file = "pyzmq-26.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e"}, + {file = "pyzmq-26.2.0-cp313-cp313-win32.whl", hash = "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad"}, + {file = "pyzmq-26.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a"}, + {file = "pyzmq-26.2.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797"}, + {file = "pyzmq-26.2.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_i686.whl", hash = "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6"}, + {file = "pyzmq-26.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0"}, + {file = "pyzmq-26.2.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1"}, + {file = "pyzmq-26.2.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097"}, + {file = "pyzmq-26.2.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win32.whl", hash = "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951"}, + {file = "pyzmq-26.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f"}, + {file = "pyzmq-26.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289"}, + {file = "pyzmq-26.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640"}, + {file = "pyzmq-26.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd"}, + {file = "pyzmq-26.2.0-cp38-cp38-win32.whl", hash = "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988"}, + {file = "pyzmq-26.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2"}, + {file = "pyzmq-26.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9"}, + {file = "pyzmq-26.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc"}, + {file = "pyzmq-26.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940"}, + {file = "pyzmq-26.2.0-cp39-cp39-win32.whl", hash = "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec"}, + {file = "pyzmq-26.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca"}, + {file = "pyzmq-26.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6"}, + {file = "pyzmq-26.2.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc"}, + {file = "pyzmq-26.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4"}, + {file = "pyzmq-26.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f"}, + {file = "pyzmq-26.2.0.tar.gz", hash = "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f"}, ] [package.dependencies] @@ -2868,13 +3036,13 @@ files = [ [[package]] name = "rich" -version = "13.7.1" +version = "13.8.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, ] [package.dependencies] @@ -2886,114 +3054,114 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.19.1" +version = "0.20.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:aaf71f95b21f9dc708123335df22e5a2fef6307e3e6f9ed773b2e0938cc4d491"}, - {file = "rpds_py-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca0dda0c5715efe2ab35bb83f813f681ebcd2840d8b1b92bfc6fe3ab382fae4a"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81db2e7282cc0487f500d4db203edc57da81acde9e35f061d69ed983228ffe3b"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1a8dfa125b60ec00c7c9baef945bb04abf8ac772d8ebefd79dae2a5f316d7850"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:271accf41b02687cef26367c775ab220372ee0f4925591c6796e7c148c50cab5"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9bc4161bd3b970cd6a6fcda70583ad4afd10f2750609fb1f3ca9505050d4ef3"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0cf2a0dbb5987da4bd92a7ca727eadb225581dd9681365beba9accbe5308f7d"}, - {file = "rpds_py-0.19.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b5e28e56143750808c1c79c70a16519e9bc0a68b623197b96292b21b62d6055c"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c7af6f7b80f687b33a4cdb0a785a5d4de1fb027a44c9a049d8eb67d5bfe8a687"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e429fc517a1c5e2a70d576077231538a98d59a45dfc552d1ac45a132844e6dfb"}, - {file = "rpds_py-0.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d2dbd8f4990d4788cb122f63bf000357533f34860d269c1a8e90ae362090ff3a"}, - {file = "rpds_py-0.19.1-cp310-none-win32.whl", hash = "sha256:e0f9d268b19e8f61bf42a1da48276bcd05f7ab5560311f541d22557f8227b866"}, - {file = "rpds_py-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:df7c841813f6265e636fe548a49664c77af31ddfa0085515326342a751a6ba51"}, - {file = "rpds_py-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:902cf4739458852fe917104365ec0efbea7d29a15e4276c96a8d33e6ed8ec137"}, - {file = "rpds_py-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f3d73022990ab0c8b172cce57c69fd9a89c24fd473a5e79cbce92df87e3d9c48"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3837c63dd6918a24de6c526277910e3766d8c2b1627c500b155f3eecad8fad65"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cdb7eb3cf3deb3dd9e7b8749323b5d970052711f9e1e9f36364163627f96da58"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26ab43b6d65d25b1a333c8d1b1c2f8399385ff683a35ab5e274ba7b8bb7dc61c"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75130df05aae7a7ac171b3b5b24714cffeabd054ad2ebc18870b3aa4526eba23"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c34f751bf67cab69638564eee34023909380ba3e0d8ee7f6fe473079bf93f09b"}, - {file = "rpds_py-0.19.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2671cb47e50a97f419a02cd1e0c339b31de017b033186358db92f4d8e2e17d8"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3c73254c256081704dba0a333457e2fb815364018788f9b501efe7c5e0ada401"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4383beb4a29935b8fa28aca8fa84c956bf545cb0c46307b091b8d312a9150e6a"}, - {file = "rpds_py-0.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dbceedcf4a9329cc665452db1aaf0845b85c666e4885b92ee0cddb1dbf7e052a"}, - {file = "rpds_py-0.19.1-cp311-none-win32.whl", hash = "sha256:f0a6d4a93d2a05daec7cb885157c97bbb0be4da739d6f9dfb02e101eb40921cd"}, - {file = "rpds_py-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:c149a652aeac4902ecff2dd93c3b2681c608bd5208c793c4a99404b3e1afc87c"}, - {file = "rpds_py-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:56313be667a837ff1ea3508cebb1ef6681d418fa2913a0635386cf29cff35165"}, - {file = "rpds_py-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d1d7539043b2b31307f2c6c72957a97c839a88b2629a348ebabe5aa8b626d6b"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e1dc59a5e7bc7f44bd0c048681f5e05356e479c50be4f2c1a7089103f1621d5"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8f78398e67a7227aefa95f876481485403eb974b29e9dc38b307bb6eb2315ea"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef07a0a1d254eeb16455d839cef6e8c2ed127f47f014bbda64a58b5482b6c836"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8124101e92c56827bebef084ff106e8ea11c743256149a95b9fd860d3a4f331f"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08ce9c95a0b093b7aec75676b356a27879901488abc27e9d029273d280438505"}, - {file = "rpds_py-0.19.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0b02dd77a2de6e49078c8937aadabe933ceac04b41c5dde5eca13a69f3cf144e"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4dd02e29c8cbed21a1875330b07246b71121a1c08e29f0ee3db5b4cfe16980c4"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9c7042488165f7251dc7894cd533a875d2875af6d3b0e09eda9c4b334627ad1c"}, - {file = "rpds_py-0.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f809a17cc78bd331e137caa25262b507225854073fd319e987bd216bed911b7c"}, - {file = "rpds_py-0.19.1-cp312-none-win32.whl", hash = "sha256:3ddab996807c6b4227967fe1587febade4e48ac47bb0e2d3e7858bc621b1cace"}, - {file = "rpds_py-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:32e0db3d6e4f45601b58e4ac75c6f24afbf99818c647cc2066f3e4b192dabb1f"}, - {file = "rpds_py-0.19.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:747251e428406b05fc86fee3904ee19550c4d2d19258cef274e2151f31ae9d38"}, - {file = "rpds_py-0.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dc733d35f861f8d78abfaf54035461e10423422999b360966bf1c443cbc42705"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbda75f245caecff8faa7e32ee94dfaa8312a3367397975527f29654cd17a6ed"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd04d8cab16cab5b0a9ffc7d10f0779cf1120ab16c3925404428f74a0a43205a"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2d66eb41ffca6cc3c91d8387509d27ba73ad28371ef90255c50cb51f8953301"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fdf4890cda3b59170009d012fca3294c00140e7f2abe1910e6a730809d0f3f9b"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1fa67ef839bad3815124f5f57e48cd50ff392f4911a9f3cf449d66fa3df62a5"}, - {file = "rpds_py-0.19.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b82c9514c6d74b89a370c4060bdb80d2299bc6857e462e4a215b4ef7aa7b090e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c7b07959866a6afb019abb9564d8a55046feb7a84506c74a6f197cbcdf8a208e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4f580ae79d0b861dfd912494ab9d477bea535bfb4756a2269130b6607a21802e"}, - {file = "rpds_py-0.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c6d20c8896c00775e6f62d8373aba32956aa0b850d02b5ec493f486c88e12859"}, - {file = "rpds_py-0.19.1-cp313-none-win32.whl", hash = "sha256:afedc35fe4b9e30ab240b208bb9dc8938cb4afe9187589e8d8d085e1aacb8309"}, - {file = "rpds_py-0.19.1-cp313-none-win_amd64.whl", hash = "sha256:1d4af2eb520d759f48f1073ad3caef997d1bfd910dc34e41261a595d3f038a94"}, - {file = "rpds_py-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:34bca66e2e3eabc8a19e9afe0d3e77789733c702c7c43cd008e953d5d1463fde"}, - {file = "rpds_py-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:24f8ae92c7fae7c28d0fae9b52829235df83f34847aa8160a47eb229d9666c7b"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71157f9db7f6bc6599a852852f3389343bea34315b4e6f109e5cbc97c1fb2963"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d494887d40dc4dd0d5a71e9d07324e5c09c4383d93942d391727e7a40ff810b"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3661e6d4ba63a094138032c1356d557de5b3ea6fd3cca62a195f623e381c76"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97fbb77eaeb97591efdc654b8b5f3ccc066406ccfb3175b41382f221ecc216e8"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cc4bc73e53af8e7a42c8fd7923bbe35babacfa7394ae9240b3430b5dcf16b2a"}, - {file = "rpds_py-0.19.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:35af5e4d5448fa179fd7fff0bba0fba51f876cd55212f96c8bbcecc5c684ae5c"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3511f6baf8438326e351097cecd137eb45c5f019944fe0fd0ae2fea2fd26be39"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:57863d16187995c10fe9cf911b897ed443ac68189179541734502353af33e693"}, - {file = "rpds_py-0.19.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:9e318e6786b1e750a62f90c6f7fa8b542102bdcf97c7c4de2a48b50b61bd36ec"}, - {file = "rpds_py-0.19.1-cp38-none-win32.whl", hash = "sha256:53dbc35808c6faa2ce3e48571f8f74ef70802218554884787b86a30947842a14"}, - {file = "rpds_py-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:8df1c283e57c9cb4d271fdc1875f4a58a143a2d1698eb0d6b7c0d7d5f49c53a1"}, - {file = "rpds_py-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e76c902d229a3aa9d5ceb813e1cbcc69bf5bda44c80d574ff1ac1fa3136dea71"}, - {file = "rpds_py-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de1f7cd5b6b351e1afd7568bdab94934d656abe273d66cda0ceea43bbc02a0c2"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24fc5a84777cb61692d17988989690d6f34f7f95968ac81398d67c0d0994a897"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:74129d5ffc4cde992d89d345f7f7d6758320e5d44a369d74d83493429dad2de5"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e360188b72f8080fefa3adfdcf3618604cc8173651c9754f189fece068d2a45"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13e6d4840897d4e4e6b2aa1443e3a8eca92b0402182aafc5f4ca1f5e24f9270a"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f09529d2332264a902688031a83c19de8fda5eb5881e44233286b9c9ec91856d"}, - {file = "rpds_py-0.19.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d4b52811dcbc1aba08fd88d475f75b4f6db0984ba12275d9bed1a04b2cae9b5"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dd635c2c4043222d80d80ca1ac4530a633102a9f2ad12252183bcf338c1b9474"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f35b34a5184d5e0cc360b61664c1c06e866aab077b5a7c538a3e20c8fcdbf90b"}, - {file = "rpds_py-0.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d4ec0046facab83012d821b33cead742a35b54575c4edfb7ed7445f63441835f"}, - {file = "rpds_py-0.19.1-cp39-none-win32.whl", hash = "sha256:f5b8353ea1a4d7dfb59a7f45c04df66ecfd363bb5b35f33b11ea579111d4655f"}, - {file = "rpds_py-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:1fb93d3486f793d54a094e2bfd9cd97031f63fcb5bc18faeb3dd4b49a1c06523"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d5c7e32f3ee42f77d8ff1a10384b5cdcc2d37035e2e3320ded909aa192d32c3"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:89cc8921a4a5028d6dd388c399fcd2eef232e7040345af3d5b16c04b91cf3c7e"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca34e913d27401bda2a6f390d0614049f5a95b3b11cd8eff80fe4ec340a1208"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5953391af1405f968eb5701ebbb577ebc5ced8d0041406f9052638bafe52209d"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:840e18c38098221ea6201f091fc5d4de6128961d2930fbbc96806fb43f69aec1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d8b735c4d162dc7d86a9cf3d717f14b6c73637a1f9cd57fe7e61002d9cb1972"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce757c7c90d35719b38fa3d4ca55654a76a40716ee299b0865f2de21c146801c"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a9421b23c85f361a133aa7c5e8ec757668f70343f4ed8fdb5a4a14abd5437244"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3b823be829407393d84ee56dc849dbe3b31b6a326f388e171555b262e8456cc1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:5e58b61dcbb483a442c6239c3836696b79f2cd8e7eec11e12155d3f6f2d886d1"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39d67896f7235b2c886fb1ee77b1491b77049dcef6fbf0f401e7b4cbed86bbd4"}, - {file = "rpds_py-0.19.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8b32cd4ab6db50c875001ba4f5a6b30c0f42151aa1fbf9c2e7e3674893fb1dc4"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1c32e41de995f39b6b315d66c27dea3ef7f7c937c06caab4c6a79a5e09e2c415"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a129c02b42d46758c87faeea21a9f574e1c858b9f358b6dd0bbd71d17713175"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:346557f5b1d8fd9966059b7a748fd79ac59f5752cd0e9498d6a40e3ac1c1875f"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:31e450840f2f27699d014cfc8865cc747184286b26d945bcea6042bb6aa4d26e"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01227f8b3e6c8961490d869aa65c99653df80d2f0a7fde8c64ebddab2b9b02fd"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:69084fd29bfeff14816666c93a466e85414fe6b7d236cfc108a9c11afa6f7301"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d2b88efe65544a7d5121b0c3b003ebba92bfede2ea3577ce548b69c5235185"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ea961a674172ed2235d990d7edf85d15d8dfa23ab8575e48306371c070cda67"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:5beffdbe766cfe4fb04f30644d822a1080b5359df7db3a63d30fa928375b2720"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:720f3108fb1bfa32e51db58b832898372eb5891e8472a8093008010911e324c5"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:c2087dbb76a87ec2c619253e021e4fb20d1a72580feeaa6892b0b3d955175a71"}, - {file = "rpds_py-0.19.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ddd50f18ebc05ec29a0d9271e9dbe93997536da3546677f8ca00b76d477680c"}, - {file = "rpds_py-0.19.1.tar.gz", hash = "sha256:31dd5794837f00b46f4096aa8ccaa5972f73a938982e32ed817bb520c465e520"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, ] [[package]] @@ -3014,19 +3182,23 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "71.1.0" +version = "74.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-71.1.0-py3-none-any.whl", hash = "sha256:33874fdc59b3188304b2e7c80d9029097ea31627180896fb549c578ceb8a0855"}, - {file = "setuptools-71.1.0.tar.gz", hash = "sha256:032d42ee9fb536e33087fb66cac5f840eb9391ed05637b3f2a76a7c8fb477936"}, + {file = "setuptools-74.0.0-py3-none-any.whl", hash = "sha256:0274581a0037b638b9fc1c6883cc71c0210865aaa76073f7882376b641b84e8f"}, + {file = "setuptools-74.0.0.tar.gz", hash = "sha256:a85e96b8be2b906f3e3e789adec6a9323abf79758ecfa3065bd740d81158b11e"}, ] [package.extras] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] [[package]] name = "shellingham" @@ -3063,13 +3235,13 @@ files = [ [[package]] name = "soupsieve" -version = "2.5" +version = "2.6" description = "A modern CSS selector implementation for Beautiful Soup." optional = false python-versions = ">=3.8" files = [ - {file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"}, - {file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"}, + {file = "soupsieve-2.6-py3-none-any.whl", hash = "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9"}, + {file = "soupsieve-2.6.tar.gz", hash = "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb"}, ] [[package]] @@ -3093,13 +3265,13 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] [[package]] name = "sympy" -version = "1.13.1" +version = "1.13.2" description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.8" files = [ - {file = "sympy-1.13.1-py3-none-any.whl", hash = "sha256:db36cdc64bf61b9b24578b6f7bab1ecdd2452cf008f34faa33776680c26d66f8"}, - {file = "sympy-1.13.1.tar.gz", hash = "sha256:9cebf7e04ff162015ce31c9c6c9144daa34a93bd082f54fd8f12deca4f47515f"}, + {file = "sympy-1.13.2-py3-none-any.whl", hash = "sha256:c51d75517712f1aed280d4ce58506a4a88d635d6b5dd48b39102a7ae1f3fcfe9"}, + {file = "sympy-1.13.2.tar.gz", hash = "sha256:401449d84d07be9d0c7a46a64bd54fe097667d5e7181bfe67ec777be9e01cb13"}, ] [package.dependencies] @@ -3160,13 +3332,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.0" +version = "0.13.2" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" files = [ - {file = "tomlkit-0.13.0-py3-none-any.whl", hash = "sha256:7075d3042d03b80f603482d69bf0c8f345c2b30e41699fd8883227f89972b264"}, - {file = "tomlkit-0.13.0.tar.gz", hash = "sha256:08ad192699734149f5b97b45f1f18dad7eb1b6d16bc72ad0c2335772650d7b72"}, + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] [[package]] @@ -3227,13 +3399,13 @@ files = [ [[package]] name = "tqdm" -version = "4.66.4" +version = "4.66.5" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, - {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, + {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, + {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, ] [package.dependencies] @@ -3277,13 +3449,13 @@ test = ["mypy", "pytest", "typing-extensions"] [[package]] name = "typer" -version = "0.12.3" +version = "0.12.5" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, - {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, + {file = "typer-0.12.5-py3-none-any.whl", hash = "sha256:62fe4e471711b147e3365034133904df3e235698399bc4de2b36c8579298d52b"}, + {file = "typer-0.12.5.tar.gz", hash = "sha256:f592f089bedcc8ec1b974125d64851029c3b1af145f04aca64d69410f0c9b722"}, ] [package.dependencies] @@ -3294,13 +3466,13 @@ typing-extensions = ">=3.7.4.3" [[package]] name = "types-python-dateutil" -version = "2.9.0.20240316" +version = "2.9.0.20240821" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240316.tar.gz", hash = "sha256:5d2f2e240b86905e40944dd787db6da9263f0deabef1076ddaed797351ec0202"}, - {file = "types_python_dateutil-2.9.0.20240316-py3-none-any.whl", hash = "sha256:6b8cb66d960771ce5ff974e9dd45e38facb81718cc1e208b10b1baccbfdbee3b"}, + {file = "types-python-dateutil-2.9.0.20240821.tar.gz", hash = "sha256:9649d1dcb6fef1046fb18bebe9ea2aa0028b160918518c34589a46045f6ebd98"}, + {file = "types_python_dateutil-2.9.0.20240821-py3-none-any.whl", hash = "sha256:f5889fcb4e63ed4aaa379b44f93c32593d50b9a94c9a60a0c854d8cc3511cd57"}, ] [[package]] @@ -3369,13 +3541,13 @@ files = [ [[package]] name = "webcolors" -version = "24.6.0" +version = "24.8.0" description = "A library for working with the color formats defined by HTML and CSS." optional = false python-versions = ">=3.8" files = [ - {file = "webcolors-24.6.0-py3-none-any.whl", hash = "sha256:8cf5bc7e28defd1d48b9e83d5fc30741328305a8195c29a8e668fa45586568a1"}, - {file = "webcolors-24.6.0.tar.gz", hash = "sha256:1d160d1de46b3e81e58d0a280d0c78b467dc80f47294b91b1ad8029d2cedb55b"}, + {file = "webcolors-24.8.0-py3-none-any.whl", hash = "sha256:fc4c3b59358ada164552084a8ebee637c221e4059267d0f8325b3b560f6c7f0a"}, + {file = "webcolors-24.8.0.tar.gz", hash = "sha256:08b07af286a01bcd30d583a7acadf629583d1f79bfef27dd2c2c5c263817277d"}, ] [package.extras] @@ -3411,13 +3583,13 @@ test = ["websockets"] [[package]] name = "widgetsnbextension" -version = "4.0.11" +version = "4.0.13" description = "Jupyter interactive widgets for Jupyter Notebook" optional = false python-versions = ">=3.7" files = [ - {file = "widgetsnbextension-4.0.11-py3-none-any.whl", hash = "sha256:55d4d6949d100e0d08b94948a42efc3ed6dfdc0e9468b2c4b128c9a2ce3a7a36"}, - {file = "widgetsnbextension-4.0.11.tar.gz", hash = "sha256:8b22a8f1910bfd188e596fe7fc05dcbd87e810c8a4ba010bdb3da86637398474"}, + {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, + {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, ] [[package]] @@ -3440,5 +3612,5 @@ pandas = ["pandas (>=1.5.3)"] [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "e625bf7c5fb2efe8cf2bae930a400ce698c638c02593dbaa6b93ff1b06bb881c" +python-versions = "^3.10.6" +content-hash = "5548a41d53b1b62bb411990811aa0b6b78a0f733c75b12c5c4bb0ad945a46dad" diff --git a/pyproject.toml b/pyproject.toml index 0008c2f5..555e9d2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "maze-dataset" -version = "0.5.6" +version = "1.0.0" description = "" authors = ["Michael Ivanitskiy ", "Dan Valentine ", "Rusheb Shah ", "Lucia Quirke ", "Can Rager ", "Alex Spies ", "Chris Mathwin ", "Tilman Rauker ", "Guillaume Corlouer "] readme = "README.md" @@ -8,18 +8,21 @@ packages = [{include = "maze_dataset"}] repository = "https://github.com/understanding-search/maze-dataset" [tool.poetry.dependencies] -python = "^3.10" +python = "^3.10.6" torch = { version = ">=1.13.1", source = "torch_cpu" } matplotlib = "^3.7.0" -muutils = "^0.6.7" +muutils = "^0.6.10" zanj = "^0.3.1" jupyter = "^1.0.0" ipykernel = "^6.22.0" jaxtyping = "^0.2.19" tqdm = "^4.65.0" +frozendict = "^2.4.4" +pandas = "^2.2.2" [tool.poetry.group.dev.dependencies] pytest = "^7.3.1" +pytest-xdist = "^3.6.1" # for parallel all tokenizers tests pycln = "^2.1.3" isort = "^5.12.0" black = "^24.1.0" @@ -46,7 +49,10 @@ filterwarnings = [ "ignore:`np\\.\\w*` is a deprecated alias for:DeprecationWarning", # Warning from matplotlib. Issue: https://github.com/matplotlib/matplotlib/issues/25244 - "ignore:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning" + "ignore:Deprecated call to `pkg_resources.declare_namespace:DeprecationWarning", + + # temporary fix for lots of deprecation warnings for old tokenizers + "ignore::maze_dataset.token_utils.TokenizerPendingDeprecationWarning", ] testpaths = "tests" norecursedirs="maze_dataset/utils/test_helpers" diff --git a/setup.py b/setup.py deleted file mode 100644 index bac24a43..00000000 --- a/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python - -import setuptools - -if __name__ == "__main__": - setuptools.setup() diff --git a/tests/all_tokenizers/test_all_tokenizers.py b/tests/all_tokenizers/test_all_tokenizers.py new file mode 100644 index 00000000..01009d84 --- /dev/null +++ b/tests/all_tokenizers/test_all_tokenizers.py @@ -0,0 +1,304 @@ +import itertools +import os +from collections import Counter +from typing import Callable, Iterable + +import pytest +from pytest import mark, param +from zanj import ZANJ + +from maze_dataset import VOCAB, VOCAB_LIST, LatticeMaze +from maze_dataset.maze.lattice_maze import SolvedMaze +from maze_dataset.testing_utils import MIXED_MAZES +from maze_dataset.token_utils import equal_except_adj_list_sequence +from maze_dataset.tokenization import ( + AdjListTokenizers, + CoordTokenizers, + EdgeGroupings, + EdgePermuters, + MazeTokenizerModular, + PathTokenizers, + PromptSequencers, + StepSizes, + StepTokenizers, + _TokenizerElement, +) +from maze_dataset.tokenization.all_tokenizers import ( + EVERY_TEST_TOKENIZERS, + MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS, + sample_tokenizers_for_test, + save_hashes, +) +from maze_dataset.utils import all_instances + +# Size of the sample from `all_tokenizers.ALL_TOKENIZERS` to test +# get from env, or set to default value of 100 +_os_env_num_tokenizers: str = os.getenv("NUM_TOKENIZERS_TO_TEST", "100") +NUM_TOKENIZERS_TO_TEST: int | None = ( + int(_os_env_num_tokenizers) if _os_env_num_tokenizers.isdigit() else None +) +print(f"{NUM_TOKENIZERS_TO_TEST = }") + +# ALL_TOKENIZERS: list[MazeTokenizerModular] = get_all_tokenizers() +SAMPLED_TOKENIZERS: list[MazeTokenizerModular] = sample_tokenizers_for_test( + NUM_TOKENIZERS_TO_TEST +) + +SAMPLED_MAZES: list[SolvedMaze] = MIXED_MAZES[:6] + + +@pytest.fixture(scope="session") +def save_tokenizer_hashes(): + save_hashes() + + +# def test_all_tokenizers(): +# assert len(ALL_TOKENIZERS) > 400 + + +@mark.parametrize( + "class_", + [param(c, id=c.__name__) for c in _TokenizerElement.__subclasses__()], +) +def test_all_instances_tokenizerelement(class_: type): + all_vals = list( + all_instances( + class_, validation_funcs=MAZE_TOKENIZER_MODULAR_DEFAULT_VALIDATION_FUNCS + ) + ) + assert len({hash(elem) for elem in all_vals}) == len(all_vals) + + +SAMPLE_MIN: int = len(EVERY_TEST_TOKENIZERS) + + +@mark.parametrize( + "n, result", + [ + param(i, result) + for i, result in [ + (SAMPLE_MIN - 1, ValueError), + (SAMPLE_MIN, None), + (SAMPLE_MIN + 5, None), + (SAMPLE_MIN + 200, None), + ] + ], +) +def test_sample_tokenizers_for_test(n: int, result: type[Exception] | None): + if isinstance(result, type) and issubclass(result, Exception): + with pytest.raises(result): + sample_tokenizers_for_test(n) + return + mts: list[MazeTokenizerModular] = sample_tokenizers_for_test(n) + mts_set: set[MazeTokenizerModular] = set(mts) + assert len(mts) == len(mts_set) + assert set(EVERY_TEST_TOKENIZERS).issubset(mts_set) + if n > SAMPLE_MIN + 1: + mts2: list[MazeTokenizerModular] = sample_tokenizers_for_test(n) + assert set(mts2) != mts_set # Check that succesive samples are different + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_token_region_delimiters(tokenizer: MazeTokenizerModular): + """ and similar token region delimiters should appear at most 1 time, regardless of tokenizer.""" + for maze in SAMPLED_MAZES: + counts: Counter = Counter(maze.as_tokens(tokenizer)) + assert all([counts[tok] < 2 for tok in VOCAB_LIST[:8]]) + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_token_stability(tokenizer: MazeTokenizerModular): + """Tests consistency of tokenizations over multiple method calls.""" + for maze in SAMPLED_MAZES: + tokens1: list[str] = maze.as_tokens(tokenizer) + tokens2: list[str] = maze.as_tokens(tokenizer) + if tokenizer.has_element( + EdgeGroupings.ByLeadingCoord, EdgePermuters.RandomCoords + ) or tokenizer.has_element( + AdjListTokenizers.AdjListCardinal, EdgePermuters.RandomCoords + ): + # In this case, the adjlist is expected to have different token counts over multiple calls + # Exclude that region from the test + non_adjlist1 = tokens1[: tokens1.index(VOCAB.ADJLIST_START)] + non_adjlist1.extend(tokens1[tokens1.index(VOCAB.ADJLIST_END) :]) + non_adjlist2 = tokens2[: tokens2.index(VOCAB.ADJLIST_START)] + non_adjlist2.extend(tokens2[tokens2.index(VOCAB.ADJLIST_END) :]) + assert non_adjlist1 == non_adjlist2 + else: + assert equal_except_adj_list_sequence(tokens1, tokens2) + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_tokenizer_properties(tokenizer: MazeTokenizerModular): + # Just make sure the call doesn't raise exception + assert len(tokenizer.name) > 5 + + assert tokenizer.vocab_size == 4096 + assert isinstance(tokenizer.token_arr, Iterable) + assert all(isinstance(token, str) for token in tokenizer.token_arr) + assert tokenizer.token_arr[tokenizer.padding_token_index] == VOCAB.PADDING + + # Just make sure the call doesn't raise exception + print(tokenizer.summary()) + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_encode_decode(tokenizer: MazeTokenizerModular): + for maze in SAMPLED_MAZES: + maze_tok: list[str] = maze.as_tokens(maze_tokenizer=tokenizer) + maze_encoded: list[int] = tokenizer.encode(maze_tok) + maze_decoded: LatticeMaze = tokenizer.decode(maze_encoded) + assert maze_tok == maze_decoded + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_zanj_save_read(tokenizer: MazeTokenizerModular): + path = os.path.abspath( + os.path.join( + os.path.curdir, + "data", + "MazeTokenizerModular_" + hex(hash(tokenizer)) + ".zanj", + ) + ) + zanj = ZANJ() + zanj.save(tokenizer, path) + assert zanj.read(path) == tokenizer + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_is_AOTP(tokenizer: MazeTokenizerModular): + if isinstance(tokenizer.prompt_sequencer, PromptSequencers.AOTP): + assert tokenizer.is_AOTP() + else: + assert not tokenizer.is_AOTP() + + +@mark.parametrize( + "tokenizer", + [param(tokenizer, id=tokenizer.name) for tokenizer in SAMPLED_TOKENIZERS], +) +def test_is_UT(tokenizer: MazeTokenizerModular): + if isinstance(tokenizer.prompt_sequencer.coord_tokenizer, CoordTokenizers.UT): + assert tokenizer.is_UT() + else: + assert not tokenizer.is_UT() + + +_has_elems_type = ( + type[_TokenizerElement] + | _TokenizerElement + | Iterable[type[_TokenizerElement] | _TokenizerElement] +) + + +@mark.parametrize( + "tokenizer, elems, result_func", + [ + param( + tokenizer, + elems_tuple[0], + elems_tuple[1], + id=f"{tokenizer.name}-{elems_tuple[0]}", + ) + for tokenizer, elems_tuple in itertools.product( + SAMPLED_TOKENIZERS, + [ + ( + [PromptSequencers.AOTP()], + lambda mt, els: mt.prompt_sequencer == els[0], + ), + (PromptSequencers.AOTP(), lambda mt, els: mt.prompt_sequencer == els), + ( + [CoordTokenizers.CTT()], + lambda mt, els: mt.prompt_sequencer.coord_tokenizer == els[0], + ), + ( + CoordTokenizers.CTT(intra=False), + lambda mt, els: mt.prompt_sequencer.coord_tokenizer == els, + ), + ( + [CoordTokenizers.CTT], + lambda mt, els: isinstance( + mt.prompt_sequencer.coord_tokenizer, els[0] + ), + ), + ( + CoordTokenizers._CoordTokenizer, + lambda mt, els: isinstance( + mt.prompt_sequencer.coord_tokenizer, els + ), + ), + ( + StepSizes.Singles, + lambda mt, els: isinstance( + mt.prompt_sequencer.path_tokenizer.step_size, els + ), + ), + ( + StepTokenizers.Coord, + lambda mt, els: any( + isinstance(step_tok, els) + for step_tok in mt.prompt_sequencer.path_tokenizer.step_tokenizers + ), + ), + ( + [CoordTokenizers.CTT()], + lambda mt, els: mt.prompt_sequencer.coord_tokenizer == els[0], + ), + ( + [CoordTokenizers.CTT, PathTokenizers.StepSequence], + lambda mt, els: isinstance( + mt.prompt_sequencer.coord_tokenizer, els[0] + ) + and isinstance(mt.prompt_sequencer.path_tokenizer, els[1]), + ), + # ((a for a in [CoordTokenizers.CTT, PathTokenizers.Coords]), + # lambda mt, els: isinstance(mt.coord_tokenizer, list(els)[0]) and isinstance(mt.path_tokenizer, list(els)[1]) + # ), + ( + [CoordTokenizers.CTT, PathTokenizers.StepSequence(post=False)], + lambda mt, els: isinstance( + mt.prompt_sequencer.coord_tokenizer, els[0] + ) + and mt.prompt_sequencer.path_tokenizer == els[1], + ), + ( + [ + CoordTokenizers.CTT, + PathTokenizers.StepSequence, + PromptSequencers.AOP(), + ], + lambda mt, els: isinstance( + mt.prompt_sequencer.coord_tokenizer, els[0] + ) + and isinstance(mt.prompt_sequencer.path_tokenizer, els[1]) + and mt.prompt_sequencer == els[2], + ), + ], + ) + ], +) +def test_has_element( + tokenizer: MazeTokenizerModular, + elems: _has_elems_type, + result_func: Callable[[MazeTokenizerModular, _has_elems_type], bool], +): + assert tokenizer.has_element(elems) == result_func(tokenizer, elems) diff --git a/tests/unit/maze_dataset/dataset/test_configs.py b/tests/unit/maze_dataset/dataset/test_configs.py new file mode 100644 index 00000000..1c392037 --- /dev/null +++ b/tests/unit/maze_dataset/dataset/test_configs.py @@ -0,0 +1,31 @@ +from maze_dataset import MazeDatasetConfig +from maze_dataset.dataset.configs import MAZE_DATASET_CONFIGS + + +def test_get_configs(): + keys: list[str] = list(MAZE_DATASET_CONFIGS.keys()) + assert len(keys) > 0, "There must be at least one key in the configs" + assert all([isinstance(key, str) for key in keys]), f"Keys must be strings: {keys}" + assert all( + [isinstance(MAZE_DATASET_CONFIGS[key], MazeDatasetConfig) for key in keys] + ), f"Values must be dictionaries: {MAZE_DATASET_CONFIGS}" + + assert len(MAZE_DATASET_CONFIGS.keys()) == len(MAZE_DATASET_CONFIGS) + assert len(MAZE_DATASET_CONFIGS.items()) == len(MAZE_DATASET_CONFIGS) + assert len(MAZE_DATASET_CONFIGS.values()) == len(MAZE_DATASET_CONFIGS) + + assert all( + [isinstance(key, str) for key in MAZE_DATASET_CONFIGS.keys()] + ), f".keys() must be strings: {MAZE_DATASET_CONFIGS.keys()}" + assert all( + [ + isinstance(value, MazeDatasetConfig) + for value in MAZE_DATASET_CONFIGS.values() + ] + ), f".values() must be configs: {MAZE_DATASET_CONFIGS.values()}" + assert all( + [ + isinstance(key, str) and isinstance(value, MazeDatasetConfig) + for key, value in MAZE_DATASET_CONFIGS.items() + ] + ), f".items() must be (str, config) tuples {MAZE_DATASET_CONFIGS.items()}" diff --git a/tests/unit/maze_dataset/generation/test_coord_str_tuple.py b/tests/unit/maze_dataset/generation/test_coord_str_tuple.py index 92b4f679..30229828 100644 --- a/tests/unit/maze_dataset/generation/test_coord_str_tuple.py +++ b/tests/unit/maze_dataset/generation/test_coord_str_tuple.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from maze_dataset.tokenization.util import ( +from maze_dataset.token_utils import ( _coord_to_strings_indexed, _coord_to_strings_UT, coord_str_to_coord_np, diff --git a/tests/unit/maze_dataset/generation/test_maze_dataset.py b/tests/unit/maze_dataset/generation/test_maze_dataset.py index 54e7ff36..c24e9e33 100644 --- a/tests/unit/maze_dataset/generation/test_maze_dataset.py +++ b/tests/unit/maze_dataset/generation/test_maze_dataset.py @@ -1,8 +1,9 @@ import copy -import os +from pathlib import Path import numpy as np import pytest +from pytest import mark, param from zanj import ZANJ from maze_dataset.constants import CoordArray @@ -14,6 +15,7 @@ MazeDataset, MazeDatasetConfig, register_maze_filter, + set_serialize_minimal_threshold, ) from maze_dataset.generation.generators import GENERATORS_MAP from maze_dataset.maze import SolvedMaze @@ -24,131 +26,153 @@ class TestMazeDatasetConfig: pass -class TestMazeDataset: - config = MazeDatasetConfig(name="test", grid_n=3, n_mazes=5) +TEST_CONFIGS = [ + MazeDatasetConfig( + name="test", + grid_n=grid_n, + n_mazes=n_mazes, + maze_ctor=GENERATORS_MAP["gen_dfs"], + maze_ctor_kwargs=maze_ctor_kwargs, + ) + for grid_n, n_mazes, maze_ctor_kwargs in [ + (3, 5, {}), + (3, 1, {}), + (5, 5, dict(do_forks=False)), + ] +] - def test_generate_serial(self): - dataset = MazeDataset.generate(self.config, gen_parallel=False) - assert len(dataset) == 5 - for i, maze in enumerate(dataset): - assert maze.grid_shape == (3, 3) +def test_generate_serial(): + dataset = MazeDataset.generate(TEST_CONFIGS[0], gen_parallel=False) - def test_generate_parallel(self): - dataset = MazeDataset.generate( - self.config, gen_parallel=True, verbose=True, pool_kwargs=dict(processes=2) - ) + assert len(dataset) == 5 + for i, maze in enumerate(dataset): + assert maze.grid_shape == (3, 3) - assert len(dataset) == 5 - for i, maze in enumerate(dataset): - assert maze.grid_shape == (3, 3) - - def test_data_hash(self): - dataset = MazeDataset.generate(self.config) - # TODO: dataset.data_hash doesn't work right now - - def test_download(self): - with pytest.raises(NotImplementedError): - MazeDataset.download(self.config) - - def test_serialize_load(self): - dataset = MazeDataset.generate(self.config) - dataset_copy = MazeDataset.load(dataset.serialize()) - - assert dataset.cfg == dataset_copy.cfg - for maze, maze_copy in zip(dataset, dataset_copy): - assert maze == maze_copy - - def test_serialize_load_minimal(self): - cfgs = [self.config] - cfgs.extend( - [ - MazeDatasetConfig( - name="test", - grid_n=grid_n, - n_mazes=n_mazes, - maze_ctor=maze_ctor, - maze_ctor_kwargs=maze_ctor_kwargs, - ) - for grid_n, n_mazes, maze_ctor, maze_ctor_kwargs in [ - (3, 1, GENERATORS_MAP["gen_dfs"], {}), - (5, 5, GENERATORS_MAP["gen_dfs"], dict(do_forks=False)), - ] - ] - ) - for c in cfgs: - d = MazeDataset.generate(c, gen_parallel=False) - assert MazeDataset.load(d._serialize_minimal()) == d - - def test_save_read_minimal(self): - cfgs = [self.config] - cfgs.extend( - [ - MazeDatasetConfig( - name="test", - grid_n=grid_n, - n_mazes=n_mazes, - maze_ctor=maze_ctor, - maze_ctor_kwargs=maze_ctor_kwargs, - ) - for grid_n, n_mazes, maze_ctor, maze_ctor_kwargs in [ - (3, 1, GENERATORS_MAP["gen_dfs"], {}), - (5, 5, GENERATORS_MAP["gen_dfs"], dict(do_forks=False)), - ] - ] + +def test_generate_parallel(): + dataset = MazeDataset.generate( + TEST_CONFIGS[0], gen_parallel=True, verbose=True, pool_kwargs=dict(processes=2) + ) + + assert len(dataset) == 5 + for i, maze in enumerate(dataset): + assert maze.grid_shape == (3, 3) + + +def test_data_hash(): + dataset = MazeDataset.generate(TEST_CONFIGS[0]) + # TODO: dataset.data_hash doesn't work right now + + +def test_download(): + with pytest.raises(NotImplementedError): + MazeDataset.download(TEST_CONFIGS[0]) + + +def test_serialize_load(): + dataset = MazeDataset.generate(TEST_CONFIGS[0]) + dataset_copy = MazeDataset.load(dataset.serialize()) + + assert dataset.cfg == dataset_copy.cfg + for maze, maze_copy in zip(dataset, dataset_copy): + assert maze == maze_copy + + +@mark.parametrize( + "config", + [ + param( + c, + id=f"{c.grid_n=}; {c.n_mazes=}; {c.maze_ctor_kwargs=}", ) - for c in cfgs: - d = MazeDataset.generate(c, gen_parallel=False) - p = os.path.abspath( - os.path.join(os.getcwd(), "..", "data", d.cfg.to_fname() + ".zanj") - ) - d.save(file_path=p) - # read as MazeDataset - roundtrip = MazeDataset.read(p) - cfg_diff = roundtrip.cfg.diff(d.cfg) - assert cfg_diff == {} - assert roundtrip.cfg == d.cfg - assert roundtrip.mazes == d.mazes - assert roundtrip == d - # read from zanj - z = ZANJ() - roundtrip_zanj = z.read(p) - assert roundtrip_zanj == d - - def test_custom_maze_filter(self): - connection_list = bool_array_from_string( - """ - F T - F F - - T F - T F - """, - shape=[2, 2, 2], + for c in TEST_CONFIGS + ], +) +def test_serialize_load_minimal(config): + d = MazeDataset.generate(config, gen_parallel=False) + assert MazeDataset.load(d._serialize_minimal()) == d + + +@mark.parametrize( + "config", + [ + param( + c, + id=f"{c.grid_n=}; {c.n_mazes=}; {c.maze_ctor_kwargs=}", ) - solutions = [ - [[0, 0], [0, 1], [1, 1]], - [[0, 0], [0, 1]], - [[0, 0]], - ] + for c in TEST_CONFIGS + ], +) +def test_save_read_minimal(config): + def save_and_read(d: MazeDataset, p: str): + d.save(file_path=p) + # read as MazeDataset + roundtrip = MazeDataset.read(p) + assert roundtrip == d + # read from zanj + z = ZANJ() + roundtrip_zanj = z.read(p) + assert roundtrip_zanj == d + + d = MazeDataset.generate(config, gen_parallel=False) + p = Path("tests/_temp/test_maze_dataset/") / (d.cfg.to_fname() + ".zanj") + + # Test with full serialization + set_serialize_minimal_threshold(None) + save_and_read(d, p) + + # Test with minimal serialization + set_serialize_minimal_threshold(0) + save_and_read(d, p) + + d.save(file_path=p) + # read as MazeDataset + roundtrip = MazeDataset.read(p) + assert d.cfg.diff(roundtrip.cfg) == dict() + cfg_diff = roundtrip.cfg.diff(d.cfg) + assert cfg_diff == {} + assert roundtrip.cfg == d.cfg + assert roundtrip.mazes == d.mazes + assert roundtrip == d + # read from zanj + z = ZANJ() + roundtrip_zanj = z.read(p) + assert roundtrip_zanj == d + + +def test_custom_maze_filter(): + connection_list = bool_array_from_string( + """ + F T + F F - def custom_filter_solution_length( - maze: SolvedMaze, solution_length: int - ) -> bool: - return len(maze.solution) == solution_length + T F + T F + """, + shape=[2, 2, 2], + ) + solutions = [ + [[0, 0], [0, 1], [1, 1]], + [[0, 0], [0, 1]], + [[0, 0]], + ] - mazes = [ - SolvedMaze(connection_list=connection_list, solution=solution) - for solution in solutions - ] - dataset = MazeDataset(cfg=self.config, mazes=mazes) + def custom_filter_solution_length(maze: SolvedMaze, solution_length: int) -> bool: + return len(maze.solution) == solution_length - filtered_lambda = dataset.custom_maze_filter(lambda m: len(m.solution) == 1) - filtered_func = dataset.custom_maze_filter( - custom_filter_solution_length, solution_length=1 - ) + mazes = [ + SolvedMaze(connection_list=connection_list, solution=solution) + for solution in solutions + ] + dataset = MazeDataset(cfg=TEST_CONFIGS[0], mazes=mazes) + + filtered_lambda = dataset.custom_maze_filter(lambda m: len(m.solution) == 1) + filtered_func = dataset.custom_maze_filter( + custom_filter_solution_length, solution_length=1 + ) - assert filtered_lambda.mazes == filtered_func.mazes == [mazes[2]] + assert filtered_lambda.mazes == filtered_func.mazes == [mazes[2]] class TestMazeDatasetFilters: diff --git a/tests/unit/maze_dataset/generation/test_solved_maze.py b/tests/unit/maze_dataset/generation/test_solved_maze.py index d729629f..2a8f73c2 100644 --- a/tests/unit/maze_dataset/generation/test_solved_maze.py +++ b/tests/unit/maze_dataset/generation/test_solved_maze.py @@ -2,18 +2,18 @@ from maze_dataset import SolvedMaze from maze_dataset.generation.generators import get_maze_with_solution -from maze_dataset.tokenization import MazeTokenizer, TokenizationMode +from maze_dataset.testing_utils import LEGACY_AND_EQUIVALENT_TOKENIZERS +from maze_dataset.tokenization import MazeTokenizer, MazeTokenizerModular @mark.parametrize( - "tok_mode", - [param(tok_mode, id=tok_mode.name) for tok_mode in TokenizationMode], + "tokenizer", + [ + param(tokenizer, id=tokenizer.name) + for tokenizer in LEGACY_AND_EQUIVALENT_TOKENIZERS + ], ) -def test_from_tokens(tok_mode: TokenizationMode): - tokenizer: MazeTokenizer = MazeTokenizer( - tokenization_mode=tok_mode, - max_grid_size=20, - ) +def test_from_tokens(tokenizer: MazeTokenizer | MazeTokenizerModular): maze_size: int = 2 solved_maze: SolvedMaze = get_maze_with_solution("gen_dfs", (maze_size, maze_size)) diff --git a/tests/unit/maze_dataset/tokenization/test_coords_string_split.py b/tests/unit/maze_dataset/tokenization/test_coords_string_split.py index f8a0e116..c614a18b 100644 --- a/tests/unit/maze_dataset/tokenization/test_coords_string_split.py +++ b/tests/unit/maze_dataset/tokenization/test_coords_string_split.py @@ -1,4 +1,4 @@ -from maze_dataset.tokenization.util import coords_string_split_UT +from maze_dataset.token_utils import coords_string_split_UT def test_coords_string_split_UT(): diff --git a/tests/unit/maze_dataset/tokenization/test_maze_tokenization.py b/tests/unit/maze_dataset/tokenization/test_maze_tokenization.py index 3cae84b7..e5cfd7ee 100644 --- a/tests/unit/maze_dataset/tokenization/test_maze_tokenization.py +++ b/tests/unit/maze_dataset/tokenization/test_maze_tokenization.py @@ -6,14 +6,18 @@ MazeDatasetConfig, SolvedMaze, ) -from maze_dataset.tokenization import MazeTokenizer, TokenizationMode +from maze_dataset.testing_utils import LEGACY_AND_EQUIVALENT_TOKENIZERS +from maze_dataset.tokenization import MazeTokenizer, MazeTokenizerModular @mark.parametrize( - "tok_mode", - [param(tok_mode, id=tok_mode.name) for tok_mode in TokenizationMode], + "tokenizer", + [ + param(tokenizer, id=tokenizer.name) + for tokenizer in LEGACY_AND_EQUIVALENT_TOKENIZERS + ], ) -def test_tokenization_roundtrip(tok_mode: TokenizationMode): +def test_tokenization_roundtrip(tokenizer: MazeTokenizer | MazeTokenizerModular): dataset: MazeDataset = MazeDataset.from_config( MazeDatasetConfig( name="test", @@ -23,10 +27,6 @@ def test_tokenization_roundtrip(tok_mode: TokenizationMode): ), allow_generation_metadata_filter_mismatch=True, ) - tokenizer: MazeTokenizer = MazeTokenizer( - tokenization_mode=tok_mode, - max_grid_size=20, - ) dataset_tokenized: list[list[str]] = dataset.as_tokens(tokenizer) dataset_tokenized_joined: list[str] = dataset.as_tokens( diff --git a/tests/unit/maze_dataset/tokenization/test_token_utils.py b/tests/unit/maze_dataset/tokenization/test_token_utils.py index 6d9c0438..ee8fff78 100644 --- a/tests/unit/maze_dataset/tokenization/test_token_utils.py +++ b/tests/unit/maze_dataset/tokenization/test_token_utils.py @@ -1,31 +1,54 @@ +import itertools +from typing import Callable + +import frozendict +import numpy as np import pytest +from jaxtyping import Int from pytest import mark, param +from maze_dataset import LatticeMaze +from maze_dataset.constants import VOCAB, Connection, ConnectionArray from maze_dataset.dataset.maze_dataset import MazeDatasetConfig -from maze_dataset.tokenization.token_utils import ( +from maze_dataset.generation import numpy_rng +from maze_dataset.testing_utils import GRID_N, MAZE_DATASET +from maze_dataset.token_utils import ( + _coord_to_strings_UT, + coords_to_strings, + equal_except_adj_list_sequence, get_adj_list_tokens, get_origin_tokens, get_path_tokens, + get_relative_direction, get_target_tokens, - get_tokens_up_to_path_start, + is_connection, + strings_to_coords, tokens_between, ) -from maze_dataset.tokenization.util import ( - _coord_to_strings_UT, - coords_to_strings, - strings_to_coords, +from maze_dataset.tokenization import ( + PathTokenizers, + StepTokenizers, + get_tokens_up_to_path_start, +) +from maze_dataset.utils import ( + FiniteValued, + all_instances, + lattice_connection_array, + manhattan_distance, ) -MAZE_TOKENS = ( +MAZE_TOKENS: tuple[list[str], str] = ( " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), "AOTP_UT", ) -# setattr(MAZE_TOKENS, "name", 'AOTP_UT') -MAZE_TOKENS_AOTP_CTT_indexed = ( +MAZE_TOKENS_AOTP_CTT_indexed: tuple[list[str], str] = ( " ( 0 , 1 ) <--> ( 1 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split(), "AOTP_CTT_indexed", ) -TEST_TOKEN_LISTS = [MAZE_TOKENS, MAZE_TOKENS_AOTP_CTT_indexed] +TEST_TOKEN_LISTS: list[tuple[list[str], str]] = [ + MAZE_TOKENS, + MAZE_TOKENS_AOTP_CTT_indexed, +] @mark.parametrize( @@ -358,3 +381,265 @@ def test_coords_to_strings(toks: list[str], tokenizer_name: str): coords_to_strings( coords, coord_to_strings_func=_coord_to_strings_UT, when_noncoord="error" ) + + +def test_equal_except_adj_list_sequence(): + assert equal_except_adj_list_sequence(MAZE_TOKENS[0], MAZE_TOKENS[0]) + assert not equal_except_adj_list_sequence( + MAZE_TOKENS[0], MAZE_TOKENS_AOTP_CTT_indexed[0] + ) + assert equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + assert equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (0,1) <--> (1,1) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + assert equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (1,1) <--> (0,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + assert not equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (0,1) <--> (1,1) ; (1,0) (1,1) (1,1) (1,0) ".split(), + ) + assert not equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + assert not equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + assert not equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + "(0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + with pytest.raises(ValueError): + equal_except_adj_list_sequence( + "(0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + "(0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + with pytest.raises(ValueError): + equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + assert not equal_except_adj_list_sequence( + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + " (0,1) <--> (1,1) ; (1,0) <--> (1,1) ; (0,1) <--> (0,0) ; (1,0) (1,1) (1,0) (1,1) ".split(), + ) + + # CTT + assert equal_except_adj_list_sequence( + " ( 0 , 1 ) <--> ( 1 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split(), + " ( 0 , 1 ) <--> ( 1 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split(), + ) + assert equal_except_adj_list_sequence( + " ( 0 , 1 ) <--> ( 1 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split(), + " ( 1 , 1 ) <--> ( 0 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split(), + ) + # This inactive test demonstrates the lack of robustness of the function for comparing source `LatticeMaze` objects. + # See function documentation for details. + # assert not equal_except_adj_list_sequence( + # " ( 0 , 1 ) <--> ( 1 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split(), + # " ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 1 , 0 ) <--> ( 1 , 1 ) ; ( 0 , 1 ) <--> ( 0 , 0 ) ; ( 1 , 0 ) ( 1 , 1 ) ( 1 , 0 ) ( 1 , 1 ) ".split() + # ) + + +# @mivanit: this was really difficult to understand +@mark.parametrize( + "type_, validation_funcs, assertion", + [ + param( + type_, + vfs, + assertion, + id=f"{i}-{type_.__name__}", + ) + for i, (type_, vfs, assertion) in enumerate( + [ + ( + # type + PathTokenizers._PathTokenizer, + # validation_funcs + dict(), + # assertion + lambda x: PathTokenizers.StepSequence( + step_tokenizers=(StepTokenizers.Distance(),) + ) + in x, + ), + ( + # type + PathTokenizers._PathTokenizer, + # validation_funcs + {PathTokenizers._PathTokenizer: lambda x: x.is_valid()}, + # assertion + lambda x: PathTokenizers.StepSequence( + step_tokenizers=(StepTokenizers.Distance(),) + ) + not in x + and PathTokenizers.StepSequence( + step_tokenizers=( + StepTokenizers.Coord(), + StepTokenizers.Coord(), + ) + ) + not in x, + ), + ] + ) + ], +) +def test_all_instances2( + type_: FiniteValued, + validation_funcs: frozendict.frozendict[ + FiniteValued, Callable[[FiniteValued], bool] + ], + assertion: Callable[[list[FiniteValued]], bool], +): + assert assertion(all_instances(type_, validation_funcs)) + + +@mark.parametrize( + "coords, result", + [ + param( + np.array(coords), + res, + id=f"{coords}", + ) + for coords, res in ( + [ + ([[0, 0], [0, 1], [1, 1]], VOCAB.PATH_RIGHT), + ([[0, 0], [1, 0], [1, 1]], VOCAB.PATH_LEFT), + ([[0, 0], [0, 1], [0, 2]], VOCAB.PATH_FORWARD), + ([[0, 0], [0, 1], [0, 0]], VOCAB.PATH_BACKWARD), + ([[0, 0], [0, 1], [0, 1]], VOCAB.PATH_STAY), + ([[1, 1], [0, 1], [0, 0]], VOCAB.PATH_LEFT), + ([[1, 1], [1, 0], [0, 0]], VOCAB.PATH_RIGHT), + ([[0, 2], [0, 1], [0, 0]], VOCAB.PATH_FORWARD), + ([[0, 0], [0, 1], [0, 0]], VOCAB.PATH_BACKWARD), + ([[0, 1], [0, 1], [0, 0]], ValueError), + ([[0, 1], [1, 1], [0, 0]], ValueError), + ([[1, 0], [1, 1], [0, 0]], ValueError), + ([[0, 1], [0, 2], [0, 0]], ValueError), + ([[0, 1], [0, 0], [0, 0]], VOCAB.PATH_STAY), + ([[1, 1], [0, 0], [0, 1]], ValueError), + ([[1, 1], [0, 0], [1, 0]], ValueError), + ([[0, 2], [0, 0], [0, 1]], ValueError), + ([[0, 0], [0, 0], [0, 1]], ValueError), + ([[0, 1], [0, 0], [0, 1]], VOCAB.PATH_BACKWARD), + ([[-1, 0], [0, 0], [1, 0]], VOCAB.PATH_FORWARD), + ([[-1, 0], [0, 0], [0, 1]], VOCAB.PATH_LEFT), + ([[-1, 0], [0, 0], [-1, 0]], VOCAB.PATH_BACKWARD), + ([[-1, 0], [0, 0], [0, -1]], VOCAB.PATH_RIGHT), + ([[-1, 0], [0, 0], [1, 0], [2, 0]], ValueError), + ([[-1, 0], [0, 0]], ValueError), + ([[-1, 0, 0], [0, 0, 0]], ValueError), + ] + ) + ], +) +def test_get_relative_direction( + coords: Int[np.ndarray, "prev_cur_next=3 axis=2"], result: str | type[Exception] +): + if isinstance(result, type) and issubclass(result, Exception): + with pytest.raises(result): + get_relative_direction(coords) + return + assert get_relative_direction(coords) == result + + +@mark.parametrize( + "edges, result", + [ + param( + edges, + res, + id=f"{edges}", + ) + for edges, res in ( + [ + (np.array([[0, 0], [0, 1]]), 1), + (np.array([[1, 0], [0, 1]]), 2), + (np.array([[-1, 0], [0, 1]]), 2), + (np.array([[0, 0], [5, 3]]), 8), + ( + np.array( + [ + [[0, 0], [0, 1]], + [[1, 0], [0, 1]], + [[-1, 0], [0, 1]], + [[0, 0], [5, 3]], + ] + ), + [1, 2, 2, 8], + ), + (np.array([[[0, 0], [5, 3]]]), [8]), + ] + ) + ], +) +def test_manhattan_distance( + edges: ConnectionArray | Connection, + result: Int[np.ndarray, "edges"] | Int[np.ndarray, ""] | type[Exception], +): + if isinstance(result, type) and issubclass(result, Exception): + with pytest.raises(result): + manhattan_distance(edges) + return + assert np.array_equal(manhattan_distance(edges), np.array(result, dtype=np.int8)) + + +@mark.parametrize( + "n", + [param(n) for n in [2, 3, 5, 20]], +) +def test_lattice_connection_arrray(n): + edges = lattice_connection_array(n) + assert tuple(edges.shape) == (2 * n * (n - 1), 2, 2) + assert np.all(np.sum(edges[:, 1], axis=1) > np.sum(edges[:, 0], axis=1)) + assert tuple(np.unique(edges, axis=0).shape) == (2 * n * (n - 1), 2, 2) + + +@mark.parametrize( + "edges, maze", + [ + param( + edges(), + maze, + id=f"edges[{i}]; maze[{j}]", + ) + for (i, edges), (j, maze) in itertools.product( + enumerate( + [ + lambda: lattice_connection_array(GRID_N), + lambda: np.flip(lattice_connection_array(GRID_N), axis=1), + lambda: lattice_connection_array(GRID_N - 1), + lambda: numpy_rng.choice( + lattice_connection_array(GRID_N), 2 * GRID_N, axis=0 + ), + lambda: numpy_rng.choice( + lattice_connection_array(GRID_N), 1, axis=0 + ), + ] + ), + enumerate(MAZE_DATASET.mazes), + ) + ], +) +def test_is_connection(edges: ConnectionArray, maze: LatticeMaze): + output = is_connection(edges, maze.connection_list) + sorted_edges = np.sort(edges, axis=1) + edge_direction = ( + (sorted_edges[:, 1, :] - sorted_edges[:, 0, :])[:, 0] == 0 + ).astype(np.int8) + assert np.array_equal( + output, + maze.connection_list[ + edge_direction, sorted_edges[:, 0, 0], sorted_edges[:, 0, 1] + ], + ) diff --git a/tests/unit/maze_dataset/tokenization/test_tokenizer.py b/tests/unit/maze_dataset/tokenization/test_tokenizer.py index d22382d2..3f07a45b 100644 --- a/tests/unit/maze_dataset/tokenization/test_tokenizer.py +++ b/tests/unit/maze_dataset/tokenization/test_tokenizer.py @@ -1,14 +1,61 @@ +import itertools +import random import re from collections import Counter from itertools import product -from typing import Iterable +from typing import Iterable, Sequence +import frozendict +import numpy as np +from jaxtyping import Int +from muutils.misc import flatten +from muutils.mlutils import GLOBAL_SEED from pytest import mark, param -from maze_dataset import MazeDataset, MazeDatasetConfig, SolvedMaze +from maze_dataset import ( + VOCAB, + ConnectionArray, + Coord, + CoordArray, + CoordTup, + LatticeMaze, + MazeDataset, + MazeDatasetConfig, + SolvedMaze, +) from maze_dataset.generation import LatticeMazeGenerators from maze_dataset.plotting.print_tokens import color_maze_tokens_AOTP -from maze_dataset.tokenization import MazeTokenizer, TokenizationMode +from maze_dataset.testing_utils import ( + ASCII_MAZES, + LEGACY_AND_EQUIVALENT_TOKENIZERS, + MANUAL_MAZE, + MAZE_DATASET, + MIXED_MAZES, +) +from maze_dataset.token_utils import ( + connection_list_to_adj_list, + equal_except_adj_list_sequence, +) +from maze_dataset.tokenization import ( + AdjListTokenizers, + CoordTokenizers, + EdgeGroupings, + EdgePermuters, + EdgeSubsets, + MazeTokenizer, + MazeTokenizerModular, + PathTokenizers, + PromptSequencers, + StepSizes, + StepTokenizers, + TargetTokenizers, + TokenizationMode, + _TokenizerElement, +) +from maze_dataset.utils import all_instances, lattice_max_degrees, manhattan_distance + +# Use for test fuzzing when there are too many possible tokenizers +NUM_TOKENIZERS_TO_TEST = 100 @mark.parametrize( @@ -66,9 +113,7 @@ def test_tokenizer(): assert tokenizer.name == f"maze_tokenizer-{mode.name}-g{100}" if mode == TokenizationMode.AOTP_CTT_indexed: - # TODO: fix these asserts assert tokenizer.node_strings_map is not None - # assert len(tokenizer.node_strings_map) == 100 # `tokenizer.node_strings_map` is a `Kappa` which has no length assert 100 < tokenizer.vocab_size < 200 elif mode in ( TokenizationMode.AOTP_UT_rasterized, @@ -111,116 +156,595 @@ def test_tokenizer(): print(color_maze_tokens_AOTP(maze_tok, fmt="terminal")) -_ASCII_MAZES: dict[str, tuple[str, list[str]]] = dict( - small_3x3=( - " (2,0) <--> (2,1) ; (0,0) <--> (0,1) ; (0,0) <--> (1,0) ; (0,2) <--> (1,2) ; (1,0) <--> (2,0) ; (0,2) <--> (0,1) ; (2,2) <--> (2,1) ; (1,1) <--> (2,1) ; (0,0) (2,1) (0,0) (1,0) (2,0) (2,1) ", - [ - "#######", - "#S #", - "#X### #", - "#X# # #", - "#X# ###", - "#XXE #", - "#######", - ], - ), - big_10x10=( - " (8,2) <--> (8,3) ; (3,7) <--> (3,6) ; (6,7) <--> (6,8) ; (4,6) <--> (5,6) ; (9,5) <--> (9,4) ; (3,3) <--> (3,4) ; (5,1) <--> (4,1) ; (2,6) <--> (2,7) ; (8,5) <--> (8,4) ; (1,9) <--> (2,9) ; (4,1) <--> (4,2) ; (0,8) <--> (0,7) ; (5,4) <--> (5,3) ; (6,3) <--> (6,4) ; (5,0) <--> (4,0) ; (5,3) <--> (5,2) ; (3,1) <--> (2,1) ; (9,1) <--> (9,0) ; (3,5) <--> (3,6) ; (5,5) <--> (6,5) ; (7,1) <--> (7,2) ; (0,1) <--> (1,1) ; (7,8) <--> (8,8) ; (3,9) <--> (4,9) ; (4,6) <--> (4,7) ; (0,6) <--> (0,7) ; (3,4) <--> (3,5) ; (6,0) <--> (5,0) ; (7,7) <--> (7,6) ; (1,6) <--> (0,6) ; (6,1) <--> (6,0) ; (8,6) <--> (8,7) ; (9,9) <--> (9,8) ; (1,8) <--> (1,9) ; (2,1) <--> (2,2) ; (9,2) <--> (9,3) ; (5,9) <--> (6,9) ; (3,2) <--> (2,2) ; (0,8) <--> (0,9) ; (5,6) <--> (5,7) ; (2,3) <--> (2,4) ; (4,5) <--> (4,4) ; (8,9) <--> (8,8) ; (9,6) <--> (8,6) ; (3,7) <--> (3,8) ; (8,0) <--> (7,0) ; (6,1) <--> (6,2) ; (0,1) <--> (0,0) ; (7,3) <--> (7,4) ; (9,4) <--> (9,3) ; (9,6) <--> (9,5) ; (8,7) <--> (7,7) ; (5,2) <--> (5,1) ; (0,0) <--> (1,0) ; (7,2) <--> (7,3) ; (2,5) <--> (2,6) ; (4,9) <--> (5,9) ; (5,5) <--> (5,4) ; (5,6) <--> (6,6) ; (7,8) <--> (7,9) ; (1,7) <--> (2,7) ; (4,6) <--> (4,5) ; (1,1) <--> (1,2) ; (3,1) <--> (3,0) ; (1,5) <--> (1,6) ; (8,3) <--> (8,4) ; (9,9) <--> (8,9) ; (8,5) <--> (7,5) ; (1,4) <--> (2,4) ; (3,0) <--> (4,0) ; (3,3) <--> (4,3) ; (6,9) <--> (6,8) ; (1,0) <--> (2,0) ; (6,0) <--> (7,0) ; (8,0) <--> (9,0) ; (2,3) <--> (2,2) ; (2,8) <--> (3,8) ; (5,7) <--> (6,7) ; (1,3) <--> (0,3) ; (9,7) <--> (9,8) ; (7,5) <--> (7,4) ; (1,8) <--> (2,8) ; (6,5) <--> (6,4) ; (0,2) <--> (1,2) ; (0,7) <--> (1,7) ; (0,3) <--> (0,2) ; (4,3) <--> (4,2) ; (5,8) <--> (4,8) ; (9,1) <--> (8,1) ; (9,2) <--> (8,2) ; (1,3) <--> (1,4) ; (2,9) <--> (3,9) ; (4,8) <--> (4,7) ; (0,5) <--> (0,4) ; (8,1) <--> (7,1) ; (0,3) <--> (0,4) ; (9,7) <--> (9,6) ; (7,6) <--> (6,6) ; (1,5) <--> (0,5) ; (6,2) (2,1) (6,2) (6,1) (6,0) (5,0) (4,0) (3,0) (3,1) (2,1) ", - [ - "#####################", - "# # # #", - "# # # # ### # # #####", - "# # # # # # #", - "# ####### ##### # # #", - "# #E # # # #", - "###X# ########### # #", - "#XXX# # # #", - "#X##### ########### #", - "#X# # # #", - "#X# ######### ### # #", - "#X# # # # #", - "#X######### # # ### #", - "#XXXXS# # # #", - "# ########### #######", - "# # # # #", - "# # ####### ### # ###", - "# # # # # #", - "# # # ####### ##### #", - "# # #", - "#####################", - ], - ), -) - - @mark.parametrize( - "maze_ascii, tok_mode, tokens", + "maze_ascii, tokenizer, tokens", [ param( - _ASCII_MAZES[maze_ascii_key][1], # maze_ascii - tok_mode, # tok_mode - _ASCII_MAZES[maze_ascii_key][0], # tokens - id=f"{tok_mode.name}_{maze_ascii_key}", + ASCII_MAZES[maze_ascii_key][1], # maze_ascii + tokenizer, # tok_mode + ASCII_MAZES[maze_ascii_key][0], # tokens + id=f"{tokenizer.name}_{maze_ascii_key}", ) - for maze_ascii_key, tok_mode in product( + for maze_ascii_key, tokenizer in product( ["small_3x3", "big_10x10"], - [ - TokenizationMode.AOTP_UT_uniform, - TokenizationMode.AOTP_UT_rasterized, - TokenizationMode.AOTP_CTT_indexed, - ], + LEGACY_AND_EQUIVALENT_TOKENIZERS, ) ], ) def test_maze_to_tokens_roundtrip( maze_ascii: list[str], - tok_mode: TokenizationMode, + tokenizer: MazeTokenizer | MazeTokenizerModular, tokens: str, ): - if tok_mode == TokenizationMode.AOTP_CTT_indexed: - # The hardcoded `tokens` assumes a UT tokenizer. Modify `tokens` to match what a `AOTP_CTT_indexed` tokenizer would produce. + if not tokenizer.is_UT(): + # The hardcoded `tokens` assumes a UT tokenizer. + # Here we modify `tokens` to match what a `AOTP_CTT_indexed` tokenizer would produce. tokens = re.sub(r"\(([0-9]),([0-9])\)", r"(\1 , \2)", tokens) tokens = re.sub(r"\(([0-9]+ ,)", r"( \1", tokens) tokens = re.sub(r"(, [0-9]+)\)", r"\1 )", tokens) tokens_original_split: list[str] = tokens.split() - def get_token_regions(toks: list[str]) -> tuple[list[str], list[str]]: - adj_list_start, adj_list_end = toks.index("") + 1, tokens.index( - "" - ) - adj_list = toks[adj_list_start:adj_list_end] - non_adj_list = toks[:adj_list_start] + toks[adj_list_end:] - return adj_list, non_adj_list - # join into a single string, and get a maze out ascii_str: str = "\n".join(maze_ascii) maze: SolvedMaze = SolvedMaze.from_ascii(ascii_str) - # init tokenizer - tokenizer: MazeTokenizer = MazeTokenizer(tokenization_mode=tok_mode) # maze as tokens tokens_from_maze: list[str] = maze.as_tokens(tokenizer) - adj_list, non_adj_list = get_token_regions(tokens_from_maze) # maze round trip maze_roundtrip: SolvedMaze = SolvedMaze.from_tokens(tokens_from_maze, tokenizer) tokens_roundtrip: list[str] = maze_roundtrip.as_tokens(tokenizer) - adj_list_rt, non_adj_list_rt = get_token_regions(tokens_roundtrip) - - # regions from original tokens - adj_list_orig, non_adj_list_orig = get_token_regions(tokens_original_split) - # check that the maze works + # check that the mazes and tokens are all equivalent assert maze == maze_roundtrip + assert equal_except_adj_list_sequence(tokens_original_split, tokens_from_maze) + assert equal_except_adj_list_sequence(tokens_original_split, tokens_roundtrip) + + +@mark.parametrize( + "tok_mode, max_grid_size, result", + [ + param( + tok_mode, + max_grid_size, + MazeTokenizer(tokenization_mode=tok_mode, max_grid_size=max_grid_size), + id=f"{tok_mode}-{max_grid_size}", + ) + for tok_mode, max_grid_size in [ + (TokenizationMode.AOTP_CTT_indexed, None), + (TokenizationMode.AOTP_UT_rasterized, None), + (TokenizationMode.AOTP_UT_uniform, None), + (TokenizationMode.AOTP_CTT_indexed, 5), + ] + ], +) +def test_to_legacy_tokenizer( + tok_mode: TokenizationMode, max_grid_size: int | None, result: MazeTokenizer +): + assert tok_mode.to_legacy_tokenizer(max_grid_size) == result + + +# MazeTokenizerModular tests +# ===================== + +# Backwards compatibility tests +# ============================= + + +@mark.parametrize( + "maze,legacy_tokenizer", + [ + param(maze[0], tok_spec, id=f"{tok_spec.value}-maze{maze[1]}") + for maze, tok_spec in itertools.product( + [(maze, i) for i, maze in enumerate(MIXED_MAZES)], + [tok_mode for tok_mode in TokenizationMode], + ) + ], +) +def test_to_tokens_backwards_compatible( + maze: SolvedMaze, legacy_tokenizer: TokenizationMode +): + tokenizer: MazeTokenizerModular = MazeTokenizerModular.from_legacy(legacy_tokenizer) + toks: list[str] = maze.as_tokens(tokenizer) + toks2: list[str] = tokenizer.to_tokens(maze) + toks_legacy: list[str] = maze.as_tokens(legacy_tokenizer) + + try: + assert equal_except_adj_list_sequence(toks, toks_legacy) + assert equal_except_adj_list_sequence(toks2, toks_legacy) + except AssertionError as e: + raise AssertionError( + "Tokens from `as_tokens` and `to_tokens` should be equal to tokens from `as_tokens` with the legacy tokenizer.\n" + f"{len(toks) = }, {len(toks2) = }, {len(toks_legacy) = }\n" + f"{toks = }\n{toks2 = }\n{toks_legacy = }", + ) from e + + +@mark.parametrize( + "coords, legacy_tok_mode", + [ + param( + coords, + tok_mode, + id=f"{tok_mode.value}-coords(type={type(coords[0])},len={len(coords)})", + ) + for tok_mode, coords in itertools.product( + [tok_mode for tok_mode in TokenizationMode], + [ + *[[maze.start_pos] for maze in MAZE_DATASET.mazes[:2]], + [maze.start_pos for maze in MAZE_DATASET.mazes], + *[[tuple(maze.start_pos)] for maze in MAZE_DATASET.mazes[:2]], + [tuple(maze.start_pos) for maze in MAZE_DATASET.mazes], + ], + ) + ], +) +def test_coords_to_strings_backwards_compatible( + coords: list[Coord, CoordTup], legacy_tok_mode: TokenizationMode +): + tokenizer: MazeTokenizerModular = MazeTokenizerModular.from_legacy(legacy_tok_mode) + legacy_tokenizer = MazeTokenizer(tokenization_mode=legacy_tok_mode) + strings: list[str] = tokenizer.coords_to_strings(coords) + strings_legacy: list[str] = legacy_tokenizer.coords_to_strings(coords) + assert strings == strings_legacy + + +@mark.parametrize( + "maze,tok_mode", + [ + param(maze[0], tok_spec, id=f"{tok_spec.value}-maze{maze[1]}") + for maze, tok_spec in itertools.product( + [(maze, i) for i, maze in enumerate(MIXED_MAZES)], + [tok_mode for tok_mode in TokenizationMode], + ) + ], +) +def test_from_tokens_backwards_compatible( + maze: LatticeMaze, tok_mode: TokenizationMode +): + tokenizer = MazeTokenizerModular.from_legacy(tok_mode) + toks = maze.as_tokens(tok_mode) + # Equality test of `as_tokens` output done in a separate unit test + maze_legacy: LatticeMaze = LatticeMaze.from_tokens(toks, tok_mode) + maze: LatticeMaze = LatticeMaze.from_tokens(toks, tokenizer) + assert maze == maze_legacy + + +# General functionality tests +# =========================== - # check that the counters match - counter_original: Counter = Counter(tokens_original_split) - counter_from_maze: Counter = Counter(tokens_from_maze) - counter_roundtrip: Counter = Counter(tokens_roundtrip) - assert counter_original == counter_from_maze - assert counter_original == counter_roundtrip +@mark.parametrize( + "el, result", + [ + param(elem, result, id=elem.name) + for elem, result in [ + (CoordTokenizers.CTT(), True), + (CoordTokenizers.CTT(intra=True), True), + (CoordTokenizers.UT(), True), + (AdjListTokenizers.AdjListCoord(), True), + (AdjListTokenizers.AdjListCoord(post=True), True), + (TargetTokenizers.Unlabeled(post=True), True), + (PathTokenizers.StepSequence(), True), + ( + PathTokenizers.StepSequence(step_tokenizers=(StepTokenizers.Coord(),)), + True, + ), + ( + PathTokenizers.StepSequence( + step_tokenizers=( + StepTokenizers.Coord(), + StepTokenizers.Coord(), + ) + ), + False, + ), + (PromptSequencers.AOP(), True), + (PromptSequencers.AOP(path_tokenizer=PathTokenizers.StepSequence()), True), + ( + PromptSequencers.AOP( + path_tokenizer=PathTokenizers.StepSequence( + step_tokenizers=(StepTokenizers.Coord(),) + ) + ), + True, + ), + ( + PromptSequencers.AOP( + path_tokenizer=PathTokenizers.StepSequence( + step_tokenizers=( + StepTokenizers.Coord(), + StepTokenizers.Coord(), + ) + ) + ), + True, + ), + ] + ], +) +def test_tokenizer_element_is_valid(el: _TokenizerElement, result: bool): + assert el.is_valid() == result - # check that the token regions match - assert non_adj_list_orig == non_adj_list - assert non_adj_list_rt == non_adj_list + +@mark.parametrize( + "tokenizer, result", + [ + param(tokenizer, result, id=str(tokenizer)) + for tokenizer, result in [ + (MazeTokenizerModular(), True), + (MazeTokenizerModular.from_legacy(TokenizationMode.AOTP_CTT_indexed), True), + (MazeTokenizerModular(prompt_sequencer=PromptSequencers.AOP()), False), + ] + ], +) +def test_is_legacy_equivalent(tokenizer: MazeTokenizerModular, result: bool): + assert tokenizer.is_legacy_equivalent() == result + + +def _helper_test_path_tokenizers( + pt: PathTokenizers._PathTokenizer, + maze: SolvedMaze, + footprint_inds: Sequence[int], +): + ct: CoordTokenizers._CoordTokenizer = CoordTokenizers.UT() + path_toks: list[str] = pt.to_tokens(maze, ct) + path_toks_set: set[str] = set(path_toks) + footprint_inds: Int[np.ndarray, "footprint_index"] = np.array(footprint_inds) + footprints: Int[np.ndarray, "footprint_index row_col=2"] = maze.solution[ + footprint_inds + ] + if StepTokenizers.Coord() in pt.step_tokenizers: + non_steps: set[CoordTup] = set(tuple(c) for c in maze.solution) - set( + tuple(c) for c in footprints + ) + assert all([ct.to_tokens(coord)[0] in path_toks_set for coord in footprints]) + assert all([ct.to_tokens(coord)[0] not in path_toks_set for coord in non_steps]) + if StepTokenizers.Distance() in pt.step_tokenizers: + distances: list[int] = footprint_inds[1:] - footprint_inds[:-1] + assert ( + len( + Counter(getattr(VOCAB, f"I_{d:03}") for d in distances) + - Counter(path_toks) + ) + == 0 + ) + if StepTokenizers.Cardinal() in pt.step_tokenizers: + c = Counter(path_toks) + assert ( + c[VOCAB.PATH_NORTH] + + c[VOCAB.PATH_SOUTH] + + c[VOCAB.PATH_EAST] + + c[VOCAB.PATH_WEST] + == len(footprint_inds) - 1 + ) + if StepTokenizers.Relative() in pt.step_tokenizers: + c = Counter(path_toks) + assert ( + c[VOCAB.PATH_LEFT] + + c[VOCAB.PATH_RIGHT] + + c[VOCAB.PATH_FORWARD] + + c[VOCAB.PATH_BACKWARD] + == len(footprint_inds) - 1 + ) + + +@mark.parametrize( + "pt,manual_maze", + [ + param(tokenizer, maze_kv[1], id=f"{tokenizer.name}-{maze_kv[0]}") + for maze_kv, tokenizer in itertools.product( + ASCII_MAZES.items(), + random.sample( + list( + all_instances( + PathTokenizers._PathTokenizer, + {_TokenizerElement: lambda x: x.is_valid()}, + ) + ), + NUM_TOKENIZERS_TO_TEST, + ), + ) + ], +) +def test_path_tokenizers(pt: PathTokenizers._PathTokenizer, manual_maze: MANUAL_MAZE): + solved_maze: SolvedMaze = SolvedMaze.from_ascii("\n".join(manual_maze.ascii)) + match type(pt.step_size): + case StepSizes.Singles: + footprint_inds = range(solved_maze.solution.shape[0]) + case StepSizes.Straightaways: + swy_coordtup_set: set[CoordTup] = set( + tuple(c) for c in manual_maze.straightaway_footprints + ) + footprint_inds: list[int] = [ + i + for i, c in enumerate(solved_maze.solution) + if tuple(c) in swy_coordtup_set + ] + case StepSizes.Forks: + footprint_inds = solved_maze.get_solution_forking_points( + always_include_endpoints=True + )[0] + case StepSizes.ForksAndStraightaways: + swy_step_inds: list[int] = StepSizes.Straightaways()._step_single_indices( + solved_maze + ) + footprint_inds: Int[np.ndarray, "footprint_index"] = np.concatenate( + ( + solved_maze.get_solution_forking_points( + always_include_endpoints=True + )[0], + swy_step_inds, + ) + ) + footprint_inds, _ = np.unique(footprint_inds, axis=0, return_index=True) + _helper_test_path_tokenizers( + pt, + solved_maze, + footprint_inds, + ) + + +@mark.parametrize( + "ep,maze", + [ + param(tokenizer, maze, id=f"{tokenizer.name}-maze[{i}]") + for (i, maze), tokenizer in itertools.product( + enumerate(MIXED_MAZES[:6]), + all_instances( + EdgePermuters._EdgePermuter, + frozendict.frozendict({_TokenizerElement: lambda x: x.is_valid()}), + ), + ) + ], +) +def test_edge_permuters(ep: EdgePermuters._EdgePermuter, maze: LatticeMaze): + edges: ConnectionArray = connection_list_to_adj_list( + maze.connection_list, shuffle_d0=False, shuffle_d1=False + ) + edges_copy: ConnectionArray = connection_list_to_adj_list( + maze.connection_list, shuffle_d0=False, shuffle_d1=False + ) + assert np.array_equal(edges, edges_copy) + old_shape = edges.shape + permuted: ConnectionArray = ep._permute(edges) + match ep: + case EdgePermuters.RandomCoords(): + assert permuted.shape == old_shape + assert edges is permuted + i = 0 + while np.array_equal(permuted, edges_copy) and i < 2: + # Permute again in case for small mazes the random selection happened to not change anything + permuted: ConnectionArray = ep._permute(permuted) + i += 1 + assert not np.array_equal(permuted, edges_copy) + case EdgePermuters.BothCoords(): + new_shape = old_shape[0] * 2, *old_shape[1:] + n = old_shape[0] + assert permuted.shape == new_shape + assert np.array_equal(permuted[:n, ...], edges_copy) + assert np.array_equal(permuted[:n, 0, :], permuted[n:, 1, :]) + assert np.array_equal(permuted[:n, 1, :], permuted[n:, 0, :]) + assert edges is not permuted + + +@mark.parametrize( + "es,maze", + [ + param(tokenizer, maze, id=f"{tokenizer.name}-maze[{i}]") + for (i, maze), tokenizer in itertools.product( + enumerate(MIXED_MAZES[:6]), + all_instances( + EdgeSubsets._EdgeSubset, + frozendict.frozendict({_TokenizerElement: lambda x: x.is_valid()}), + ), + ) + ], +) +def test_edge_subsets(es: EdgeSubsets._EdgeSubset, maze: LatticeMaze): + edges: ConnectionArray = es._get_edges(maze) + n: int = maze.grid_n + match type(es): + case EdgeSubsets.AllLatticeEdges: + assert_shape: tuple = (2 * n * (n - 1), 2, 2) + case EdgeSubsets.ConnectionEdges: + if not es.walls: + assert_shape: tuple = (np.count_nonzero(maze.connection_list), 2, 2) + else: + assert_shape: tuple = ( + 2 * n * (n - 1) - np.count_nonzero(maze.connection_list), + 2, + 2, + ) + assert edges.dtype == np.int8 + assert assert_shape == tuple(edges.shape) + assert assert_shape == tuple( + np.unique(edges, axis=0).shape + ) # All edges are unique (swapping leading/trailing coords is considered different) + assert np.array_equal( + manhattan_distance(edges), np.array([1] * assert_shape[0], dtype=np.int8) + ) + + +@mark.parametrize( + "tok_elem,es,maze", + [ + param(tok_elem, es, maze, id=f"{tok_elem.name}-{es.name}-maze[{i}]") + for (i, maze), tok_elem, es in itertools.product( + enumerate(MIXED_MAZES[:6]), + all_instances( + EdgeGroupings._EdgeGrouping, + frozendict.frozendict( + { + _TokenizerElement: lambda x: x.is_valid(), + # Add a condition to prune the range space that doesn't affect functionality being tested + EdgeGroupings.ByLeadingCoord: lambda x: x.intra + and x.connection_token_ordinal == 1, + } + ), + ), + all_instances( + EdgeSubsets._EdgeSubset, + frozendict.frozendict({_TokenizerElement: lambda x: x.is_valid()}), + ), + ) + ], +) +def test_edge_groupings( + tok_elem: EdgeGroupings._EdgeGrouping, + es: EdgeSubsets._EdgeSubset, + maze: LatticeMaze, +): + edges: ConnectionArray = es._get_edges(maze) + n: int = maze.grid_n + groups: Sequence[ConnectionArray] = tok_elem._group_edges(edges) + + assert all( + not np.any(np.diff(g[:, 0], axis=0)) for g in groups + ) # Asserts that the leading coord is the same for all edges within each group + match type(tok_elem): + case EdgeGroupings.Ungrouped: + assert_shape = edges.shape[0], 1, 2, 2 + assert tuple(groups.shape) == assert_shape + case EdgeGroupings.ByLeadingCoord: + assert len(groups) == np.unique(edges[:, 0, :], axis=0).shape[0] + assert sum(g.shape[0] for g in groups) == edges.shape[0] + trailing_coords: list[CoordArray] = [g[:, 1, :] for g in groups] + # vector_diffs is the position vector difference between the trailing coords of each group + # These are stacked into a single array since we don't care about maintaining group separation + vector_diffs: CoordArray = np.stack( + list(flatten([np.diff(g[:, 1, :], axis=0) for g in groups], 1)) + ) + if tok_elem.shuffle_group: + allowed_diffs = {(1, -1), (1, 1), (0, 2), (2, 0)} + # The set of all 2D vectors between any 2 coords adjacent to a central coord + allowed_diffs = allowed_diffs.union( + {(-d[0], -d[1]) for d in allowed_diffs} + ) + else: + # If vector_diffs are lexicographically sorted, these are the only possible values. Any other value indicates an error in sorting + allowed_diffs = {(1, -1), (1, 1), (0, 2), (2, 0)} + assert all( + tuple(diff) in allowed_diffs for diff in np.unique(vector_diffs, axis=0) + ) + + +random.seed(GLOBAL_SEED) + + +@mark.parametrize( + "tok_elem,maze", + [ + param(tok_elem, maze, id=f"{tok_elem.name}-maze[{i}]") + for (i, maze), tok_elem in itertools.product( + enumerate(MAZE_DATASET), + random.sample( + list( + all_instances( + AdjListTokenizers._AdjListTokenizer, + { + _TokenizerElement: lambda x: x.is_valid(), + }, + ) + ), + 100, + ), + ) + ], +) +def test_adjlist_tokenizers( + tok_elem: AdjListTokenizers._AdjListTokenizer, maze: LatticeMaze +): + toks: list[str] = tok_elem.to_tokens(maze, CoordTokenizers.UT()) + tok_counter: Counter = Counter(toks) + n: int = maze.grid_n + edge_count: int = 1 # To be updated in match/case blocks + group_count: int = 1 # To be updated in match/case blocks + + match tok_elem.edge_subset: + case EdgeSubsets.AllLatticeEdges(): + edge_count *= n * (n - 1) * 2 + case EdgeSubsets.ConnectionEdges(walls=False): + edge_count *= np.count_nonzero(maze.connection_list) + case EdgeSubsets.ConnectionEdges(walls=True): + edge_count *= n * (n - 1) * 2 - np.count_nonzero(maze.connection_list) + case _: + raise NotImplementedError( + f"`match` case missing for {tok_elem.edge_subset=}" + ) + + match tok_elem.edge_permuter: + case EdgePermuters.BothCoords(): + edge_count *= 2 + if tok_elem.edge_subset == EdgeSubsets.ConnectionEdges(walls=True): + group_count *= np.count_nonzero( + lattice_max_degrees(n) - maze.coord_degrees() > 0 + ) # All coords with 1 adjacent wall, not counting outer boundaries + else: + group_count *= np.count_nonzero( + maze.coord_degrees() > 0 + ) # All coords with >0 connections + case EdgePermuters.RandomCoords() | EdgePermuters.SortedCoords(): + edge_count *= 1 + group_count = None # Group count is stochastic + + match type(tok_elem.edge_grouping): + case EdgeGroupings.Ungrouped: + group_count = edge_count # Override all above cases + case EdgeGroupings.ByLeadingCoord: + if group_count is not None: + group_count *= 1 + if tok_elem.edge_grouping.intra: + assert tok_counter[VOCAB.ADJLIST_INTRA] == edge_count + case _: + raise NotImplementedError( + f"`match` case missing for {tok_elem.edge_grouping=}" + ) + + match type(tok_elem): + case AdjListTokenizers.AdjListCoord: + pass + case AdjListTokenizers.AdjListCardinal: + assert ( + tok_counter[VOCAB.PATH_NORTH] + + tok_counter[VOCAB.PATH_SOUTH] + + tok_counter[VOCAB.PATH_EAST] + + tok_counter[VOCAB.PATH_WEST] + == edge_count + ) + + if group_count is not None: + if tok_elem.pre: + assert tok_counter[VOCAB.ADJLIST_PRE] == group_count + if tok_elem.post: + assert tok_counter[VOCAB.ADJACENCY_ENDLINE] == group_count + + assert tok_counter[VOCAB.CONNECTOR] + tok_counter[VOCAB.ADJLIST_WALL] == edge_count + + +@mark.parametrize( + "tok_elem, valid", + [ + param( + tok_elem, + valid, + id=f"{repr(tok_elem)}", + ) + for tok_elem, valid in ( + [ + (StepSizes.ForksAndStraightaways(), False), + (StepSizes.Straightaways(), False), + (StepSizes.Forks(), True), + (AdjListTokenizers.AdjListCoord(), True), + (AdjListTokenizers.AdjListCoord(pre=True), False), + (AdjListTokenizers.AdjListCardinal(), True), + (AdjListTokenizers.AdjListCardinal(pre=True), False), + (EdgeGroupings.Ungrouped(), True), + (EdgeGroupings.ByLeadingCoord(), False), + (EdgeGroupings.ByLeadingCoord(connection_token_ordinal=0), False), + ] + ) + ], +) +def test_unsupported_elements(tok_elem: _TokenizerElement, valid: bool): + assert tok_elem.is_valid() == valid diff --git a/tests/unit/maze_dataset/tokenization/test_special_tokens.py b/tests/unit/maze_dataset/tokenization/test_vocab.py similarity index 59% rename from tests/unit/maze_dataset/tokenization/test_special_tokens.py rename to tests/unit/maze_dataset/tokenization/test_vocab.py index 495cac25..129c3630 100644 --- a/tests/unit/maze_dataset/tokenization/test_special_tokens.py +++ b/tests/unit/maze_dataset/tokenization/test_vocab.py @@ -1,6 +1,11 @@ import pytest -from maze_dataset.constants import SPECIAL_TOKENS +from maze_dataset.constants import ( + SPECIAL_TOKENS, + VOCAB, + VOCAB_LIST, + VOCAB_TOKEN_TO_INDEX, +) def test_special_tokens_base(): @@ -25,3 +30,14 @@ def test_special_tokens_base(): # Test the keys method assert "ADJLIST_START" in SPECIAL_TOKENS.keys() + + +def test_vocab(): + assert len(VOCAB) == 4096 + assert VOCAB.CTT_10 == "10" + assert VOCAB_LIST[0] == "" + assert VOCAB_LIST[706] == "&" + assert VOCAB_TOKEN_TO_INDEX[""] == 19 + assert VOCAB_TOKEN_TO_INDEX["0"] == 320 + assert VOCAB_TOKEN_TO_INDEX["-1"] == 703 + assert VOCAB_TOKEN_TO_INDEX["(0,0)"] == 1596 diff --git a/tests/unit/maze_dataset/utils.py b/tests/unit/maze_dataset/utils.py new file mode 100644 index 00000000..b09b5e87 --- /dev/null +++ b/tests/unit/maze_dataset/utils.py @@ -0,0 +1,299 @@ +import abc +from dataclasses import dataclass +from typing import Callable, Iterable, Literal + +import pytest +from muutils.misc import IsDataclass, dataclass_set_equals +from pytest import mark, param + +from maze_dataset.utils import FiniteValued, all_instances + + +# Test classes +@dataclass +class DC1: + x: bool + y: bool = False + + +@dataclass(frozen=True) +class DC2: + x: bool + y: bool = False + + +@dataclass(frozen=True) +class DC3: + x: DC2 = DC2(False, False) + + +@dataclass(frozen=True) +class DC4: + x: DC2 + y: bool = False + + +@dataclass(frozen=True) +class DC5: + x: int + + +@dataclass(frozen=True) +class DC6: + x: DC5 + y: bool = False + + +@dataclass(frozen=True) +class DC7(abc.ABC): + x: bool + + @abc.abstractmethod + def foo(): + pass + + +@dataclass(frozen=True) +class DC8(DC7): + x: bool = False + + def foo(): + pass + + +@dataclass(frozen=True) +class DC9(DC7): + y: bool = True + + def foo(): + pass + + +@mark.parametrize( + "type_, validation_funcs, result", + [ + param( + type_, + vfs, + result, + id=f"{type_}-vfs[{len(vfs) if vfs is not None else 'None'}]", + ) + for type_, vfs, result in ( + [ + ( + DC1, + None, + [ + DC1(False, False), + DC1(False, True), + DC1(True, False), + DC1(True, True), + ], + ), + ( + DC2, + None, + [ + DC2(False, False), + DC2(False, True), + DC2(True, False), + DC2(True, True), + ], + ), + ( + DC2, + {DC2: lambda dc: dc.x ^ dc.y}, + [ + DC2(False, True), + DC2(True, False), + ], + ), + ( + DC1 | DC2, + {DC2: lambda dc: dc.x ^ dc.y}, + [ + DC2(False, True), + DC2(True, False), + DC1(False, False), + DC1(False, True), + DC1(True, False), + DC1(True, True), + ], + ), + ( + DC1 | DC2, + { + DC1: lambda dc: dc.x == dc.y, + DC2: lambda dc: dc.x ^ dc.y, + }, + [ + DC2(False, True), + DC2(True, False), + DC1(False, False), + DC1(True, True), + ], + ), + ( + DC3, + None, + [ + DC3(DC2(False, False)), + DC3(DC2(False, True)), + DC3(DC2(True, False)), + DC3(DC2(True, True)), + ], + ), + ( + DC4, + None, + [ + DC4(DC2(False, False), True), + DC4(DC2(False, True), True), + DC4(DC2(True, False), True), + DC4(DC2(True, True), True), + DC4(DC2(False, False), False), + DC4(DC2(False, True), False), + DC4(DC2(True, False), False), + DC4(DC2(True, True), False), + ], + ), + ( + DC4, + {DC2: lambda dc: dc.x ^ dc.y}, + [ + DC4(DC2(False, True), True), + DC4(DC2(True, False), True), + DC4(DC2(False, True), False), + DC4(DC2(True, False), False), + ], + ), + (DC5, None, TypeError), + (DC6, None, TypeError), + (bool, None, [True, False]), + (bool, {bool: lambda x: x}, [True]), + (bool, {bool: lambda x: not x}, [False]), + (int, None, TypeError), + (str, None, TypeError), + (Literal[0, 1, 2], None, [0, 1, 2]), + (Literal[0, 1, 2], {int: lambda x: x % 2 == 0}, [0, 2]), + (bool | Literal[0, 1, 2], dict(), [0, 1, 2, True, False]), + (bool | Literal[0, 1, 2], {bool: lambda x: x}, [0, 1, 2, True]), + (bool | Literal[0, 1, 2], {int: lambda x: x % 2}, [1, True]), + ( + tuple[bool], + None, + [ + (True,), + (False,), + ], + ), + ( + tuple[bool, bool], + None, + [ + (True, True), + (True, False), + (False, True), + (False, False), + ], + ), + ( + tuple[bool, bool], + {bool: lambda x: x}, + [ + (True, True), + ], + ), + ( + DC8, + None, + [ + DC8(False), + DC8(True), + ], + ), + ( + DC7, + None, + [ + DC8(False), + DC8(True), + DC9(False, False), + DC9(False, True), + DC9(True, False), + DC9(True, True), + ], + ), + ( + tuple[DC7], + None, + [ + (DC8(False),), + (DC8(True),), + (DC9(False, False),), + (DC9(False, True),), + (DC9(True, False),), + (DC9(True, True),), + ], + ), + ( + tuple[DC7], + {DC9: lambda dc: dc.x == dc.y}, + [ + (DC8(False),), + (DC8(True),), + (DC9(False, False),), + (DC9(True, True),), + ], + ), + ( + tuple[DC8, DC8], + None, + [ + (DC8(False), DC8(False)), + (DC8(False), DC8(True)), + (DC8(True), DC8(False)), + (DC8(True), DC8(True)), + ], + ), + ( + tuple[DC7, bool], + None, + [ + (DC8(False), True), + (DC8(True), True), + (DC9(False, False), True), + (DC9(False, True), True), + (DC9(True, False), True), + (DC9(True, True), True), + (DC8(False), False), + (DC8(True), False), + (DC9(False, False), False), + (DC9(False, True), False), + (DC9(True, False), False), + (DC9(True, True), False), + ], + ), + ] + ) + ], +) +def test_all_instances( + type_: FiniteValued, + validation_funcs: dict[FiniteValued, Callable[[FiniteValued], bool]] | None, + result: type[Exception] | Iterable[FiniteValued], +): + if isinstance(result, type) and issubclass(result, Exception): + with pytest.raises(result): + list(all_instances(type_, validation_funcs)) + elif hasattr(type_, "__dataclass_fields__"): + assert dataclass_set_equals(all_instances(type_, validation_funcs), result) + else: # General case, due to nesting, results might contain some dataclasses and some other types + out = list(all_instances(type_, validation_funcs)) + assert dataclass_set_equals( + filter(lambda x: isinstance(x, IsDataclass), out), + filter(lambda x: isinstance(x, IsDataclass), result), + ) + assert set(filter(lambda x: not isinstance(x, IsDataclass), out)) == set( + filter(lambda x: not isinstance(x, IsDataclass), result) + )