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

141 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-15 11:18 +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 

148 >>> try: 

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

150 ... except ValueError as e: 

151 ... print(e) 

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

15318446744073709551617 for min_value=0. 

154 

155 >>> try: 

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

157 ... except ValueError as e: 

158 ... print(e) 

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

1605807, but -1..18446744073709551615 was specified. 

161 

162 >>> try: 

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

164 ... except TypeError as e: 

165 ... print(e) 

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

167 

168 >>> try: 

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

170 ... except TypeError as e: 

171 ... print(e) 

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

173 

174 >>> try: 

175 ... int_range_to_dtype(0, 1, 1) 

176 ... except TypeError as te: 

177 ... print(te) 

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

179 

180 >>> try: 

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

182 ... except TypeError as te: 

183 ... print(te) 

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

185 

186 >>> try: 

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

188 ... except ValueError as ve: 

189 ... print(ve) 

190 force_signed and force_unsigned cannot both be True. 

191 

192 >>> try: 

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

194 ... except ValueError as ve: 

195 ... print(ve) 

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

197 """ 

198 if not isinstance(min_value, int): 

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

200 if not isinstance(max_value, int): 

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

202 if not isinstance(force_signed, bool): 

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

204 if not isinstance(force_unsigned, bool): 

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

206 if force_unsigned: 

207 if force_signed: 

208 raise ValueError( 

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

210 if min_value < 0: 

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

212 "True is not permitted.") 

213 if min_value > max_value: 

214 raise ValueError( 

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

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

217 

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

219 else min_value 

220 for t in __INTS_AND_RANGES: 

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

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

223 continue 

224 return t[0] 

225 

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

227 raise ValueError( 

228 "max_value for unsigned integers must be <=" 

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

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

231 

232 raise ValueError( 

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

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

235 "was specified.") 

236 

237 

238def dtype_for_data(always_int: bool, 

239 lower_bound: int | float, 

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

241 """ 

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

243 

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

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

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

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

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

249 is returned. 

250 

251 :param always_int: is the data always integer? 

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

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

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

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

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

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

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

259 

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

261 int8 

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

263 uint8 

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

265 int16 

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

267 uint16 

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

269 int32 

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

271 uint16 

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

273 int32 

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

275 int32 

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

277 int32 

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

279 uint32 

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

281 int64 

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

283 int64 

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

285 uint64 

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

287 uint64 

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

289 uint64 

290 >>> try: 

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

292 ... except ValueError as v: 

293 ... print(v) 

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

295is 18446744073709551616 for min_value=0. 

296 

297 >>> from math import inf, nan 

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

299 uint64 

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

301 int64 

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

303 int64 

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

305 int64 

306 >>> try: 

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

308 ... except ValueError as v: 

309 ... print(v) 

310 invalid bounds [1,0]. 

311 

312 >>> try: 

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

314 ... except ValueError as v: 

315 ... print(v) 

316 invalid bounds [1,0]. 

317 

318 >>> try: 

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

320 ... except ValueError as v: 

321 ... print(v) 

322 invalid bounds [1,nan]. 

323 

324 >>> try: 

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

326 ... except ValueError as v: 

327 ... print(v) 

328 invalid bounds [nan,0]. 

329 

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

331 float64 

332 >>> try: 

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

334 ... except TypeError as v: 

335 ... print(v) 

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

337namely '0'. 

338 

339 >>> try: 

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

341 ... except TypeError as v: 

342 ... print(v) 

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

344namely 'x'. 

345 

346 >>> try: 

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

348 ... except TypeError as v: 

349 ... print(v) 

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

351float, namely 1.0. 

352 

353 >>> try: 

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

355 ... except TypeError as v: 

356 ... print(v) 

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

358float, namely 2.0. 

359 

360 >>> try: 

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

362 ... except TypeError as v: 

363 ... print(v) 

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

365 """ 

366 if not isinstance(always_int, bool): 

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

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

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

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

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

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

373 (lower_bound > upper_bound): 

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

375 if always_int: 

376 if isfinite(lower_bound): 

377 if not isinstance(lower_bound, int): 

378 raise type_error( 

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

380 if isfinite(upper_bound): 

381 if not isinstance(upper_bound, int): 

382 raise type_error( 

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

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

385 if lower_bound >= 0: 

386 return DEFAULT_UNSIGNED_INT 

387 return DEFAULT_INT 

388 return DEFAULT_FLOAT 

389 

390 

391@numba.jit(forceobj=True) 

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

393 """ 

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

395 

396 :param shape: the requested shape 

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

398 :return: the new array 

399 

400 >>> import numpy as npx 

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

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

403 [255 255 255 255] 

404 """ 

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

406 dtype=dtype) 

407 

408 

409#: the default number of bytes for random seeds 

410__SEED_BYTES: Final[int] = 8 

411#: the minimum acceptable random seed 

412__MIN_RAND_SEED: Final[int] = 0 

413#: the maximum acceptable random seed 

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

415 

416 

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

418 """ 

419 Draw a (pseudo-random) random seed. 

420 

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

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

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

424 

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

426 :return: the random seed 

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

428 `Generator` 

429 

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

431 >>> rand_seed_generate(default_rng(100)) 

432 10991970318022328789 

433 >>> rand_seed_generate(default_rng(100)) 

434 10991970318022328789 

435 >>> rand_seed_generate(default_rng(10991970318022328789)) 

436 11139051376468819756 

437 >>> rand_seed_generate(default_rng(10991970318022328789)) 

438 11139051376468819756 

439 >>> rand_seed_generate(default_rng(11139051376468819756)) 

440 16592984639586750386 

441 >>> rand_seed_generate(default_rng(11139051376468819756)) 

442 16592984639586750386 

443 >>> rand_seed_generate(default_rng(16592984639586750386)) 

444 12064014979695949294 

445 >>> rand_seed_generate(default_rng(16592984639586750386)) 

446 12064014979695949294 

447 """ 

448 if not isinstance(random, Generator): 

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

450 return rand_seed_check(int.from_bytes( 

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

452 

453 

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

455 """ 

456 Make sure that a random seed is valid. 

457 

458 :param rand_seed: the random seed to check 

459 :return: the rand seed 

460 

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

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

463 

464 >>> rand_seed_check(1) 

465 1 

466 >>> rand_seed_check(0) 

467 0 

468 >>> try: 

469 ... rand_seed_check(-1) 

470 ... except ValueError as ve: 

471 ... print(ve) 

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

473 

474 >>> rand_seed_check(18446744073709551615) 

475 18446744073709551615 

476 >>> try: 

477 ... rand_seed_check(18446744073709551616) 

478 ... except ValueError as ve: 

479 ... print(ve) 

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

48118446744073709551615. 

482 

483 >>> try: 

484 ... rand_seed_check(1.2) 

485 ... except TypeError as te: 

486 ... print(te) 

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

488 """ 

489 return check_int_range(rand_seed, "rand_seed", 

490 __MIN_RAND_SEED, __MAX_RAND_SEED) 

491 

492 

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

494 """ 

495 Instantiate a random number generator from a seed. 

496 

497 :param seed: the random seed 

498 :return: the random number generator 

499 

500 >>> type(rand_generator(1)) 

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

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

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

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

505 True 

506 """ 

507 return default_rng(rand_seed_check(seed)) 

508 

509 

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

511 """ 

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

513 

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

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

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

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

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

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

520 

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

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

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

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

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

526 alternatingly draw seeds from these two generators using 

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

528 

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

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

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

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

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

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

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

536 

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

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

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

540 Technology, Information Technology Laboratory, August 2015. 

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

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

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

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

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

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

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

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

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

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

551 Statistically Good Algorithms for Random Number Generation,* Report 

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

553 College, Computer Science Department. 

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

555 

556 :param string: the string 

557 :param n_seeds: the number of seeds 

558 :return: a list of random seeds 

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

560 :raises ValueError: if the parameter values are invalid 

561 

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

563 [11688012229199056962] 

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

565 [3727742416375614079, 11688012229199056962] 

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

567 [3727742416375614079, 11688012229199056962, 17315292100125916507] 

568 

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

570 [12323230366215963648] 

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

572 [12323230366215963648, 13673960948036381176] 

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

574 [12323230366215963648, 13673960948036381176, 18426184104943646060] 

575 """ 

576 if not isinstance(string, str): 

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

578 if len(string) <= 0: 

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

580 if not isinstance(n_seeds, int): 

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

582 if n_seeds <= 0: 

583 raise ValueError( 

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

585 

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

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

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

589 del seeds 

590 

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

592 # numbers as seed 

593 g1 = Generator(PCG64(seed1)) 

594 g2 = Generator(PCG64(seed2)) 

595 

596 generated: set[int] = set() 

597 while len(generated) < n_seeds: 

598 g1, g2 = g2, g1 

599 generated.add(rand_seed_generate(g1)) 

600 

601 result = list(generated) 

602 result.sort() 

603 

604 if len(result) != n_seeds: 

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

606 return result 

607 

608 

609@numba.njit(nogil=True) 

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

611 """ 

612 Check if an array is all finite. 

613 

614 :param a: the input array 

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

616 

617 >>> import numpy as npx 

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

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

620 True 

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

622 True 

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

624 False 

625 """ 

626 for x in a: # noqa 

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

628 return False # noqa 

629 return True # noqa 

630 

631 

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

633KEY_NUMPY_TYPE: Final[str] = "dtype" 

634 

635 

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

637 """ 

638 Convert a numpy data type to a string. 

639 

640 :param dtype: the data type 

641 :returns: a string representation 

642 

643 >>> import numpy as npx 

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

645 'l' 

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

647 'd' 

648 """ 

649 return dtype.char 

650 

651 

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

653 """ 

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

655 

656 :param number: the numpy number 

657 :returns: an integer or float representing the number 

658 

659 >>> type(np_to_py_number(1)) 

660 <class 'int'> 

661 >>> type(np_to_py_number(1.0)) 

662 <class 'float'> 

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

664 <class 'int'> 

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

666 <class 'float'> 

667 >>> try: 

668 ... np_to_py_number(np.complex64(1)) 

669 ... except TypeError as te: 

670 ... print(te) 

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

672numpy.integer} but is numpy.complex64. 

673 """ 

674 if isinstance(number, int): 

675 return number 

676 if isinstance(number, np.number): 

677 if isinstance(number, np.integer): 

678 return int(number) 

679 if isinstance(number, np.floating): 

680 return float(number) 

681 if isinstance(number, float): 

682 return number 

683 raise type_error(number, "number", 

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

685 

686 

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

688 """ 

689 Convert a numpy array to a string. 

690 

691 This method represents a numpy array as a string. 

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

693 represent it as compactly as possible. 

694 

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

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

697 

698 :param data: the data 

699 :returns: the string 

700 

701 >>> import numpy as npx 

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

703 '1;2;3' 

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

705 '1;2.2;3' 

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

707 'TFT' 

708 """ 

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

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

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

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

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

714 if k == "f": 

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

716 if k == "b": 

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

718 raise ValueError( 

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

720 

721 

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

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

724 """ 

725 Fill the canonical permutation into an array. 

726 

727 >>> import numpy 

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

729 >>> fill_in_canonical_permutation(arr) 

730 >>> print(arr) 

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

732 """ 

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

734 a[i] = i