Coverage for moptipy / utils / nputils.py: 89%

141 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""Utilities for interaction with numpy.""" 

2from hashlib import sha512 

3from math import isfinite, isnan 

4from operator import itemgetter 

5from typing import Any, Final, cast 

6 

7import numba # type: ignore 

8import numpy as np 

9from numpy.random import PCG64, Generator, default_rng 

10from pycommons.io.csv import CSV_SEPARATOR 

11from pycommons.strings.string_conv import bool_to_str, num_to_str 

12from pycommons.types import check_int_range, type_error 

13 

14#: All the numpy integer types and their ranges in increasing order of size. 

15#: The tuple contains alternating signed and unsigned types. It starts with 

16#: the smallest signed type, `numpy.int8` and ends with the largest unsigned 

17#: type `numpy.uint64`. 

18#: If we have a range `[min..max]` of valid value, then we can look up this 

19#: range and find the integer type with the smallest memory footprint to 

20#: accommodate it. This is what :func:`int_range_to_dtype` does. 

21__INTS_AND_RANGES: Final[tuple[tuple[np.dtype, int, int], ...]] = \ 

22 tuple(sorted([ 

23 (dtx, int(np.iinfo(dtx).min), int(np.iinfo(dtx).max)) 

24 for dtx in cast("set[np.dtype]", {np.dtype(bdt) for bdt in [ 

25 int, np.int8, np.int16, np.uint8, np.uint16, 

26 np.int32, np.uint32, np.int64, np.uint64]})], 

27 key=itemgetter(2, 1))) 

28 

29#: The numpy integer data types. 

30INTS: Final[tuple[np.dtype, ...]] = tuple(a[0] for a in __INTS_AND_RANGES) 

31 

32#: A map associating all numpy integer types associated to tuples 

33#: of their respective minimum and maximum value. 

34__NP_INTS_MAP: Final[dict[np.dtype, tuple[int, int]]] = \ 

35 {a[0]: (a[1], a[2]) for a in __INTS_AND_RANGES} 

36 

37#: The default integer type: the signed 64-bit integer. 

38DEFAULT_INT: Final[np.dtype] = INTS[-2] 

39 

40#: The default unsigned integer type: an unsigned 64-bit integer. 

41DEFAULT_UNSIGNED_INT: Final[np.dtype] = INTS[-1] 

42 

43#: The default boolean type. 

44DEFAULT_BOOL: Final[np.dtype] = np.dtype(np.bool_) 

45 

46#: The default floating point type. 

47DEFAULT_FLOAT: Final[np.dtype] = np.dtype(float) 

48 

49#: The default numerical types. 

50DEFAULT_NUMERICAL: Final[tuple[np.dtype, ...]] = (*list(INTS), DEFAULT_FLOAT) 

51 

52 

53def is_np_int(dtype: np.dtype) -> bool: 

54 """ 

55 Check whether a :class:`numpy.dtype` is an integer type. 

56 

57 :param dtype: the type 

58 

59 >>> import numpy as npx 

60 >>> from moptipy.utils.nputils import is_np_int 

61 >>> print(is_np_int(npx.dtype(npx.int8))) 

62 True 

63 >>> print(is_np_int(npx.dtype(npx.uint16))) 

64 True 

65 >>> print(is_np_int(npx.dtype(npx.float64))) 

66 False 

67 """ 

68 return dtype.kind in {"i", "u"} 

69 

70 

71def is_np_float(dtype: np.dtype) -> bool: 

72 """ 

73 Check whether a :class:`numpy.dtype` is a floating point type. 

74 

75 :param dtype: the type 

76 

77 >>> import numpy as npx 

78 >>> from moptipy.utils.nputils import is_np_float 

79 >>> print(is_np_float(npx.dtype(npx.int8))) 

80 False 

81 >>> print(is_np_float(npx.dtype(npx.uint16))) 

82 False 

83 >>> print(is_np_float(npx.dtype(npx.float64))) 

84 True 

85 """ 

86 return dtype.kind == "f" 

87 

88 

89def int_range_to_dtype(min_value: int, max_value: int, 

90 force_signed: bool = False, 

91 force_unsigned: bool = False) -> np.dtype: 

92 """ 

93 Convert an integer range to an appropriate numpy data type. 

94 

95 The returned type is as compact as possible and signed types are 

96 preferred over unsigned types. 

97 The returned :class:`numpy.dtype` will allow accommodating all values in 

98 the inclusive interval `min_value..max_value`. 

99 

100 :param min_value: the minimum value 

101 :param max_value: the maximum value 

102 :param force_signed: enforce signed types 

103 :param force_unsigned: enforce unsigned types 

104 :return: the numpy integer range 

105 :raises TypeError: if the parameters are not integers 

106 :raises ValueError: if the range is invalid, i.e., if `min_value` exceeds 

107 `max_value` or either of them exceeds the possible range of the 

108 largest numpy integers. 

109 

110 >>> from moptipy.utils.nputils import int_range_to_dtype 

111 >>> print(int_range_to_dtype(0, 127)) 

112 int8 

113 >>> print(int_range_to_dtype(0, 128)) 

114 uint8 

115 >>> print(int_range_to_dtype(0, 128, True)) 

116 int16 

117 >>> print(int_range_to_dtype(0, 32767)) 

118 int16 

119 >>> print(int_range_to_dtype(0, 32768)) 

120 uint16 

121 >>> print(int_range_to_dtype(0, 32768, True)) 

122 int32 

123 >>> print(int_range_to_dtype(0, (2 ** 31) - 1)) 

124 int32 

125 >>> print(int_range_to_dtype(0, 2 ** 31)) 

126 uint32 

127 >>> print(int_range_to_dtype(0, 2 ** 31, True)) 

128 int64 

129 >>> print(int_range_to_dtype(0, (2 ** 63) - 1)) 

130 int64 

131 >>> print(int_range_to_dtype(0, 2 ** 63)) 

132 uint64 

133 >>> print(int_range_to_dtype(0, (2 ** 64) - 1)) 

134 uint64 

135 >>> print(int_range_to_dtype(0, (2 ** 7) - 1)) 

136 int8 

137 >>> print(int_range_to_dtype(0, (2 ** 7) - 1, force_unsigned=True)) 

138 uint8 

139 >>> print(int_range_to_dtype(0, 32767, force_unsigned=True)) 

140 uint16 

141 >>> try: 

142 ... int_range_to_dtype(0, (2 ** 64) - 1, True) 

143 ... except ValueError as e: 

144 ... print(e) 

145 Signed integer range cannot exceed -9223372036854775808..922337203685477\ 

1465807, but 0..18446744073709551615 was specified. 

147 >>> try: 

148 ... int_range_to_dtype(0, (2 ** 64) + 1) 

149 ... except ValueError as e: 

150 ... print(e) 

151 max_value for unsigned integers must be <=18446744073709551615, but is \ 

15218446744073709551617 for min_value=0. 

153 >>> try: 

154 ... int_range_to_dtype(-1, (2 ** 64) - 1) 

155 ... except ValueError as e: 

156 ... print(e) 

157 Signed integer range cannot exceed -9223372036854775808..922337203685477\ 

1585807, but -1..18446744073709551615 was specified. 

159 >>> try: 

160 ... int_range_to_dtype(-1.0, (2 ** 64) - 1) 

161 ... except TypeError as e: 

162 ... print(e) 

163 min_value should be an instance of int but is float, namely -1.0. 

164 >>> try: 

165 ... int_range_to_dtype(-1, 'a') 

166 ... except TypeError as e: 

167 ... print(e) 

168 max_value should be an instance of int but is str, namely 'a'. 

169 >>> try: 

170 ... int_range_to_dtype(0, 1, 1) 

171 ... except TypeError as te: 

172 ... print(te) 

173 force_signed should be an instance of bool but is int, namely 1. 

174 >>> try: 

175 ... int_range_to_dtype(0, 1, force_unsigned=3) 

176 ... except TypeError as te: 

177 ... print(te) 

178 force_unsigned should be an instance of bool but is int, namely 3. 

179 >>> try: 

180 ... int_range_to_dtype(0, 1, True, True) 

181 ... except ValueError as ve: 

182 ... print(ve) 

183 force_signed and force_unsigned cannot both be True. 

184 >>> try: 

185 ... int_range_to_dtype(-1, 1, force_unsigned=True) 

186 ... except ValueError as ve: 

187 ... print(ve) 

188 min_value=-1 and force_unsigned=True is not permitted. 

189 """ 

190 if not isinstance(min_value, int): 

191 raise type_error(min_value, "min_value", int) 

192 if not isinstance(max_value, int): 

193 raise type_error(max_value, "max_value", int) 

194 if not isinstance(force_signed, bool): 

195 raise type_error(force_signed, "force_signed", bool) 

196 if not isinstance(force_unsigned, bool): 

197 raise type_error(force_unsigned, "force_unsigned", bool) 

198 if force_unsigned: 

199 if force_signed: 

200 raise ValueError( 

201 "force_signed and force_unsigned cannot both be True.") 

202 if min_value < 0: 

203 raise ValueError(f"min_value={min_value} and force_unsigned=" 

204 "True is not permitted.") 

205 if min_value > max_value: 

206 raise ValueError( 

207 f"min_value must be <= max_value, but min_value={min_value} " 

208 f"and max_value={max_value} was provided.") 

209 

210 use_min_value: Final[int] = -1 if force_signed and (min_value >= 0) \ 

211 else min_value 

212 for t in __INTS_AND_RANGES: 

213 if (use_min_value >= t[1]) and (max_value <= t[2]): 

214 if force_unsigned and (t[1] < 0): 

215 continue 

216 return t[0] 

217 

218 if (min_value >= 0) and (not force_signed): 

219 raise ValueError( 

220 "max_value for unsigned integers must be <=" 

221 f"{(__INTS_AND_RANGES[-1])[2]}, but is {max_value}" 

222 f" for min_value={min_value}.") 

223 

224 raise ValueError( 

225 f"Signed integer range cannot exceed {__INTS_AND_RANGES[-2][1]}.." 

226 f"{__INTS_AND_RANGES[-2][2]}, but {min_value}..{max_value} " 

227 "was specified.") 

228 

229 

230def dtype_for_data(always_int: bool, 

231 lower_bound: int | float, 

232 upper_bound: int | float) -> np.dtype: 

233 """ 

234 Obtain the most suitable numpy data type to represent the data. 

235 

236 If the data is always integer, the smallest possible integer type will be 

237 sought using :func:`int_range_to_dtype`. If `always_int` is `True` and 

238 one or both of the bounds are infinite, then the largest available integer 

239 type is returned. If the bounds are finite but exceed the integer range, 

240 a `ValueError` is thrown. If the data is not always integer, the `float64` 

241 is returned. 

242 

243 :param always_int: is the data always integer? 

244 :param lower_bound: the lower bound of the data, set to `-inf` if no lower 

245 bound is known and we should assume the full integer range 

246 :param upper_bound: the upper bound of the data, set to `-inf` if no upper 

247 bound is known and we should assume the full integer range 

248 :raises ValueError: if the `lower_bound > upper_bound` or any bound is 

249 `nan` or the integer bounds exceed the largest int range 

250 :raises TypeError: if, well, you provide parameters of the wrong types 

251 

252 >>> print(dtype_for_data(True, 0, 127)) 

253 int8 

254 >>> print(dtype_for_data(True, 0, 128)) 

255 uint8 

256 >>> print(dtype_for_data(True, -1, 32767)) 

257 int16 

258 >>> print(dtype_for_data(True, 0, 32768)) 

259 uint16 

260 >>> print(dtype_for_data(True, -1, 32768)) 

261 int32 

262 >>> print(dtype_for_data(True, 0, 65535)) 

263 uint16 

264 >>> print(dtype_for_data(True, 0, 65536)) 

265 int32 

266 >>> print(dtype_for_data(True, -1, 65535)) 

267 int32 

268 >>> print(dtype_for_data(True, 0, (2 ** 31) - 1)) 

269 int32 

270 >>> print(dtype_for_data(True, 0, 2 ** 31)) 

271 uint32 

272 >>> print(dtype_for_data(True, -1, 2 ** 31)) 

273 int64 

274 >>> print(dtype_for_data(True, 0, (2 ** 63) - 1)) 

275 int64 

276 >>> print(dtype_for_data(True, 0, 2 ** 63)) 

277 uint64 

278 >>> print(dtype_for_data(True, 0, (2 ** 63) + 1)) 

279 uint64 

280 >>> print(dtype_for_data(True, 0, (2 ** 64) - 1)) 

281 uint64 

282 >>> try: 

283 ... dtype_for_data(True, 0, 2 ** 64) 

284 ... except ValueError as v: 

285 ... print(v) 

286 max_value for unsigned integers must be <=18446744073709551615, but \ 

287is 18446744073709551616 for min_value=0. 

288 >>> from math import inf, nan 

289 >>> print(dtype_for_data(True, 0, inf)) 

290 uint64 

291 >>> print(dtype_for_data(True, -1, inf)) 

292 int64 

293 >>> print(dtype_for_data(True, -inf, inf)) 

294 int64 

295 >>> print(dtype_for_data(True, -inf, inf)) 

296 int64 

297 >>> try: 

298 ... dtype_for_data(True, 1, 0) 

299 ... except ValueError as v: 

300 ... print(v) 

301 invalid bounds [1,0]. 

302 >>> try: 

303 ... dtype_for_data(False, 1, 0) 

304 ... except ValueError as v: 

305 ... print(v) 

306 invalid bounds [1,0]. 

307 >>> try: 

308 ... dtype_for_data(True, 1, nan) 

309 ... except ValueError as v: 

310 ... print(v) 

311 invalid bounds [1,nan]. 

312 >>> try: 

313 ... dtype_for_data(False, nan, 0) 

314 ... except ValueError as v: 

315 ... print(v) 

316 invalid bounds [nan,0]. 

317 >>> print(dtype_for_data(False, 1, 2)) 

318 float64 

319 >>> try: 

320 ... dtype_for_data(False, nan, '0') 

321 ... except TypeError as v: 

322 ... print(v) 

323 upper_bound should be an instance of any in {float, int} but is str, \ 

324namely '0'. 

325 >>> try: 

326 ... dtype_for_data(True, 'x', 0) 

327 ... except TypeError as v: 

328 ... print(v) 

329 lower_bound should be an instance of any in {float, int} but is str, \ 

330namely 'x'. 

331 >>> try: 

332 ... dtype_for_data(True, 1.0, 2.0) 

333 ... except TypeError as v: 

334 ... print(v) 

335 finite lower_bound of always_int should be an instance of int but is \ 

336float, namely 1.0. 

337 >>> try: 

338 ... dtype_for_data(True, 0, 2.0) 

339 ... except TypeError as v: 

340 ... print(v) 

341 finite upper_bound of always_int should be an instance of int but is \ 

342float, namely 2.0. 

343 >>> try: 

344 ... dtype_for_data(3, 0, 2) 

345 ... except TypeError as v: 

346 ... print(v) 

347 always_int should be an instance of bool but is int, namely 3. 

348 """ 

349 if not isinstance(always_int, bool): 

350 raise type_error(always_int, "always_int", bool) 

351 if not isinstance(lower_bound, int | float): 

352 raise type_error(lower_bound, "lower_bound", (int, float)) 

353 if not isinstance(upper_bound, int | float): 

354 raise type_error(upper_bound, "upper_bound", (int, float)) 

355 if isnan(lower_bound) or isnan(upper_bound) or \ 

356 (lower_bound > upper_bound): 

357 raise ValueError(f"invalid bounds [{lower_bound},{upper_bound}].") 

358 if always_int: 

359 if isfinite(lower_bound): 

360 if not isinstance(lower_bound, int): 

361 raise type_error( 

362 lower_bound, "finite lower_bound of always_int", int) 

363 if isfinite(upper_bound): 

364 if not isinstance(upper_bound, int): 

365 raise type_error( 

366 upper_bound, "finite upper_bound of always_int", int) 

367 return int_range_to_dtype(int(lower_bound), int(upper_bound)) 

368 if lower_bound >= 0: 

369 return DEFAULT_UNSIGNED_INT 

370 return DEFAULT_INT 

371 return DEFAULT_FLOAT 

372 

373 

374@numba.jit(forceobj=True) 

375def np_ints_max(shape, dtype: np.dtype = DEFAULT_INT) -> np.ndarray: 

376 """ 

377 Create an integer array of the given length filled with the maximum value. 

378 

379 :param shape: the requested shape 

380 :param dtype: the data type (defaults to 64bit integers) 

381 :return: the new array 

382 

383 >>> import numpy as npx 

384 >>> from moptipy.utils.nputils import np_ints_max 

385 >>> print(np_ints_max(4, npx.dtype("uint8"))) 

386 [255 255 255 255] 

387 """ 

388 return np.full(shape=shape, fill_value=__NP_INTS_MAP[dtype][1], 

389 dtype=dtype) 

390 

391 

392#: the default number of bytes for random seeds 

393__SEED_BYTES: Final[int] = 8 

394#: the minimum acceptable random seed 

395__MIN_RAND_SEED: Final[int] = 0 

396#: the maximum acceptable random seed 

397__MAX_RAND_SEED: Final[int] = int((1 << (__SEED_BYTES * 8)) - 1) 

398 

399 

400def rand_seed_generate(random: Generator = default_rng()) -> int: 

401 """ 

402 Draw a (pseudo-random) random seed. 

403 

404 This method either uses a provided random number generator `random` or a 

405 default generator. It draws 8 bytes from this generator and converts them 

406 to an unsigned (64 bit) integer big-endian style. 

407 

408 :param random: the random number generator to be used to generate the seed 

409 :return: the random seed 

410 :raises TypeError: if `random` is specified but is not an instance of 

411 `Generator` 

412 

413 >>> from numpy.random import default_rng as drg 

414 >>> rand_seed_generate(default_rng(100)) 

415 10991970318022328789 

416 >>> rand_seed_generate(default_rng(100)) 

417 10991970318022328789 

418 >>> rand_seed_generate(default_rng(10991970318022328789)) 

419 11139051376468819756 

420 >>> rand_seed_generate(default_rng(10991970318022328789)) 

421 11139051376468819756 

422 >>> rand_seed_generate(default_rng(11139051376468819756)) 

423 16592984639586750386 

424 >>> rand_seed_generate(default_rng(11139051376468819756)) 

425 16592984639586750386 

426 >>> rand_seed_generate(default_rng(16592984639586750386)) 

427 12064014979695949294 

428 >>> rand_seed_generate(default_rng(16592984639586750386)) 

429 12064014979695949294 

430 """ 

431 if not isinstance(random, Generator): 

432 raise type_error(random, "random", Generator) 

433 return rand_seed_check(int.from_bytes( 

434 random.bytes(__SEED_BYTES), byteorder="big", signed=False)) 

435 

436 

437def rand_seed_check(rand_seed: Any) -> int: 

438 """ 

439 Make sure that a random seed is valid. 

440 

441 :param rand_seed: the random seed to check 

442 :return: the rand seed 

443 

444 :raises TypeError: if the random seed is not an `int` 

445 :raises ValueError: if the random seed is not valid 

446 

447 >>> rand_seed_check(1) 

448 1 

449 >>> rand_seed_check(0) 

450 0 

451 >>> try: 

452 ... rand_seed_check(-1) 

453 ... except ValueError as ve: 

454 ... print(ve) 

455 rand_seed=-1 is invalid, must be in 0..18446744073709551615. 

456 >>> rand_seed_check(18446744073709551615) 

457 18446744073709551615 

458 >>> try: 

459 ... rand_seed_check(18446744073709551616) 

460 ... except ValueError as ve: 

461 ... print(ve) 

462 rand_seed=18446744073709551616 is invalid, must be in 0..\ 

46318446744073709551615. 

464 >>> try: 

465 ... rand_seed_check(1.2) 

466 ... except TypeError as te: 

467 ... print(te) 

468 rand_seed should be an instance of int but is float, namely 1.2. 

469 """ 

470 return check_int_range(rand_seed, "rand_seed", 

471 __MIN_RAND_SEED, __MAX_RAND_SEED) 

472 

473 

474def rand_generator(seed: int) -> Generator: 

475 """ 

476 Instantiate a random number generator from a seed. 

477 

478 :param seed: the random seed 

479 :return: the random number generator 

480 

481 >>> type(rand_generator(1)) 

482 <class 'numpy.random._generator.Generator'> 

483 >>> type(rand_generator(1).bit_generator) 

484 <class 'numpy.random._pcg64.PCG64'> 

485 >>> rand_generator(1).random() == rand_generator(1).random() 

486 True 

487 """ 

488 return default_rng(rand_seed_check(seed)) 

489 

490 

491def rand_seeds_from_str(string: str, n_seeds: int) -> list[int]: 

492 """ 

493 Reproducibly generate `n_seeds` unique random seeds from a `string`. 

494 

495 This function will produce a sorted sequence of `n_seeds` random seeds, 

496 each of which being an unsigned 64-bit integer, from the string passed in. 

497 The same string will always yield the same sequence reproducibly. 

498 Running the function twice with different values of `n_seeds` will result 

499 in the two sets of random seeds, where the larger one (for the larger 

500 value of `n_seeds`) contains all elements of the smaller one. 

501 

502 This works as follows: First, we encode the string to an array of bytes 

503 using the UTF-8 encoding (`string.encode("utf8")`). Then, we compute the 

504 SHA-512 digest of this byte array (using `hashlib.sha512`). 

505 From this digest, we then use two chunks of 32 bytes (256 bit) to seed two 

506 :class:`~numpy.random.PCG64` random number generators. We then 

507 alternatingly draw seeds from these two generators using 

508 :func:`rand_seed_generate` until we have `n_seeds` unique values. 

509 

510 This procedure is used in :func:`moptipy.api.experiment.run_experiment` to 

511 draw the random seeds for the algorithm runs to be performed. As `string` 

512 input, that method uses the string representation of the problem instance. 

513 This guarantees that all algorithms start with the same seeds on the same 

514 problems. It also guarantees that an experiment is repeatable, i.e., will 

515 use the same seeds when executed twice. Finally, it ensures that 

516 cherry-picking is impossible, as all seeds are fairly pseudo-random. 

517 

518 1. Penny Pritzker and Willie E. May, editors, *Secure Hash Standard 

519 (SHS),* Federal Information Processing Standards Publication FIPS PUB 

520 180-4, Gaithersburg, MD, USA: National Institute of Standards and 

521 Technology, Information Technology Laboratory, August 2015. 

522 doi: https://dx.doi.org/10.6028/NIST.FIPS.180-4 

523 https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf 

524 2. Unicode Consortium, editors, *The Unicode(R) Standard, Version 

525 15.0 - Core Specification,* Mountain View, CA, USA: Unicode, Inc., 

526 September 2022, ISBN:978-1-936213-32-0, 

527 https://www.unicode.org/versions/Unicode15.0.0/ 

528 3. NumPy Community, Permuted Congruential Generator (64-bit, PCG64), in 

529 *NumPy Reference, Release 1.23.0,* June 2022, Austin, TX, USA: 

530 NumFOCUS, Inc., https://numpy.org/doc/1.23/numpy-ref.pdf 

531 4. Melissa E. O'Neill: *PCG: A Family of Simple Fast Space-Efficient 

532 Statistically Good Algorithms for Random Number Generation,* Report 

533 HMC-CS-2014-0905, September 5, 2014, Claremont, CA, USA: Harvey Mudd 

534 College, Computer Science Department. 

535 https://www.cs.hmc.edu/tr/hmc-cs-2014-0905.pdf 

536 

537 :param string: the string 

538 :param n_seeds: the number of seeds 

539 :return: a list of random seeds 

540 :raises TypeError: if the parameters do not follow the type contract 

541 :raises ValueError: if the parameter values are invalid 

542 

543 >>> rand_seeds_from_str("hello world!", 1) 

544 [11688012229199056962] 

545 >>> rand_seeds_from_str("hello world!", 2) 

546 [3727742416375614079, 11688012229199056962] 

547 >>> rand_seeds_from_str("hello world!", 3) 

548 [3727742416375614079, 11688012229199056962, 17315292100125916507] 

549 

550 >>> rand_seeds_from_str("metaheuristic optimization", 1) 

551 [12323230366215963648] 

552 >>> rand_seeds_from_str("metaheuristic optimization", 2) 

553 [12323230366215963648, 13673960948036381176] 

554 >>> rand_seeds_from_str("metaheuristic optimization", 3) 

555 [12323230366215963648, 13673960948036381176, 18426184104943646060] 

556 """ 

557 if not isinstance(string, str): 

558 raise type_error(string, "string", str) 

559 if len(string) <= 0: 

560 raise ValueError("string must not be empty.") 

561 if not isinstance(n_seeds, int): 

562 raise type_error(n_seeds, "n_seeds", int) 

563 if n_seeds <= 0: 

564 raise ValueError( 

565 f"n_seeds must be positive, but is {n_seeds}.") 

566 

567 seeds = bytearray(sha512(string.encode("utf8")).digest()) 

568 seed1 = int.from_bytes(seeds[0:32], byteorder="big", signed=False) 

569 seed2 = int.from_bytes(seeds[32:64], byteorder="big", signed=False) 

570 del seeds 

571 

572 # seed two PCG64 generators, each of which should use two 256 bit 

573 # numbers as seed 

574 g1 = Generator(PCG64(seed1)) 

575 g2 = Generator(PCG64(seed2)) 

576 

577 generated: set[int] = set() 

578 while len(generated) < n_seeds: 

579 g1, g2 = g2, g1 

580 generated.add(rand_seed_generate(g1)) 

581 

582 result = list(generated) 

583 result.sort() 

584 

585 if len(result) != n_seeds: 

586 raise ValueError(f"Failed to generate {n_seeds} unique seeds.") 

587 return result 

588 

589 

590@numba.njit(nogil=True) 

591def is_all_finite(a: np.ndarray) -> bool: 

592 """ 

593 Check if an array is all finite. 

594 

595 :param a: the input array 

596 :return: `True` if all elements in the array are finite, `False` otherwise 

597 

598 >>> import numpy as npx 

599 >>> from moptipy.utils.nputils import is_all_finite 

600 >>> print(is_all_finite(npx.array([1.1, 2.1, 3]))) 

601 True 

602 >>> print(is_all_finite(npx.array([1, 2, 3]))) 

603 True 

604 >>> print(is_all_finite(npx.array([1.1, npx.inf, 3]))) 

605 False 

606 """ 

607 for x in a: # noqa 

608 if not np.isfinite(x): # noqa 

609 return False # noqa 

610 return True # noqa 

611 

612 

613#: the character identifying the numpy data type backing the space 

614KEY_NUMPY_TYPE: Final[str] = "dtype" 

615 

616 

617def numpy_type_to_str(dtype: np.dtype) -> str: 

618 """ 

619 Convert a numpy data type to a string. 

620 

621 :param dtype: the data type 

622 :returns: a string representation 

623 

624 >>> import numpy as npx 

625 >>> numpy_type_to_str(npx.dtype(int)) 

626 'l' 

627 >>> numpy_type_to_str(npx.dtype(float)) 

628 'd' 

629 """ 

630 return dtype.char 

631 

632 

633def np_to_py_number(number: Any) -> int | float: 

634 """ 

635 Convert a scalar number from numpy to a corresponding Python type. 

636 

637 :param number: the numpy number 

638 :returns: an integer or float representing the number 

639 

640 >>> type(np_to_py_number(1)) 

641 <class 'int'> 

642 >>> type(np_to_py_number(1.0)) 

643 <class 'float'> 

644 >>> type(np_to_py_number(np.int8(1))) 

645 <class 'int'> 

646 >>> type(np_to_py_number(np.float64(1))) 

647 <class 'float'> 

648 >>> try: 

649 ... np_to_py_number(np.complex64(1)) 

650 ... except TypeError as te: 

651 ... print(te) 

652 number should be an instance of any in {float, int, numpy.floating, \ 

653numpy.integer} but is numpy.complex64. 

654 """ 

655 if isinstance(number, int): 

656 return number 

657 if isinstance(number, np.number): 

658 if isinstance(number, np.integer): 

659 return int(number) 

660 if isinstance(number, np.floating): 

661 return float(number) 

662 if isinstance(number, float): 

663 return number 

664 raise type_error(number, "number", 

665 (int, float, np.integer, np.floating)) 

666 

667 

668def array_to_str(data: np.ndarray) -> str: 

669 """ 

670 Convert a numpy array to a string. 

671 

672 This method represents a numpy array as a string. 

673 It makes sure to include all the information stored in the array and to 

674 represent it as compactly as possible. 

675 

676 If the array has numerical values, it will use the default CSV separator. 

677 If the array contains Boolean values, it will use no separator at all. 

678 

679 :param data: the data 

680 :returns: the string 

681 

682 >>> import numpy as npx 

683 >>> array_to_str(npx.array([1, 2, 3])) 

684 '1;2;3' 

685 >>> array_to_str(npx.array([1, 2.2, 3])) 

686 '1;2.2;3' 

687 >>> array_to_str(npx.array([True, False, True])) 

688 'TFT' 

689 """ 

690 if not isinstance(data, np.ndarray): 

691 raise type_error(data, "data", np.ndarray) 

692 k: Final[str] = data.dtype.kind 

693 if k in {"i", "u"}: 

694 return CSV_SEPARATOR.join(map(str, data)) 

695 if k == "f": 

696 return CSV_SEPARATOR.join(num_to_str(float(d)) for d in data) 

697 if k == "b": 

698 return "".join(bool_to_str(bool(d)) for d in data) 

699 raise ValueError( 

700 f"unsupported data kind {k!r} of type {str(data.dtype)!r}.") 

701 

702 

703@numba.njit(cache=True, inline="always") 

704def fill_in_canonical_permutation(a: np.ndarray) -> None: 

705 """ 

706 Fill the canonical permutation into an array. 

707 

708 >>> import numpy 

709 >>> arr = numpy.empty(10, int) 

710 >>> fill_in_canonical_permutation(arr) 

711 >>> print(arr) 

712 [0 1 2 3 4 5 6 7 8 9] 

713 """ 

714 for i in range(len(a)): # pylint: disable=C0200 

715 a[i] = i