hypergrid 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2023-present Justin Yan
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.1
2
+ Name: hypergrid
3
+ Version: 0.0.1
4
+ Summary: Hypergrid enables concise declaration of parameter grids for hyperparameter optimization and batch jobs.
5
+ Author-email: Justin Yan <justin@iomorphic.com>
6
+ Project-URL: Homepage, https://github.com/justin-yan/hypergrid
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: sklearn
20
+ Requires-Dist: scikit-learn<2; extra == "sklearn"
21
+
22
+ # hypergrid
23
+
24
+ Hypergrid enables concise declaration and manipulation of parameter grid spaces, with an aim towards use cases such as hyperparameter tuning or defining large batch jobs.
25
+
26
+ Use the following features to lazily declare a parameter grid:
27
+
28
+ - Dimension and Grid direct instantiation
29
+ - `+` and `|` for "sum" or "union" types (concatenation)
30
+ - `*` for "product" types
31
+ - `&` for coiteration (zip)
32
+ - `select` to project dimensions by name
33
+
34
+ There are also a few transformations that can be lazily applied element-wise, which take a GridElement (a namedtuple of dimension<->value) as input.
35
+
36
+ - `filter` to apply boolean predicate
37
+ - `map` for lambda transformation
38
+ - `map_to` for map + concat
39
+
40
+ Once a parameter grid is declared, there are two ways to "materialize" your grid, which return GridElements.
41
+
42
+ - `__iter__`: a grid is directly iterable
43
+ - `sample`: allows you to sample from the grid according to a sampling strategy
44
+
45
+ ## Usage Examples
46
+
47
+ ```python
48
+ from hypergrid.dsl import *
49
+ from dataclasses import dataclass
50
+
51
+ # First, we need to create a Dimension, which is essentially a named, finite, 1-d collection
52
+ d = Dimension(custom_name=[1, 2, 3]) # any python Collection will work - set, dict, range(), etc.
53
+ assert d.name == "custom_name" # the argument name is used as the dimension's name.
54
+ d.with_name("ints") # which you can reset
55
+ Uniform(low=1, high=5).take(5)
56
+ ExponentialStep(start=1, step=1.1).take(6) # You can also take a dimension from a Distribution or HIterable
57
+
58
+ # You can `len(d)` or `[i for i in d]`, but grids are more interesting
59
+ g = d.to_grid()
60
+ i2d = Dimension(ints=[4, 5, 6])
61
+ cd = Dimension(chars=["a", "b", "c", "d"])
62
+ union_ints = g + i2d # result is length 6: Concatenate two grids that have the same underlying dimensions
63
+ product_g = g * cd # result is length 12: tuples (1, "a") - take the cartesian product of the underlying grids
64
+ zip_g = g & cd # result is length 3: tuples (1, "a") - zip two grids together, up to the shorter grid
65
+
66
+ ml = [i for i in zip_g] # You can iterate through a grid
67
+ tl = zip_g.take(5) # or you can just take up to a certain number of grid elements from it
68
+ print(tl[0].ints), print(tl[0].chars) # The iterator elements are python NamedTuples taken from the dimension names.
69
+
70
+ # These gridelements can be referenced and used in the grid higher-order functions
71
+ zip_g.filter(lambda ge: ge.chars in ["a", "b"]) # result is length 2: keep the tuples (1, "a") and (2, "b")
72
+ zip_g.map(doubled=lambda ge: ge.ints * 2) # result is length 3, with single attribute (drops `ints` and `chars`)
73
+ mt = zip_g.map_to(doubled=lambda ge: ge.ints * 2) # result is length 3, appends `doubled` and keeps `ints` and `chars`
74
+ print(mt.select("doubled", "ints").take(1)[0]) # resulting gridelement no longer has `chars`
75
+
76
+ # There are some other utility methods on a grid:
77
+ zip_g.sample() # Randomly samples a single grid element from a grid
78
+ zip_g.to_sklearn() # The Grid.to_* methods convert HyperGrids to other grid formats
79
+
80
+ # The general idea is to allow for fairly extensive grid construction routines
81
+ @dataclass
82
+ class FakeModel:
83
+ idx: int
84
+ param1: float
85
+
86
+ g = HyperGrid( # A grid with 4 x 10 combinations
87
+ ExponentialStep(start=1.0, step=1.5).take(4).with_name("param1"),
88
+ idx=range(10)
89
+ ).instantiate(model=FakeModel).select("model") + \
90
+ HyperGrid( # A different grid with 15 combinations
91
+ Uniform(low=-1, high=1).take(5).with_name("param1"),
92
+ idx=[10, 20, 30]
93
+ ).instantiate(model=FakeModel).select("model")
94
+ assert len(g) == 55
95
+ g.sample()
96
+ ```
97
+
@@ -0,0 +1,76 @@
1
+ # hypergrid
2
+
3
+ Hypergrid enables concise declaration and manipulation of parameter grid spaces, with an aim towards use cases such as hyperparameter tuning or defining large batch jobs.
4
+
5
+ Use the following features to lazily declare a parameter grid:
6
+
7
+ - Dimension and Grid direct instantiation
8
+ - `+` and `|` for "sum" or "union" types (concatenation)
9
+ - `*` for "product" types
10
+ - `&` for coiteration (zip)
11
+ - `select` to project dimensions by name
12
+
13
+ There are also a few transformations that can be lazily applied element-wise, which take a GridElement (a namedtuple of dimension<->value) as input.
14
+
15
+ - `filter` to apply boolean predicate
16
+ - `map` for lambda transformation
17
+ - `map_to` for map + concat
18
+
19
+ Once a parameter grid is declared, there are two ways to "materialize" your grid, which return GridElements.
20
+
21
+ - `__iter__`: a grid is directly iterable
22
+ - `sample`: allows you to sample from the grid according to a sampling strategy
23
+
24
+ ## Usage Examples
25
+
26
+ ```python
27
+ from hypergrid.dsl import *
28
+ from dataclasses import dataclass
29
+
30
+ # First, we need to create a Dimension, which is essentially a named, finite, 1-d collection
31
+ d = Dimension(custom_name=[1, 2, 3]) # any python Collection will work - set, dict, range(), etc.
32
+ assert d.name == "custom_name" # the argument name is used as the dimension's name.
33
+ d.with_name("ints") # which you can reset
34
+ Uniform(low=1, high=5).take(5)
35
+ ExponentialStep(start=1, step=1.1).take(6) # You can also take a dimension from a Distribution or HIterable
36
+
37
+ # You can `len(d)` or `[i for i in d]`, but grids are more interesting
38
+ g = d.to_grid()
39
+ i2d = Dimension(ints=[4, 5, 6])
40
+ cd = Dimension(chars=["a", "b", "c", "d"])
41
+ union_ints = g + i2d # result is length 6: Concatenate two grids that have the same underlying dimensions
42
+ product_g = g * cd # result is length 12: tuples (1, "a") - take the cartesian product of the underlying grids
43
+ zip_g = g & cd # result is length 3: tuples (1, "a") - zip two grids together, up to the shorter grid
44
+
45
+ ml = [i for i in zip_g] # You can iterate through a grid
46
+ tl = zip_g.take(5) # or you can just take up to a certain number of grid elements from it
47
+ print(tl[0].ints), print(tl[0].chars) # The iterator elements are python NamedTuples taken from the dimension names.
48
+
49
+ # These gridelements can be referenced and used in the grid higher-order functions
50
+ zip_g.filter(lambda ge: ge.chars in ["a", "b"]) # result is length 2: keep the tuples (1, "a") and (2, "b")
51
+ zip_g.map(doubled=lambda ge: ge.ints * 2) # result is length 3, with single attribute (drops `ints` and `chars`)
52
+ mt = zip_g.map_to(doubled=lambda ge: ge.ints * 2) # result is length 3, appends `doubled` and keeps `ints` and `chars`
53
+ print(mt.select("doubled", "ints").take(1)[0]) # resulting gridelement no longer has `chars`
54
+
55
+ # There are some other utility methods on a grid:
56
+ zip_g.sample() # Randomly samples a single grid element from a grid
57
+ zip_g.to_sklearn() # The Grid.to_* methods convert HyperGrids to other grid formats
58
+
59
+ # The general idea is to allow for fairly extensive grid construction routines
60
+ @dataclass
61
+ class FakeModel:
62
+ idx: int
63
+ param1: float
64
+
65
+ g = HyperGrid( # A grid with 4 x 10 combinations
66
+ ExponentialStep(start=1.0, step=1.5).take(4).with_name("param1"),
67
+ idx=range(10)
68
+ ).instantiate(model=FakeModel).select("model") + \
69
+ HyperGrid( # A different grid with 15 combinations
70
+ Uniform(low=-1, high=1).take(5).with_name("param1"),
71
+ idx=[10, 20, 30]
72
+ ).instantiate(model=FakeModel).select("model")
73
+ assert len(g) == 55
74
+ g.sample()
75
+ ```
76
+
@@ -0,0 +1,117 @@
1
+ [build-system]
2
+ requires = ["setuptools"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "hypergrid"
7
+ version = "0.0.1"
8
+ authors = [
9
+ { name="Justin Yan", email="justin@iomorphic.com" }
10
+ ]
11
+ description = "Hypergrid enables concise declaration of parameter grids for hyperparameter optimization and batch jobs."
12
+ readme = "README.md"
13
+ requires-python = ">=3.11"
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ ]
25
+ dependencies = [
26
+ ######
27
+ ### Custom Dependencies Section Begin
28
+ ######
29
+
30
+ ######
31
+ ### Custom Dependencies Section End
32
+ ######
33
+ ]
34
+
35
+ [project.urls]
36
+ "Homepage" = "https://github.com/justin-yan/hypergrid"
37
+
38
+ [dependency-groups]
39
+ dev = [
40
+ "pytest>5",
41
+ "hypothesis>5",
42
+ "coverage>5",
43
+ "ruff>0.2.1",
44
+ "mypy>1.2"
45
+ ]
46
+
47
+ [tool.setuptools]
48
+ zip-safe = false
49
+ include-package-data = true
50
+
51
+ [tool.setuptools.package-data]
52
+ "hypergrid" = ["py.typed"]
53
+
54
+ [tool.setuptools.packages.find]
55
+ where = ["src"]
56
+
57
+ #######
58
+ ### Miscellaneous Tool Configuration
59
+ #######
60
+ [tool.ruff]
61
+ line-length = 150
62
+ target-version = "py311"
63
+
64
+ [tool.ruff.format]
65
+ quote-style = "double"
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "W", "I"]
69
+
70
+ [tool.ruff.lint.isort]
71
+ known-first-party = ["hypergrid"]
72
+
73
+ [tool.pytest.ini_options]
74
+ addopts = "-ra -q --doctest-modules"
75
+ log_cli = true
76
+ log_cli_level = "WARN"
77
+ log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
78
+ log_cli_date_format = "%Y-%m-%d %H:%M:%S"
79
+
80
+ [tool.mypy]
81
+ mypy_path = "src"
82
+ disallow_untyped_defs = true
83
+ disallow_any_unimported = true
84
+ allow_redefinition = false
85
+ ignore_errors = false
86
+ implicit_reexport = false
87
+ local_partial_types = true
88
+ no_implicit_optional = true
89
+ strict_equality = true
90
+ strict_optional = true
91
+ warn_no_return = true
92
+ warn_redundant_casts = true
93
+ warn_unreachable = true
94
+ warn_unused_configs = true
95
+ warn_unused_ignores = true
96
+
97
+ ######
98
+ ### Custom Directives Section Begin
99
+ ######
100
+
101
+
102
+ [[tool.mypy.overrides]]
103
+ module = [
104
+ "sklearn.*",
105
+ ]
106
+ ignore_errors = true
107
+ ignore_missing_imports = true
108
+
109
+
110
+ [project.optional-dependencies]
111
+ sklearn = [
112
+ "scikit-learn<2",
113
+ ]
114
+
115
+ ######
116
+ ### Custom Directives Section End
117
+ ######
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
File without changes
@@ -0,0 +1,44 @@
1
+ import random
2
+ from collections.abc import Collection
3
+ from typing import TYPE_CHECKING, Generic, Iterator, Self, TypeAlias, TypeVar
4
+
5
+ if TYPE_CHECKING:
6
+ from hypergrid.grid import HyperGrid
7
+
8
+ T = TypeVar("T")
9
+ RawDimension: TypeAlias = tuple[str, Collection]
10
+
11
+
12
+ class Dimension(Generic[T]):
13
+ name: str
14
+
15
+ def __init__(self, **kwargs: Collection[T]):
16
+ assert len(kwargs) == 1, "Dimension is 1-d, use Grids for multiple dimensions"
17
+ for name, values in kwargs.items():
18
+ assert isinstance(values, Collection), "Dimension assumes finite length"
19
+ self.name = name
20
+ self.values = values
21
+
22
+ def __repr__(self) -> str:
23
+ return f"Dimension({repr(self.values)})"
24
+
25
+ def __str__(self) -> str:
26
+ return self.__repr__()
27
+
28
+ def __len__(self) -> int:
29
+ return len(self.values)
30
+
31
+ def __iter__(self) -> Iterator[T]:
32
+ yield from self.values
33
+
34
+ def sample(self) -> T:
35
+ return random.choice(self.values) # type: ignore
36
+
37
+ def with_name(self, name: str) -> Self:
38
+ self.name = name
39
+ return self
40
+
41
+ def to_grid(self) -> "HyperGrid":
42
+ from hypergrid.grid import HyperGrid
43
+
44
+ return HyperGrid(self)
@@ -0,0 +1,6 @@
1
+ from hypergrid.dimension import Dimension
2
+ from hypergrid.gen.distribution import Uniform
3
+ from hypergrid.gen.iterable import ExponentialStep
4
+ from hypergrid.grid import HyperGrid
5
+
6
+ __all__ = ["HyperGrid", "Dimension", "Uniform", "ExponentialStep"]
File without changes
@@ -0,0 +1,32 @@
1
+ try:
2
+ from sklearn.model_selection import ParameterGrid
3
+ except ImportError:
4
+ raise ImportError("If using sklearn conversion functionality, install hypergrid with `sklearn` extras via `pip install hypergrid[sklearn]`")
5
+
6
+ from hypergrid.grid import Grid, HyperGrid, ProductGrid
7
+
8
+
9
+ def _grid_to_sklearn(grid: Grid) -> ParameterGrid: # type: ignore[no-any-unimported]
10
+ """
11
+ SKLearn's ParameterGrid accepts {str: sequence}
12
+
13
+ Because these ParameterGrids don't directly compose, we use a recursive helper, and then convert the composable dicts
14
+ into a ParameterGrid in this outer wrapper.
15
+ """
16
+ return ParameterGrid(_grid_to_sklearn_recursive_helper(grid))
17
+
18
+
19
+ def _grid_to_sklearn_recursive_helper(grid: Grid) -> dict:
20
+ """
21
+ SKLearn's param_grid dictionaries only support simple cartesian products, so only the Grid and ProductGrid elements
22
+ are convertible to SKLearn parameter grids.
23
+ """
24
+ match grid:
25
+ case HyperGrid():
26
+ return {dim.name: [v for v in dim] for dim in grid.dimensions}
27
+ case ProductGrid():
28
+ d1 = _grid_to_sklearn_recursive_helper(grid.grid1)
29
+ d2 = _grid_to_sklearn_recursive_helper(grid.grid2)
30
+ return d1 | d2
31
+ case _:
32
+ raise ValueError("Converting Grid to SKLearn ParameterGrid is not compatible with")
File without changes
@@ -0,0 +1,27 @@
1
+ import random
2
+ from typing import Any, Iterator, Protocol, TypeVar, runtime_checkable
3
+
4
+ from hypergrid.gen.iterable import HIterable
5
+
6
+ T = TypeVar("T", covariant=True)
7
+
8
+
9
+ @runtime_checkable
10
+ class Distribution(HIterable, Protocol[T]):
11
+ def sample(self) -> T: ...
12
+
13
+ def __iter__(self) -> Iterator[T]:
14
+ while True:
15
+ yield self.sample()
16
+
17
+ def __call__(self, *args: Any, **kwargs: Any) -> T:
18
+ return self.sample()
19
+
20
+
21
+ class Uniform(Distribution):
22
+ def __init__(self, low: float, high: float) -> None:
23
+ self.low = low
24
+ self.high = high
25
+
26
+ def sample(self) -> float:
27
+ return random.uniform(self.low, self.high)
@@ -0,0 +1,32 @@
1
+ from itertools import islice
2
+ from typing import Iterator, Protocol, Self, TypeVar, runtime_checkable
3
+
4
+ from hypergrid.dimension import Dimension
5
+
6
+ T = TypeVar("T")
7
+
8
+
9
+ @runtime_checkable
10
+ class HIterable(Protocol[T]):
11
+ name: str = "anonymous"
12
+
13
+ def __iter__(self) -> Iterator[T]: ...
14
+
15
+ def take(self, n: int) -> Dimension[T]:
16
+ return Dimension(**{self.name: [i for i in islice(self, n)]})
17
+
18
+ def with_name(self, name: str) -> Self:
19
+ self.name = name
20
+ return self
21
+
22
+
23
+ class ExponentialStep(HIterable):
24
+ def __init__(self, start: float, step: float) -> None:
25
+ self.start = start
26
+ self.step = step
27
+
28
+ def __iter__(self) -> Iterator[float]:
29
+ cursor = self.start
30
+ while True:
31
+ yield cursor
32
+ cursor *= self.step
@@ -0,0 +1,310 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ import random
5
+ from collections import namedtuple
6
+ from collections.abc import Collection
7
+ from functools import cached_property
8
+ from math import prod
9
+ from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Protocol, Type, runtime_checkable
10
+
11
+ from hypergrid.gen.iterable import HIterable
12
+ from hypergrid.util import instantiate_lambda
13
+
14
+ if TYPE_CHECKING:
15
+ from sklearn.model_selection import ParameterGrid
16
+
17
+ from hypergrid.dimension import Dimension, RawDimension
18
+
19
+
20
+ @runtime_checkable
21
+ class Grid(Protocol):
22
+ grid_element: Type[tuple]
23
+
24
+ @property
25
+ def dimension_names(self) -> list[str]:
26
+ return list(self.grid_element._fields) # type: ignore[attr-defined]
27
+
28
+ def __repr__(self) -> str: ...
29
+
30
+ def __str__(self) -> str:
31
+ return self.__repr__()
32
+
33
+ def __len__(self) -> int: ...
34
+
35
+ def __iter__(self) -> Iterator: ...
36
+
37
+ def take(self, n: int) -> list:
38
+ return [i for i in itertools.islice(self, n)]
39
+
40
+ def sample(self) -> tuple: ...
41
+
42
+ def __add__(self, other: Grid | Dimension | RawDimension) -> SumGrid:
43
+ match other:
44
+ case Grid():
45
+ return SumGrid(self, other)
46
+ case Dimension():
47
+ return SumGrid(self, HyperGrid(other))
48
+ case (str(s), coll) if isinstance(coll, Collection): # RawDimension
49
+ return SumGrid(self, HyperGrid(Dimension(**{s: coll})))
50
+ case _:
51
+ raise ValueError("Invalid argument for grid operation")
52
+
53
+ def __or__(self, other: Grid | Dimension | RawDimension) -> SumGrid:
54
+ return self.__add__(other)
55
+
56
+ def __mul__(self, other: Grid | Dimension | RawDimension) -> ProductGrid:
57
+ match other:
58
+ case Grid():
59
+ return ProductGrid(self, other)
60
+ case Dimension():
61
+ return ProductGrid(self, HyperGrid(other))
62
+ case (str(s), coll) if isinstance(coll, Collection): # RawDimension
63
+ return ProductGrid(self, HyperGrid(Dimension(**{s: coll})))
64
+ case _:
65
+ raise ValueError("Invalid argument for grid operation")
66
+
67
+ def __and__(self, other: Grid | Dimension | HIterable | RawDimension) -> ZipGrid:
68
+ match other:
69
+ case Grid():
70
+ return ZipGrid(self, other)
71
+ case Dimension():
72
+ return ZipGrid(self, HyperGrid(other))
73
+ case (str(s), coll) if isinstance(coll, Collection): # RawDimension
74
+ return ZipGrid(self, HyperGrid(Dimension(**{s: coll})))
75
+ case HIterable():
76
+ return ZipGrid(self, HyperGrid(other.take(len(self))))
77
+ case _:
78
+ raise ValueError("Invalid argument for grid operation")
79
+
80
+ def filter(self, predicate: Callable[[Any], bool]) -> FilterGrid:
81
+ return FilterGrid(self, predicate)
82
+
83
+ def select(self, *dim_names: str) -> SelectGrid:
84
+ return SelectGrid(self, *dim_names)
85
+
86
+ def map(self, **kwargs: Callable[[Any], Any]) -> MapGrid:
87
+ return MapGrid(self, **kwargs)
88
+
89
+ def map_to(self, **kwargs: Callable[[Any], Any]) -> MapToGrid:
90
+ return MapToGrid(self, **kwargs)
91
+
92
+ def instantiate(self, **kwargs: Type) -> MapToGrid:
93
+ return self.map_to(**{name: instantiate_lambda(cls) for name, cls in kwargs.items()})
94
+
95
+ def to_sklearn(self) -> ParameterGrid: # type: ignore[no-any-unimported]
96
+ from hypergrid.ext.sklearn import _grid_to_sklearn
97
+
98
+ return _grid_to_sklearn(self)
99
+
100
+
101
+ class HyperGrid(Grid):
102
+ dimensions: list[Dimension]
103
+
104
+ def __init__(self, *args: Dimension, **kwargs: Collection) -> None:
105
+ dims = list(args)
106
+ for dim, values in kwargs.items():
107
+ dims.append(Dimension(**{dim: values}))
108
+ assert len(dims) > 0, "Must provide at least one meaningful dimension"
109
+ assert len(dims) == len(set(dims)), "Dimension names must be unique"
110
+ self.dimensions = dims
111
+ self.grid_element = namedtuple("GridElement", [dim.name for dim in self.dimensions]) # type: ignore[misc]
112
+
113
+ def __repr__(self) -> str:
114
+ dim_str = ", ".join([repr(dim) for dim in self.dimensions])
115
+ return f"HyperGrid({dim_str})"
116
+
117
+ def __len__(self) -> int:
118
+ return prod([len(dim) for dim in self.dimensions])
119
+
120
+ def __iter__(self) -> Iterator:
121
+ for element_tuple in itertools.product(*[dim.__iter__() for dim in self.dimensions]):
122
+ yield self.grid_element(*element_tuple)
123
+
124
+ def sample(self) -> tuple:
125
+ return self.grid_element(*tuple([dim.sample() for dim in self.dimensions]))
126
+
127
+
128
+ class SumGrid(Grid):
129
+ def __init__(self, grid1: Grid, grid2: Grid) -> None:
130
+ assert set(grid1.dimension_names) == set(grid2.dimension_names)
131
+ self.grid1 = grid1
132
+ self.grid2 = grid2
133
+ self.grid_element = grid1.grid_element
134
+
135
+ def __repr__(self) -> str:
136
+ return f"SumGrid({repr(self.grid1)}, {repr(self.grid2)})"
137
+
138
+ def __len__(self) -> int:
139
+ return len(self.grid1) + len(self.grid2)
140
+
141
+ def __iter__(self) -> Iterator:
142
+ for grid_element in itertools.chain(self.grid1, self.grid2):
143
+ yield grid_element
144
+
145
+ def sample(self) -> tuple:
146
+ return random.choice([ge for ge in self])
147
+
148
+
149
+ class ProductGrid(Grid):
150
+ def __init__(self, grid1: Grid, grid2: Grid) -> None:
151
+ assert set(grid1.dimension_names).isdisjoint(set(grid2.dimension_names)), "Dimensions must be exactly matching"
152
+ self.grid1 = grid1
153
+ self.grid2 = grid2
154
+ self.grid_element = namedtuple("GridElement", grid1.dimension_names + grid2.dimension_names) # type: ignore[misc]
155
+
156
+ def __repr__(self) -> str:
157
+ return f"ProductGrid({repr(self.grid1)}, {repr(self.grid2)})"
158
+
159
+ def __len__(self) -> int:
160
+ return len(self.grid1) * len(self.grid2)
161
+
162
+ def __iter__(self) -> Iterator:
163
+ for grid_element1, grid_element2 in itertools.product(self.grid1, self.grid2):
164
+ yield self.grid_element(*(grid_element1 + grid_element2))
165
+
166
+ def sample(self) -> tuple:
167
+ ge1 = self.grid1.sample()
168
+ ge2 = self.grid2.sample()
169
+ return self.grid_element(*(ge1 + ge2))
170
+
171
+
172
+ class ZipGrid(Grid):
173
+ """
174
+ Mimic python "zip" of two iterables.
175
+ """
176
+
177
+ def __init__(self, grid1: Grid, grid2: Grid) -> None:
178
+ assert set(grid1.dimension_names).isdisjoint(set(grid2.dimension_names)), "Dimensions must be exactly matching"
179
+ self.grid1 = grid1
180
+ self.grid2 = grid2
181
+ self.grid_element = namedtuple("GridElement", grid1.dimension_names + grid2.dimension_names) # type: ignore[misc]
182
+
183
+ def __repr__(self) -> str:
184
+ return f"ZipGrid({repr(self.grid1)}, {repr(self.grid2)})"
185
+
186
+ def __len__(self) -> int:
187
+ return min(len(self.grid1), len(self.grid2))
188
+
189
+ def __iter__(self) -> Iterator:
190
+ for grid_element1, grid_element2 in zip(self.grid1, self.grid2):
191
+ yield self.grid_element(*(grid_element1 + grid_element2))
192
+
193
+ def sample(self) -> tuple:
194
+ return random.choice([ge for ge in self])
195
+
196
+
197
+ class FilterGrid(Grid):
198
+ _iter_cache: Optional[list] = None
199
+
200
+ def __init__(self, grid: Grid, predicate: Callable[[Any], bool]) -> None:
201
+ self.grid = grid
202
+ self.predicate = predicate
203
+ self.grid_element = grid.grid_element
204
+
205
+ def __repr__(self) -> str:
206
+ return f"FilterGrid({repr(self.grid)}, {self.predicate.__name__})"
207
+
208
+ def __len__(self) -> int:
209
+ return self._len
210
+
211
+ @cached_property
212
+ def _len(self) -> int:
213
+ return len([x for x in self])
214
+
215
+ def __iter__(self) -> Iterator:
216
+ for grid_element in self.grid:
217
+ if self.predicate(grid_element):
218
+ yield grid_element
219
+
220
+ def sample(self) -> tuple:
221
+ if self._iter_cache is not None:
222
+ sublist = self._iter_cache
223
+ else:
224
+ sublist = [ge for ge in self]
225
+ self._iter_cache = sublist
226
+ return random.choice(sublist)
227
+
228
+
229
+ class SelectGrid(Grid):
230
+ def __init__(self, grid: Grid, *select_dims: str) -> None:
231
+ assert len(set(select_dims)) == len(select_dims), "Selected columns must all be unique"
232
+ assert set(select_dims) <= set(grid.dimension_names), "Selected dimensions must be subset of grid dimensions"
233
+ self.grid = grid
234
+ self.select_dims = select_dims
235
+ self.grid_element = namedtuple("GridElement", [name for name in grid.dimension_names if name in self.select_dims]) # type: ignore[misc]
236
+
237
+ def __repr__(self) -> str:
238
+ return f"SelectGrid({repr(self.grid)}, {repr(self.select_dims)})"
239
+
240
+ def __len__(self) -> int:
241
+ return len(self.grid)
242
+
243
+ def __iter__(self) -> Iterator:
244
+ for grid_element in self.grid:
245
+ yield self.grid_element(*self._process_single(grid_element))
246
+
247
+ def sample(self) -> tuple:
248
+ return self.grid_element(*self._process_single(self.grid.sample()))
249
+
250
+ def _process_single(self, ge: tuple) -> list:
251
+ element_list = []
252
+ for dim_name in self.dimension_names:
253
+ try:
254
+ selected_value = getattr(ge, dim_name)
255
+ except AttributeError:
256
+ selected_value = None
257
+ element_list.append(selected_value)
258
+ return element_list
259
+
260
+
261
+ class MapGrid(Grid):
262
+ def __init__(self, grid: Grid, **kwargs: Callable[[Any], Any]) -> None:
263
+ assert len(set(kwargs.keys())) == len(kwargs.keys()), "New columns must all have unique names"
264
+ self.grid = grid
265
+ self.dimension_mapping = kwargs
266
+ self.grid_element = namedtuple("GridElement", list(kwargs.keys())) # type: ignore[misc]
267
+
268
+ def __repr__(self) -> str:
269
+ mappings_str = ", ".join([f"{dim_name}={func.__name__}" for dim_name, func in self.dimension_mapping.items()])
270
+ return f"MapGrid({repr(self.grid)}, {mappings_str})"
271
+
272
+ def __len__(self) -> int:
273
+ return len(self.grid)
274
+
275
+ def __iter__(self) -> Iterator:
276
+ for grid_element in self.grid:
277
+ yield self.grid_element(**self._process_single(grid_element))
278
+
279
+ def sample(self) -> tuple:
280
+ return self.grid_element(**self._process_single(self.grid.sample()))
281
+
282
+ def _process_single(self, ge: tuple) -> dict:
283
+ return {dim_name: func(ge) for dim_name, func in self.dimension_mapping.items()}
284
+
285
+
286
+ class MapToGrid(Grid):
287
+ def __init__(self, grid: Grid, **kwargs: Callable[[Any], Any]) -> None:
288
+ assert len(set(kwargs.keys())) == len(kwargs.keys()), "New columns must all have unique names"
289
+ assert set(grid.dimension_names).isdisjoint(set(kwargs.keys())), "New columns must not have name collisions with old columns"
290
+ self.grid = grid
291
+ self.dimension_mapping = kwargs
292
+ self.grid_element = namedtuple("GridElement", grid.dimension_names + list(kwargs.keys())) # type: ignore[misc]
293
+
294
+ def __repr__(self) -> str:
295
+ mappings_str = ", ".join([f"{dim_name}={func.__name__}" for dim_name, func in self.dimension_mapping.items()])
296
+ return f"MapToGrid({repr(self.grid)}, {mappings_str})"
297
+
298
+ def __len__(self) -> int:
299
+ return len(self.grid)
300
+
301
+ def __iter__(self) -> Iterator:
302
+ for grid_element in self.grid:
303
+ yield self.grid_element(**self._process_single(grid_element))
304
+
305
+ def sample(self) -> tuple:
306
+ return self.grid_element(**self._process_single(self.grid.sample()))
307
+
308
+ def _process_single(self, ge: tuple) -> dict:
309
+ new_values = {dim_name: func(ge) for dim_name, func in self.dimension_mapping.items()}
310
+ return ge._asdict() | new_values # type: ignore
File without changes
@@ -0,0 +1,5 @@
1
+ from typing import Callable, Type
2
+
3
+
4
+ def instantiate_lambda(cls: Type) -> Callable:
5
+ return lambda ge: cls(**ge._asdict())
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.1
2
+ Name: hypergrid
3
+ Version: 0.0.1
4
+ Summary: Hypergrid enables concise declaration of parameter grids for hyperparameter optimization and batch jobs.
5
+ Author-email: Justin Yan <justin@iomorphic.com>
6
+ Project-URL: Homepage, https://github.com/justin-yan/hypergrid
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: sklearn
20
+ Requires-Dist: scikit-learn<2; extra == "sklearn"
21
+
22
+ # hypergrid
23
+
24
+ Hypergrid enables concise declaration and manipulation of parameter grid spaces, with an aim towards use cases such as hyperparameter tuning or defining large batch jobs.
25
+
26
+ Use the following features to lazily declare a parameter grid:
27
+
28
+ - Dimension and Grid direct instantiation
29
+ - `+` and `|` for "sum" or "union" types (concatenation)
30
+ - `*` for "product" types
31
+ - `&` for coiteration (zip)
32
+ - `select` to project dimensions by name
33
+
34
+ There are also a few transformations that can be lazily applied element-wise, which take a GridElement (a namedtuple of dimension<->value) as input.
35
+
36
+ - `filter` to apply boolean predicate
37
+ - `map` for lambda transformation
38
+ - `map_to` for map + concat
39
+
40
+ Once a parameter grid is declared, there are two ways to "materialize" your grid, which return GridElements.
41
+
42
+ - `__iter__`: a grid is directly iterable
43
+ - `sample`: allows you to sample from the grid according to a sampling strategy
44
+
45
+ ## Usage Examples
46
+
47
+ ```python
48
+ from hypergrid.dsl import *
49
+ from dataclasses import dataclass
50
+
51
+ # First, we need to create a Dimension, which is essentially a named, finite, 1-d collection
52
+ d = Dimension(custom_name=[1, 2, 3]) # any python Collection will work - set, dict, range(), etc.
53
+ assert d.name == "custom_name" # the argument name is used as the dimension's name.
54
+ d.with_name("ints") # which you can reset
55
+ Uniform(low=1, high=5).take(5)
56
+ ExponentialStep(start=1, step=1.1).take(6) # You can also take a dimension from a Distribution or HIterable
57
+
58
+ # You can `len(d)` or `[i for i in d]`, but grids are more interesting
59
+ g = d.to_grid()
60
+ i2d = Dimension(ints=[4, 5, 6])
61
+ cd = Dimension(chars=["a", "b", "c", "d"])
62
+ union_ints = g + i2d # result is length 6: Concatenate two grids that have the same underlying dimensions
63
+ product_g = g * cd # result is length 12: tuples (1, "a") - take the cartesian product of the underlying grids
64
+ zip_g = g & cd # result is length 3: tuples (1, "a") - zip two grids together, up to the shorter grid
65
+
66
+ ml = [i for i in zip_g] # You can iterate through a grid
67
+ tl = zip_g.take(5) # or you can just take up to a certain number of grid elements from it
68
+ print(tl[0].ints), print(tl[0].chars) # The iterator elements are python NamedTuples taken from the dimension names.
69
+
70
+ # These gridelements can be referenced and used in the grid higher-order functions
71
+ zip_g.filter(lambda ge: ge.chars in ["a", "b"]) # result is length 2: keep the tuples (1, "a") and (2, "b")
72
+ zip_g.map(doubled=lambda ge: ge.ints * 2) # result is length 3, with single attribute (drops `ints` and `chars`)
73
+ mt = zip_g.map_to(doubled=lambda ge: ge.ints * 2) # result is length 3, appends `doubled` and keeps `ints` and `chars`
74
+ print(mt.select("doubled", "ints").take(1)[0]) # resulting gridelement no longer has `chars`
75
+
76
+ # There are some other utility methods on a grid:
77
+ zip_g.sample() # Randomly samples a single grid element from a grid
78
+ zip_g.to_sklearn() # The Grid.to_* methods convert HyperGrids to other grid formats
79
+
80
+ # The general idea is to allow for fairly extensive grid construction routines
81
+ @dataclass
82
+ class FakeModel:
83
+ idx: int
84
+ param1: float
85
+
86
+ g = HyperGrid( # A grid with 4 x 10 combinations
87
+ ExponentialStep(start=1.0, step=1.5).take(4).with_name("param1"),
88
+ idx=range(10)
89
+ ).instantiate(model=FakeModel).select("model") + \
90
+ HyperGrid( # A different grid with 15 combinations
91
+ Uniform(low=-1, high=1).take(5).with_name("param1"),
92
+ idx=[10, 20, 30]
93
+ ).instantiate(model=FakeModel).select("model")
94
+ assert len(g) == 55
95
+ g.sample()
96
+ ```
97
+
@@ -0,0 +1,20 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/hypergrid/__init__.py
5
+ src/hypergrid/dimension.py
6
+ src/hypergrid/dsl.py
7
+ src/hypergrid/grid.py
8
+ src/hypergrid/py.typed
9
+ src/hypergrid/util.py
10
+ src/hypergrid.egg-info/PKG-INFO
11
+ src/hypergrid.egg-info/SOURCES.txt
12
+ src/hypergrid.egg-info/dependency_links.txt
13
+ src/hypergrid.egg-info/not-zip-safe
14
+ src/hypergrid.egg-info/requires.txt
15
+ src/hypergrid.egg-info/top_level.txt
16
+ src/hypergrid/ext/__init__.py
17
+ src/hypergrid/ext/sklearn.py
18
+ src/hypergrid/gen/__init__.py
19
+ src/hypergrid/gen/distribution.py
20
+ src/hypergrid/gen/iterable.py
@@ -0,0 +1,3 @@
1
+
2
+ [sklearn]
3
+ scikit-learn<2
@@ -0,0 +1 @@
1
+ hypergrid