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
« 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
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
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)))
29#: The numpy integer data types.
30INTS: Final[tuple[np.dtype, ...]] = tuple(a[0] for a in __INTS_AND_RANGES)
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}
37#: The default integer type: the signed 64-bit integer.
38DEFAULT_INT: Final[np.dtype] = INTS[-2]
40#: The default unsigned integer type: an unsigned 64-bit integer.
41DEFAULT_UNSIGNED_INT: Final[np.dtype] = INTS[-1]
43#: The default boolean type.
44DEFAULT_BOOL: Final[np.dtype] = np.dtype(np.bool_)
46#: The default floating point type.
47DEFAULT_FLOAT: Final[np.dtype] = np.dtype(float)
49#: The default numerical types.
50DEFAULT_NUMERICAL: Final[tuple[np.dtype, ...]] = (*list(INTS), DEFAULT_FLOAT)
53def is_np_int(dtype: np.dtype) -> bool:
54 """
55 Check whether a :class:`numpy.dtype` is an integer type.
57 :param dtype: the type
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"}
71def is_np_float(dtype: np.dtype) -> bool:
72 """
73 Check whether a :class:`numpy.dtype` is a floating point type.
75 :param dtype: the type
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"
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.
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`.
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.
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.
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.
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.
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.
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'.
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.
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.
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.
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.")
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]
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}.")
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.")
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.
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.
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
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.
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].
312 >>> try:
313 ... dtype_for_data(False, 1, 0)
314 ... except ValueError as v:
315 ... print(v)
316 invalid bounds [1,0].
318 >>> try:
319 ... dtype_for_data(True, 1, nan)
320 ... except ValueError as v:
321 ... print(v)
322 invalid bounds [1,nan].
324 >>> try:
325 ... dtype_for_data(False, nan, 0)
326 ... except ValueError as v:
327 ... print(v)
328 invalid bounds [nan,0].
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'.
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'.
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.
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.
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
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.
396 :param shape: the requested shape
397 :param dtype: the data type (defaults to 64bit integers)
398 :return: the new array
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)
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)
417def rand_seed_generate(random: Generator = default_rng()) -> int:
418 """
419 Draw a (pseudo-random) random seed.
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.
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`
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))
454def rand_seed_check(rand_seed: Any) -> int:
455 """
456 Make sure that a random seed is valid.
458 :param rand_seed: the random seed to check
459 :return: the rand seed
461 :raises TypeError: if the random seed is not an `int`
462 :raises ValueError: if the random seed is not valid
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.
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.
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)
493def rand_generator(seed: int) -> Generator:
494 """
495 Instantiate a random number generator from a seed.
497 :param seed: the random seed
498 :return: the random number generator
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))
510def rand_seeds_from_str(string: str, n_seeds: int) -> list[int]:
511 """
512 Reproducibly generate `n_seeds` unique random seeds from a `string`.
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.
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.
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.
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
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
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]
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}.")
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
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))
596 generated: set[int] = set()
597 while len(generated) < n_seeds:
598 g1, g2 = g2, g1
599 generated.add(rand_seed_generate(g1))
601 result = list(generated)
602 result.sort()
604 if len(result) != n_seeds:
605 raise ValueError(f"Failed to generate {n_seeds} unique seeds.")
606 return result
609@numba.njit(nogil=True)
610def is_all_finite(a: np.ndarray) -> bool:
611 """
612 Check if an array is all finite.
614 :param a: the input array
615 :return: `True` if all elements in the array are finite, `False` otherwise
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
632#: the character identifying the numpy data type backing the space
633KEY_NUMPY_TYPE: Final[str] = "dtype"
636def numpy_type_to_str(dtype: np.dtype) -> str:
637 """
638 Convert a numpy data type to a string.
640 :param dtype: the data type
641 :returns: a string representation
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
652def np_to_py_number(number: Any) -> int | float:
653 """
654 Convert a scalar number from numpy to a corresponding Python type.
656 :param number: the numpy number
657 :returns: an integer or float representing the number
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))
687def array_to_str(data: np.ndarray) -> str:
688 """
689 Convert a numpy array to a string.
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.
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.
698 :param data: the data
699 :returns: the string
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}.")
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.
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