Coverage for adrs/search/random.py: 79%
58 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-29 05:07 +0800
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-29 05:07 +0800
1import math
2import numpy as np
3from typing import Sequence, override, cast
5from adrs.search import Search, ParameterGrid, Permutation
7from enum import StrEnum
10class Distribution(StrEnum):
11 UNIFORM = "uniform"
12 NORMAL = "normal"
13 LOGNORMAL = "lognormal"
14 EXPONENTIAL = "exponential"
15 BETA = "beta"
16 GAMMA = "gamma"
19class RandomSearch(Search):
20 def __init__(
21 self,
22 samples: int | float | np.float64,
23 seed: int,
24 dist: Distribution,
25 **dist_kwargs,
26 ):
27 self.samples = samples
28 self.rng = np.random.default_rng(seed)
29 self.dist = dist
30 self.dist_kwargs = dist_kwargs
32 @staticmethod
33 def id() -> str:
34 return "randomsearch"
36 def _dist(self, n: int) -> np.ndarray:
37 match self.dist:
38 case Distribution.UNIFORM:
39 return self.rng.uniform(0, 1, n)
40 case Distribution.NORMAL:
41 return self.rng.normal(0, 1, n)
42 case Distribution.LOGNORMAL:
43 return self.rng.lognormal(0, 1, n)
44 case Distribution.EXPONENTIAL:
45 return self.rng.exponential(1, n)
46 case Distribution.BETA:
47 return self.rng.beta(
48 self.dist_kwargs.get("a", 2), self.dist_kwargs.get("b", 5), n
49 )
50 case Distribution.GAMMA:
51 return self.rng.gamma(
52 self.dist_kwargs.get("shape", 2),
53 self.dist_kwargs.get("scale", 1),
54 n,
55 )
56 case dist:
57 raise ValueError(f"Invalid distribution: {dist}")
59 @override
60 def search(self, grid: ParameterGrid) -> Sequence[Permutation]:
61 if len(grid) == 0:
62 return []
64 # determine number of samples
65 total_perms = math.prod(map(lambda x: len(x), grid.values()))
66 n = (
67 round(total_perms * self.samples)
68 if isinstance(self.samples, float | np.float64)
69 else cast(int, self.samples)
70 )
72 permutations = []
74 for _ in range(n):
75 perm: Permutation = {}
76 for k, v in grid.items():
77 # sample from distribution and avoid negatives
78 weights = np.abs(self._dist(len(v))) # type: ignore
79 probs = weights / weights.sum() # normalize to (0, 1)
80 perm[k] = self.rng.choice(a=v, p=probs)
81 permutations.append(perm)
83 return permutations
85 @override
86 def filter[T](self, permutations: Sequence[T]) -> Sequence[T]:
87 # determine number of samples
88 n = (
89 round(len(permutations) * self.samples)
90 if isinstance(self.samples, float | np.float64)
91 else cast(int, self.samples)
92 )
94 weights = np.abs(self._dist(len(permutations)))
95 probs = weights / weights.sum()
96 return self.rng.choice(a=permutations, p=probs, size=n, replace=False) # type: ignore