Coverage for moptipy / operators / tools.py: 50%
12 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-24 08:49 +0000
1"""Some tools for optimization."""
3from math import exp, log
5import numba # type: ignore
8@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
9def exponential_step_size(step_size: float,
10 min_steps: int, max_steps: int) -> int:
11 """
12 Translate a step size in `[0,1]` to an integer in `min_steps..max_steps`.
14 The idea is that we would like to have step-size dependent operators of
15 type :class:`~moptipy.api.operators.Op1WithStepSize`. These operators
16 allow algorithms to tune the amount of change to be applied to a solution
17 between `0` and `1` (inclusively). As described in the documentation of
18 :meth:`~moptipy.api.operators.Op1WithStepSize.op1`, `0` means that the
19 smallest possible change should be applied and `1` means that the largest
20 possible change should be applied.
22 Now for many search spaces, we need to translate this step size from
23 `[0,1]` to an integer. For instance, if we have `n`-dimensional
24 :mod:`~moptipy.spaces.bitstrings` as search space, then we can flip
25 anything between `1` and `n` bits. Straightforwardly, one would linearly
26 scale the step size from `[0,1]` to `1..n`. Unfortunately, if we do that,
27 then different values of `step_size` will have very different meaning
28 depending on `n`. For example, `step_size=0.05` would translate to
29 `round(1 + step_size * (n-1)) = 1` bits to be flipped if `n=10`, to
30 `6` bit flips for `n=100`, and to `501` bit flips for `n=10_000`. While
31 flipping one bit is a very small move and flipping six bits may be a
32 medium-size move in discrete optimization, flipping over 500 bits is
33 actually always quite a lot.
35 What we would like is to have scale-independent small moves but still be
36 able to make large moves. We can get this by exponentially transforming
37 the `step_width`. Most `step_size` values will result in small integer
38 steps and only `step_size` values close to `1` will yield really big
39 results.
41 And for a `step_size=0.05`, we get for different `n`:
43 >>> exponential_step_size(0.05, 1, 10)
44 1
45 >>> exponential_step_size(0.05, 1, 100)
46 1
47 >>> exponential_step_size(0.05, 1, 10_000)
48 2
49 >>> exponential_step_size(0.05, 1, 1_000_000)
50 2
51 >>> exponential_step_size(0.05, 1, 1_000_000_000)
52 3
53 >>> exponential_step_size(0.05, 1, 1_000_000_000_000)
54 4
56 For different values of `step_size` and a fixed `n`, we can still obtain
57 the whole spectrum of possible changes. For `n=10`, for example, we get:
59 >>> exponential_step_size(0.0, 1, 10)
60 1
61 >>> exponential_step_size(0.1, 1, 10)
62 1
63 >>> exponential_step_size(0.2, 1, 10)
64 2
65 >>> exponential_step_size(0.3, 1, 10)
66 2
67 >>> exponential_step_size(0.4, 1, 10)
68 3
69 >>> exponential_step_size(0.5, 1, 10)
70 3
71 >>> exponential_step_size(0.6, 1, 10)
72 4
73 >>> exponential_step_size(0.7, 1, 10)
74 5
75 >>> exponential_step_size(0.8, 1, 10)
76 6
77 >>> exponential_step_size(0.85, 1, 10)
78 7
79 >>> exponential_step_size(0.9, 1, 10)
80 8
81 >>> exponential_step_size(0.95, 1, 10)
82 9
83 >>> exponential_step_size(1.0, 1, 10)
84 10
86 So we can still reach the whole range of possible steps from `1` to `n`.
88 >>> isinstance(exponential_step_size(0.5, 1, 9), int)
89 True
90 >>> exponential_step_size(0.0, 1, 100)
91 1
92 >>> exponential_step_size(1.0, 1, 100)
93 100
94 >>> exponential_step_size(1.0 / 3.0, 1, 10)
95 2
96 >>> exponential_step_size(1.0 / 3.0, 1, 100)
97 5
98 >>> exponential_step_size(1.0 / 3.0, 1, 10_000)
99 22
100 >>> exponential_step_size(0.0, 2, 10)
101 2
102 >>> exponential_step_size(0.0, 9, 10)
103 9
104 >>> exponential_step_size(0.0, 10, 10)
105 10
106 >>> exponential_step_size(1.0, 10, 10)
107 10
109 :param step_size: the step size from `[0,1]` to be transformed to an
110 integer
111 :param min_steps: the minimum (inclusive) value for the returned integer
112 :param max_steps: the maximum (inclusive) value for the returned integer
113 """
114 return round(min_steps + exp(step_size * log(
115 max_steps - min_steps + 1))) - 1
118@numba.njit(cache=True, inline="always", fastmath=True, boundscheck=False)
119def inv_exponential_step_size(int_val: int,
120 min_steps: int, max_steps: int) -> float:
121 """
122 Compute the inverse of :func:`exponential_step_size`.
124 This routine exists mainly to make testing easier.
126 :param int_val: the return value of :func:`exponential_step_size`.
127 :param min_steps: the minimum (inclusive) value any `int_val`
128 :param max_steps: the maximum (inclusive) value any `int_val`
130 >>> exponential_step_size(0.47712125471966244, 1, 10)
131 3
132 >>> inv_exponential_step_size(3, 1, 10)
133 0.47712125471966244
134 >>> inv_exponential_step_size(1, 1, 10)
135 0.0
136 >>> inv_exponential_step_size(10, 1, 10)
137 1.0
138 >>> inv_exponential_step_size(33, 6, 673)
139 0.5123088678224029
140 >>> exponential_step_size(0.5123088678224029, 6, 673)
141 33
142 >>> inv_exponential_step_size(3, 3, 3)
143 1.0
144 >>> inv_exponential_step_size(3, 3, 10)
145 0.0
146 >>> inv_exponential_step_size(10, 3, 10)
147 1.0
148 """
149 if int_val >= max_steps:
150 return 1.0
151 if int_val <= min_steps:
152 return 0.0
153 return float(log(int_val - min_steps + 1) / log(
154 max_steps - min_steps + 1))