Skip to content

Commit 0188b56

Browse files
sgassebluss
authored andcommitted
Add examples for type conversion
- Add `type_conversion.rs` to illustrate some common conversions. - Update the documentation for numpy users.
1 parent 5d30944 commit 0188b56

File tree

2 files changed

+209
-0
lines changed

2 files changed

+209
-0
lines changed

examples/type_conversion.rs

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
#[cfg(feature = "approx")]
2+
use {approx::assert_abs_diff_eq, ndarray::prelude::*, std::convert::TryFrom};
3+
4+
#[cfg(feature = "approx")]
5+
fn main() {
6+
// Converting an array from one datatype to another is implemented with the
7+
// `ArrayBase::mapv()` function. We pass a closure that is applied to each
8+
// element independently. This allows for more control and flexiblity in
9+
// converting types.
10+
//
11+
// Below, we illustrate four different approaches for the actual conversion
12+
// in the closure.
13+
// - `From` ensures lossless conversions known at compile time and is the
14+
// best default choice.
15+
// - `TryFrom` either converts data losslessly or panics, ensuring that the
16+
// rest of the program does not continue with unexpected data.
17+
// - `as` never panics and may silently convert in a lossy way, depending
18+
// on the source and target datatypes. More details can be found in the
19+
// reference: https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast
20+
// - Using custom logic in the closure, e.g. to clip values or for NaN
21+
// handling in floats.
22+
//
23+
// For a brush-up on casting between numeric types in Rust, refer to:
24+
// https://doc.rust-lang.org/rust-by-example/types/cast.html
25+
26+
// Infallible, lossless conversion with `From`
27+
// The trait `std::convert::From` is only implemented for conversions that
28+
// can be guaranteed to be lossless at compile time. This is the safest
29+
// approach.
30+
let a_u8: Array<u8, _> = array![[1, 2, 3], [4, 5, 6]];
31+
let a_f32 = a_u8.mapv(|element| f32::from(element));
32+
assert_abs_diff_eq!(a_f32, array![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
33+
34+
// Fallible, lossless conversion with `TryFrom`
35+
// `i8` numbers can be negative, in such a case, there is no perfect
36+
// conversion to `u8` defined. In this example, all numbers are positive and
37+
// in bounds and can be converted at runtime. But for unknown runtime input,
38+
// this would panic with the message provided in `.expect()`. Note that you
39+
// can also use `.unwrap()` to be more concise.
40+
let a_i8: Array<i8, _> = array![120, 8, 0];
41+
let a_u8 = a_i8.mapv(|element| u8::try_from(element).expect("Could not convert i8 to u8"));
42+
assert_eq!(a_u8, array![120u8, 8u8, 0u8]);
43+
44+
// Unsigned to signed integer conversion with `as`
45+
// A real-life example of this would be coordinates on a grid.
46+
// A `usize` value can be larger than what fits into a `isize`, therefore,
47+
// it would be safer to use `TryFrom`. Nevertheless, `as` can be used for
48+
// either simplicity or performance.
49+
// The example includes `usize::MAX` to illustrate potentially undesired
50+
// behavior. It will be interpreted as -1 (noop-casting + 2-complement), see
51+
// https://doc.rust-lang.org/reference/expressions/operator-expr.html#numeric-cast
52+
let a_usize: Array<usize, _> = array![1, 2, 3, usize::MAX];
53+
let a_isize = a_usize.mapv(|element| element as isize);
54+
assert_eq!(a_isize, array![1_isize, 2_isize, 3_isize, -1_isize]);
55+
56+
// Simple upcasting with `as`
57+
// Every `u8` fits perfectly into a `u32`, therefore this is a lossless
58+
// conversion.
59+
// Note that it is up to the programmer to ensure the validity of the
60+
// conversion over the lifetime of a program. With type inference, subtle
61+
// bugs can creep in since conversions with `as` will always compile, so a
62+
// programmer might not notice that a prior lossless conversion became a
63+
// lossy conversion. With `From`, this would be noticed at compile-time and
64+
// with `TryFrom`, it would also be either handled or make the program
65+
// panic.
66+
let a_u8: Array<u8, _> = array![[1, 2, 3], [4, 5, 6]];
67+
let a_u32 = a_u8.mapv(|element| element as u32);
68+
assert_eq!(a_u32, array![[1u32, 2u32, 3u32], [4u32, 5u32, 6u32]]);
69+
70+
// Saturating cast with `as`
71+
// The `as` keyword performs a *saturating cast* When casting floats to
72+
// ints. This means that numbers which do not fit into the target datatype
73+
// will silently be clipped to the maximum/minimum numbers. Since this is
74+
// not obvious, we discourage the intentional use of casting with `as` with
75+
// silent saturation and recommend a custom logic instead which makes the
76+
// intent clear.
77+
let a_f32: Array<f32, _> = array![
78+
256.0, // saturated to 255
79+
255.7, // saturated to 255
80+
255.1, // saturated to 255
81+
254.7, // rounded down to 254 by cutting the decimal part
82+
254.1, // rounded down to 254 by cutting the decimal part
83+
-1.0, // saturated to 0 on the lower end
84+
f32::INFINITY, // saturated to 255
85+
f32::NAN, // converted to zero
86+
];
87+
let a_u8 = a_f32.mapv(|element| element as u8);
88+
assert_eq!(a_u8, array![255, 255, 255, 254, 254, 0, 255, 0]);
89+
90+
// Custom mapping logic
91+
// Given that we pass a closure for the conversion, we can also define
92+
// custom logic to e.g. replace NaN values and clip others. This also
93+
// makes the intent clear.
94+
let a_f32: Array<f32, _> = array![
95+
270.0, // clipped to 200
96+
-1.2, // clipped to 0
97+
4.7, // rounded up to 5 instead of just stripping decimals
98+
f32::INFINITY, // clipped to 200
99+
f32::NAN, // replaced with upper bound 200
100+
];
101+
let a_u8_custom = a_f32.mapv(|element| {
102+
if element == f32::INFINITY || element.is_nan() {
103+
return 200;
104+
}
105+
if let Some(std::cmp::Ordering::Less) = element.partial_cmp(&0.0) {
106+
return 0;
107+
}
108+
200.min(element.round() as u8)
109+
});
110+
assert_eq!(a_u8_custom, array![200, 0, 5, 200, 200]);
111+
}
112+
113+
#[cfg(not(feature = "approx"))]
114+
fn main() {}

src/doc/ndarray_for_numpy_users/mod.rs

+95
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,101 @@
524524
//! </td></tr>
525525
//! </table>
526526
//!
527+
//! ## Type conversions
528+
//!
529+
//! In `ndarray`, conversions between datatypes are done with `mapv()` by
530+
//! passing a closure to convert every element independently.
531+
//! For the conversion itself, we have several options:
532+
//! - `std::convert::From` ensures lossless, safe conversions at compile-time
533+
//! and is generally recommended.
534+
//! - `std::convert::TryFrom` can be used for potentially unsafe conversions. It
535+
//! will return a `Result` which can be handled or `unwrap()`ed to panic if
536+
//! any value at runtime cannot be converted losslessly.
537+
//! - The `as` keyword compiles to lossless/lossy conversions depending on the
538+
//! source and target datatypes. It can be useful when `TryFrom` is a
539+
//! performance issue or does not apply. A notable difference to NumPy is that
540+
//! `as` performs a [*saturating* cast][sat_conv] when casting
541+
//! from floats to integers. Further information can be found in the
542+
//! [reference on type cast expressions][as_typecast].
543+
//!
544+
//! For details, be sure to check out the type conversion examples.
545+
//!
546+
547+
//! <table>
548+
//! <tr><th>
549+
//!
550+
//! NumPy
551+
//!
552+
//! </th><th>
553+
//!
554+
//! `ndarray`
555+
//!
556+
//! </th><th>
557+
//!
558+
//! Notes
559+
//!
560+
//! </th></tr>
561+
//!
562+
//! <tr><td>
563+
//!
564+
//! `a.astype(np.float32)`
565+
//!
566+
//! </td><td>
567+
//!
568+
//! `a.mapv(|x| f32::from(x))`
569+
//!
570+
//! </td><td>
571+
//!
572+
//! convert `u8` array infallibly to `f32` array with `std::convert::From`, generally recommended
573+
//!
574+
//! </td></tr>
575+
//!
576+
//! <tr><td>
577+
//!
578+
//! `a.astype(np.int32)`
579+
//!
580+
//! </td><td>
581+
//!
582+
//! `a.mapv(|x| i32::from(x))`
583+
//!
584+
//! </td><td>
585+
//!
586+
//! upcast `u8` array to `i32` array with `std::convert::From`, preferable over `as` because it ensures at compile-time that the conversion is lossless
587+
//!
588+
//! </td></tr>
589+
//!
590+
//! <tr><td>
591+
//!
592+
//! `a.astype(np.uint8)`
593+
//!
594+
//! </td><td>
595+
//!
596+
//! `a.mapv(|x| u8::try_from(x).unwrap())`
597+
//!
598+
//! </td><td>
599+
//!
600+
//! try to convert `i8` array to `u8` array, panic if any value cannot be converted lossless at runtime (e.g. negative value)
601+
//!
602+
//! </td></tr>
603+
//!
604+
//! <tr><td>
605+
//!
606+
//! `a.astype(np.int32)`
607+
//!
608+
//! </td><td>
609+
//!
610+
//! `a.mapv(|x| x as i32)`
611+
//!
612+
//! </td><td>
613+
//!
614+
//! convert `f32` array to `i32` array with ["saturating" conversion][sat_conv]; care needed because it can be a lossy conversion or result in non-finite values! See [the reference for information][as_typecast].
615+
//!
616+
//! </td></tr>
617+
//!
618+
//! [as_conv]: https://doc.rust-lang.org/rust-by-example/types/cast.html
619+
//! [sat_conv]: https://blog.rust-lang.org/2020/07/16/Rust-1.45.0.html#fixing-unsoundness-in-casts
620+
//! [as_typecast]: https://doc.rust-lang.org/reference/expressions/operator-expr.html#type-cast-expressions
621+
//!
527622
//! ## Array manipulation
528623
//!
529624
//! NumPy | `ndarray` | Notes

0 commit comments

Comments
 (0)