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
« 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
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.
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.")
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]
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}.")
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.")
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.
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.
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
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
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.
379 :param shape: the requested shape
380 :param dtype: the data type (defaults to 64bit integers)
381 :return: the new array
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)
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)
400def rand_seed_generate(random: Generator = default_rng()) -> int:
401 """
402 Draw a (pseudo-random) random seed.
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.
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`
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))
437def rand_seed_check(rand_seed: Any) -> int:
438 """
439 Make sure that a random seed is valid.
441 :param rand_seed: the random seed to check
442 :return: the rand seed
444 :raises TypeError: if the random seed is not an `int`
445 :raises ValueError: if the random seed is not valid
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)
474def rand_generator(seed: int) -> Generator:
475 """
476 Instantiate a random number generator from a seed.
478 :param seed: the random seed
479 :return: the random number generator
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))
491def rand_seeds_from_str(string: str, n_seeds: int) -> list[int]:
492 """
493 Reproducibly generate `n_seeds` unique random seeds from a `string`.
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.
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.
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.
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
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
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]
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}.")
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
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))
577 generated: set[int] = set()
578 while len(generated) < n_seeds:
579 g1, g2 = g2, g1
580 generated.add(rand_seed_generate(g1))
582 result = list(generated)
583 result.sort()
585 if len(result) != n_seeds:
586 raise ValueError(f"Failed to generate {n_seeds} unique seeds.")
587 return result
590@numba.njit(nogil=True)
591def is_all_finite(a: np.ndarray) -> bool:
592 """
593 Check if an array is all finite.
595 :param a: the input array
596 :return: `True` if all elements in the array are finite, `False` otherwise
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
613#: the character identifying the numpy data type backing the space
614KEY_NUMPY_TYPE: Final[str] = "dtype"
617def numpy_type_to_str(dtype: np.dtype) -> str:
618 """
619 Convert a numpy data type to a string.
621 :param dtype: the data type
622 :returns: a string representation
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
633def np_to_py_number(number: Any) -> int | float:
634 """
635 Convert a scalar number from numpy to a corresponding Python type.
637 :param number: the numpy number
638 :returns: an integer or float representing the number
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))
668def array_to_str(data: np.ndarray) -> str:
669 """
670 Convert a numpy array to a string.
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.
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.
679 :param data: the data
680 :returns: the string
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}.")
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.
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