Skip to content

Commit 7455bc2

Browse files
committed
Added geomspace_int function
1 parent bae1388 commit 7455bc2

File tree

4 files changed

+139
-3
lines changed

4 files changed

+139
-3
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ axes.
1515
## Mathematical functions:
1616
- `xlogx` calculates $x \log(x)$ with the correct limit for $x=0$
1717
- `random_uniform_fixed_sum` samples uniformly distributed, positive numbers adding to 1
18+
- `geomspace_int` provides an (approximately) geometric sequence of integers

tests/test_mathematics.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pytest
99
from scipy import stats
1010

11-
from utilitiez import random_uniform_fixed_sum, xlogx
11+
from utilitiez import geomspace_int, random_uniform_fixed_sum, xlogx
1212

1313

1414
@pytest.mark.parametrize("jit", [True, False])
@@ -117,3 +117,38 @@ def f(dim, size):
117117
assert stats.ks_1samp(xs[:, 2], cdf).statistic < 0.1
118118
else:
119119
raise NotImplementedError("Check not implemented for dim>3")
120+
121+
122+
def test_geomspace_int():
123+
"""Test the `geomspace_int` function."""
124+
for num in [3, 20]:
125+
for a, b in [[0, 5], [1, 100]]:
126+
x = geomspace_int(a, b, num)
127+
assert np.issubdtype(x.dtype, np.integer)
128+
assert x[0] == a
129+
assert x[-1] == b
130+
assert len(x) <= num
131+
132+
x = geomspace_int(10, 1000, 32)
133+
y = np.geomspace(10, 1000, 32)
134+
np.testing.assert_allclose(x - y, 0, atol=1)
135+
136+
assert np.issubdtype(geomspace_int(0, 1, 0).dtype, np.integer)
137+
assert np.issubdtype(geomspace_int(0, 0, 10).dtype, np.integer)
138+
np.testing.assert_equal(geomspace_int(0, 1, 0), np.array([]))
139+
np.testing.assert_equal(geomspace_int(0, 0, 10), np.array([0]))
140+
np.testing.assert_equal(geomspace_int(0, 10, 1), np.array([0]))
141+
np.testing.assert_equal(geomspace_int(0, 2, 10), np.array([0, 1, 2]))
142+
np.testing.assert_equal(geomspace_int(0, 20, 2), np.array([0, 20]))
143+
np.testing.assert_equal(geomspace_int(0, 20, 3), np.array([0, 1, 20]))
144+
145+
x = geomspace_int(10, 100, 20)
146+
y = geomspace_int(100, 10, 20)
147+
np.testing.assert_equal(x, y[::-1])
148+
149+
with pytest.raises(ValueError):
150+
geomspace_int(0, 1, -1)
151+
with pytest.raises(ValueError):
152+
geomspace_int(-1, 2)
153+
with pytest.raises(ValueError):
154+
geomspace_int(1, -2)

utilitiez/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
"""
55

66
from .densityplot import densityplot
7-
from .mathematics import random_uniform_fixed_sum, xlogx
7+
from .mathematics import *

utilitiez/mathematics.py

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import numba as nb
1818
import numpy as np
1919
from numba.extending import overload, register_jitable
20-
from numpy.typing import ArrayLike
20+
from numpy.typing import ArrayLike, NDArray
2121

2222

2323
def xlogx_scalar(x):
@@ -182,3 +182,103 @@ def impl(dim, size=None):
182182
raise nb.TypingError("`size` must be positive integer or None")
183183

184184
return impl
185+
186+
187+
def geomspace_int(
188+
start: int, end: int, num: int = 50, *, max_steps: int = 100
189+
) -> NDArray[np.integer]:
190+
"""Return integers spaced (approximately) evenly on a log scale.
191+
192+
Parameters:
193+
start (int):
194+
The starting value of the sequence.
195+
final (int):
196+
The final value of the sequence.
197+
num (int, optional)
198+
Number of samples to generate. Default is 50.
199+
max_steps (int, optional)
200+
The maximal number of steps of the iterative algorithm. If the algorithm
201+
could not find a solution, a `RuntimeError` is raised.
202+
203+
Returns:
204+
an ordered sequence of at most `num` integers from `start` to `end` with
205+
approximately logarithmic spacing.
206+
"""
207+
# check whether the supplied number is valid
208+
num = int(num)
209+
if num < 0:
210+
raise ValueError(f"Number of samples, {num}, must be non-negative.")
211+
if num == 0:
212+
return np.array([], dtype=int)
213+
214+
# check corner cases
215+
start = int(start)
216+
end = int(end)
217+
if start < 0 or end < 0:
218+
raise ValueError("`start` and `end` must be positive numbers")
219+
if num == 1 or start == end:
220+
return np.array([start])
221+
222+
if start > end:
223+
# inverted sequence
224+
return geomspace_int(end, start, num)[::-1]
225+
226+
if num == 2:
227+
# return end intervals, which could be inverted by above line
228+
return np.array([start, end])
229+
230+
if num > end - start:
231+
# all integers need to be returned
232+
return np.arange(start, end + 1)
233+
234+
# calculate the maximal size of underlying logarithmic range
235+
if start == 0:
236+
start = 1
237+
num -= 1
238+
add_zero = True
239+
else:
240+
add_zero = False
241+
242+
num_max = int(
243+
np.ceil((math.log(end) - math.log(start)) / (math.log(end) - math.log(end - 1)))
244+
)
245+
a, b = num, num_max # interval of log-range
246+
n = a
247+
248+
# try different log-ranges
249+
for _ in range(max_steps):
250+
# determine discretized logarithmic range
251+
ys = np.geomspace(start, end, num=n)
252+
ys = np.unique(ys.astype(int))
253+
ys_len = len(ys)
254+
255+
if ys_len == num:
256+
break # reached correct number
257+
258+
if ys_len < num:
259+
# n is too small
260+
a = n
261+
n = int(math.sqrt(n * b))
262+
if a == n:
263+
n += 1
264+
if n == b:
265+
break
266+
267+
elif ys_len > num:
268+
# n is too large
269+
b = n
270+
n = int(math.sqrt(a * n))
271+
if b == n:
272+
n -= 1
273+
if n == a:
274+
break
275+
else:
276+
raise RuntimeError("Exceeded attempts")
277+
278+
if add_zero:
279+
return np.r_[0, ys]
280+
else:
281+
return ys
282+
283+
284+
__all__ = ["geomspace_int", "random_uniform_fixed_sum", "xlogx"]

0 commit comments

Comments
 (0)