From 4a9578d9195fe8c7a11ab36ea5a93f80579032f4 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 09:07:02 +0100 Subject: [PATCH 1/9] see what is even happening --- src/compute.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/compute.rs b/src/compute.rs index 8f71a76..ff37d91 100644 --- a/src/compute.rs +++ b/src/compute.rs @@ -358,6 +358,8 @@ impl Warper { ) -> Result, WarperError> { let source_raster: ArrayView2 = source_raster.into(); + dbg!(&source_raster); + self.validate_source_raster_shape(&source_raster)?; Zip::from(&source_raster).par_fold( From 5994d10354f2d2888135300f0805a8b593e62712 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 12:58:33 +0100 Subject: [PATCH 2/9] rewrite tests to make more sense --- src/compute.rs | 6 +- tests/errcheck.rs | 300 ++++++++++++++++++++++++++++++++++++++++++++-- tests/nodata.rs | 202 ------------------------------- 3 files changed, 295 insertions(+), 213 deletions(-) delete mode 100644 tests/nodata.rs diff --git a/src/compute.rs b/src/compute.rs index ff37d91..86238f4 100644 --- a/src/compute.rs +++ b/src/compute.rs @@ -116,7 +116,7 @@ impl Warper { *v = result; FoldWhile::Continue(Ok(())) } else { - FoldWhile::Done(Err(WarperError::NanError)) + FoldWhile::Done(Err(WarperError::WarpingError)) } }) .into_inner()?; @@ -154,7 +154,7 @@ impl Warper { let value = values[[j, i]]; if value.is_nan() { - return FoldWhile::Done(Err(WarperError::WarpingError)); + return FoldWhile::Done(Err(WarperError::NanError)); } let x_weight = intr.x_weights[i]; inner_weight_accum += x_weight; @@ -358,8 +358,6 @@ impl Warper { ) -> Result, WarperError> { let source_raster: ArrayView2 = source_raster.into(); - dbg!(&source_raster); - self.validate_source_raster_shape(&source_raster)?; Zip::from(&source_raster).par_fold( diff --git a/tests/errcheck.rs b/tests/errcheck.rs index b2d2cf1..865619d 100644 --- a/tests/errcheck.rs +++ b/tests/errcheck.rs @@ -1,16 +1,215 @@ use core::f64; use anyhow::Result; +use float_cmp::assert_approx_eq; use mappers::{ Ellipsoid, projections::{LambertConformalConic, LongitudeLatitude}, }; use mappers_warp::{CubicBSpline, RasterBoundsDefinition, Warper, WarperError}; -use ndarray::{Array2, s}; +use ndarray::{Array2, Zip, s}; mod utils; use utils::*; +#[test] +fn result_ok() -> Result<()> { + let src_proj = LongitudeLatitude; + let tgt_proj = LambertConformalConic::builder() + .ref_lonlat(80., 24.) + .standard_parallels(12.472955, 35.1728044444444) + .ellipsoid(Ellipsoid::WGS84) + .initialize_projection()?; + + let source_bounds = + RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; + let target_bounds = RasterBoundsDefinition::new( + (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), + (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), + 10_000., + 10_000., + tgt_proj, + )?; + + let warper = Warper::initialize::( + &source_bounds, + &target_bounds, + )?; + + let source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; + let ref_raster: Array2 = open_nc_data("./tests/data/waves_ref.nc")?; + + let target_raster = warper.warp_unchecked(&source_raster); + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + let target_raster = warper.warp_reject_nodata(&source_raster)?; + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + let target_raster = warper.warp_ignore_nodata(&source_raster)?; + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + let target_raster = warper.warp_discard_nodata(&source_raster)?; + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + #[cfg(feature = "multithreading")] + { + let target_raster = warper.warp_unchecked_parallel(&source_raster); + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + let target_raster = warper.warp_reject_nodata_parallel(&source_raster)?; + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + let target_raster = warper.warp_ignore_nodata_parallel(&source_raster)?; + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + + let target_raster = warper.warp_discard_nodata_parallel(&source_raster)?; + assert_eq!(target_raster.shape(), ref_raster.shape()); + Zip::from(&target_raster) + .and(&ref_raster) + .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); + } + + Ok(()) +} + +#[test] +fn single_nan_input_area() -> Result<()> { + let src_proj = LongitudeLatitude; + let tgt_proj = LambertConformalConic::builder() + .ref_lonlat(80., 24.) + .standard_parallels(12.472955, 35.1728044444444) + .ellipsoid(Ellipsoid::WGS84) + .initialize_projection()?; + + let source_bounds = + RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; + let target_bounds = RasterBoundsDefinition::new( + (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), + (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), + 10_000., + 10_000., + tgt_proj, + )?; + + let warper = Warper::initialize::( + &source_bounds, + &target_bounds, + )?; + + let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; + source_raster.slice_mut(s![14..15, 18..19]).fill(f64::NAN); + + let _ = warper.warp_unchecked(&source_raster); + + let target_raster = warper.warp_reject_nodata(&source_raster).unwrap_err(); + assert!(matches!(target_raster, WarperError::NanError)); + + let target_raster = warper.warp_ignore_nodata(&source_raster); + assert!(target_raster.is_ok()); + + let target_raster = warper.warp_discard_nodata(&source_raster); + assert!(target_raster.is_ok()); + + #[cfg(feature = "multithreading")] + { + let _ = warper.warp_unchecked_parallel(&source_raster); + + let target_raster = warper + .warp_reject_nodata_parallel(&source_raster) + .unwrap_err(); + assert!(matches!(target_raster, WarperError::NanError)); + + let target_raster = warper.warp_ignore_nodata_parallel(&source_raster); + assert!(target_raster.is_ok()); + + let target_raster = warper.warp_discard_nodata_parallel(&source_raster); + assert!(target_raster.is_ok()); + } + + Ok(()) +} + +#[test] +fn multi_nan_input_area() -> Result<()> { + let src_proj = LongitudeLatitude; + let tgt_proj = LambertConformalConic::builder() + .ref_lonlat(80., 24.) + .standard_parallels(12.472955, 35.1728044444444) + .ellipsoid(Ellipsoid::WGS84) + .initialize_projection()?; + + let source_bounds = + RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; + let target_bounds = RasterBoundsDefinition::new( + (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), + (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), + 10_000., + 10_000., + tgt_proj, + )?; + + let warper = Warper::initialize::( + &source_bounds, + &target_bounds, + )?; + + let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; + source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); + source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); + source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); + source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); + + let _ = warper.warp_unchecked(&source_raster); + + let target_raster = warper.warp_reject_nodata(&source_raster).unwrap_err(); + assert!(matches!(target_raster, WarperError::NanError)); + + let target_raster = warper.warp_ignore_nodata(&source_raster); + assert!(target_raster.is_ok()); + + let target_raster = warper.warp_discard_nodata(&source_raster); + assert!(target_raster.is_ok()); + + #[cfg(feature = "multithreading")] + { + let _ = warper.warp_unchecked_parallel(&source_raster); + + let target_raster = warper + .warp_reject_nodata_parallel(&source_raster) + .unwrap_err(); + assert!(matches!(target_raster, WarperError::NanError)); + + let target_raster = warper.warp_ignore_nodata_parallel(&source_raster); + assert!(target_raster.is_ok()); + + let target_raster = warper.warp_discard_nodata_parallel(&source_raster); + assert!(target_raster.is_ok()); + } + + Ok(()) +} + #[test] fn non_finite_result() -> Result<()> { let src_proj = LongitudeLatitude; @@ -40,15 +239,43 @@ fn non_finite_result() -> Result<()> { .slice_mut(s![13..15, 21..23]) .fill(f64::INFINITY); - assert!(warper.warp_discard_nodata(&source_raster).is_err()); - assert!(warper.warp_reject_nodata(&source_raster).is_err()); - assert!(warper.warp_ignore_nodata(&source_raster).is_err()); + let _ = warper.warp_unchecked(&source_raster); + + assert!(matches!( + warper.warp_discard_nodata(&source_raster).unwrap_err(), + WarperError::WarpingError + )); + assert!(matches!( + warper.warp_reject_nodata(&source_raster).unwrap_err(), + WarperError::WarpingError + )); + assert!(matches!( + warper.warp_ignore_nodata(&source_raster).unwrap_err(), + WarperError::WarpingError + )); #[cfg(feature = "multithreading")] { - assert!(warper.warp_discard_nodata_parallel(&source_raster).is_err()); - assert!(warper.warp_reject_nodata_parallel(&source_raster).is_err()); - assert!(warper.warp_ignore_nodata_parallel(&source_raster).is_err()); + let _ = warper.warp_unchecked_parallel(&source_raster); + + assert!(matches!( + warper + .warp_discard_nodata_parallel(&source_raster) + .unwrap_err(), + WarperError::WarpingError + )); + assert!(matches!( + warper + .warp_reject_nodata_parallel(&source_raster) + .unwrap_err(), + WarperError::WarpingError + )); + assert!(matches!( + warper + .warp_ignore_nodata_parallel(&source_raster) + .unwrap_err(), + WarperError::WarpingError + )); } Ok(()) @@ -153,3 +380,62 @@ fn invalid_input_shape() -> Result<()> { Ok(()) } + +#[test] +#[should_panic = "Slice end 11 is past end of axis of length 10"] +fn invalid_input_shape_unchecked() { + let src_proj = LongitudeLatitude; + let tgt_proj = LambertConformalConic::builder() + .ref_lonlat(80., 24.) + .standard_parallels(12.472955, 35.1728044444444) + .ellipsoid(Ellipsoid::WGS84) + .initialize_projection() + .unwrap(); + + let source_bounds = + RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj).unwrap(); + let target_bounds = RasterBoundsDefinition::new( + (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), + (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), + 10_000., + 10_000., + tgt_proj, + ) + .unwrap(); + + let warper = Warper::initialize::(&source_bounds, &target_bounds).unwrap(); + + // valid shape: (34, 34) + let invalid_source_raster = Array2::::zeros((10, 10)); + let _ = warper.warp_unchecked(&invalid_source_raster); +} + +#[test] +#[should_panic = "Slice end 11 is past end of axis of length 10"] +#[cfg(feature = "multithreading")] +fn invalid_input_shape_unchecked_parallel() { + let src_proj = LongitudeLatitude; + let tgt_proj = LambertConformalConic::builder() + .ref_lonlat(80., 24.) + .standard_parallels(12.472955, 35.1728044444444) + .ellipsoid(Ellipsoid::WGS84) + .initialize_projection() + .unwrap(); + + let source_bounds = + RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj).unwrap(); + let target_bounds = RasterBoundsDefinition::new( + (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), + (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), + 10_000., + 10_000., + tgt_proj, + ) + .unwrap(); + + let warper = Warper::initialize::(&source_bounds, &target_bounds).unwrap(); + + // valid shape: (34, 34) + let invalid_source_raster = Array2::::zeros((10, 10)); + let _ = warper.warp_unchecked_parallel(&invalid_source_raster); +} diff --git a/tests/nodata.rs b/tests/nodata.rs deleted file mode 100644 index 121dc5e..0000000 --- a/tests/nodata.rs +++ /dev/null @@ -1,202 +0,0 @@ -use anyhow::Result; -use float_cmp::assert_approx_eq; -use mappers::{ - Ellipsoid, - projections::{LambertConformalConic, LongitudeLatitude}, -}; -use mappers_warp::{CubicBSpline, RasterBoundsDefinition, Warper}; -use ndarray::{Array2, Zip, s}; - -mod utils; -use utils::*; - -#[test] -fn waves_unchecked() -> Result<()> { - let src_proj = LongitudeLatitude; - let tgt_proj = LambertConformalConic::builder() - .ref_lonlat(80., 24.) - .standard_parallels(12.472955, 35.1728044444444) - .ellipsoid(Ellipsoid::WGS84) - .initialize_projection()?; - - let source_bounds = - RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; - let target_bounds = RasterBoundsDefinition::new( - (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), - (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), - 10_000., - 10_000., - tgt_proj, - )?; - - let warper = Warper::initialize::( - &source_bounds, - &target_bounds, - )?; - - let source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - let ref_raster: Array2 = open_nc_data("./tests/data/waves_ref.nc")?; - let target_raster = warper.warp_unchecked(&source_raster); - - assert_eq!(target_raster.shape(), ref_raster.shape()); - Zip::from(&target_raster) - .and(&ref_raster) - .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); - - #[cfg(feature = "multithreading")] - { - let par_target_raster = warper.warp_unchecked_parallel(&source_raster); - assert_eq!(par_target_raster, target_raster); - } - - Ok(()) -} - -#[test] -fn nan_ignore() -> Result<()> { - let src_proj = LongitudeLatitude; - let tgt_proj = LambertConformalConic::builder() - .ref_lonlat(80., 24.) - .standard_parallels(12.472955, 35.1728044444444) - .ellipsoid(Ellipsoid::WGS84) - .initialize_projection()?; - - let source_bounds = - RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; - let target_bounds = RasterBoundsDefinition::new( - (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), - (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), - 10_000., - 10_000., - tgt_proj, - )?; - - let warper = Warper::initialize::( - &source_bounds, - &target_bounds, - )?; - - let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); - source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); - source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); - source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); - - let target_raster = warper.warp_ignore_nodata(&source_raster)?; - let ref_raster: Array2 = open_nc_data("./tests/data/waves_nan_ignore_ref.nc")?; - - assert_eq!(target_raster.shape(), ref_raster.shape()); - Zip::from(&target_raster) - .and(&ref_raster) - .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); - - #[cfg(feature = "multithreading")] - { - let par_target_raster = warper.warp_ignore_nodata_parallel(&source_raster)?; - Zip::from(&target_raster) - .and(&par_target_raster) - .for_each(|&ser, &par| assert_approx_eq!(f64, ser, par)); - } - - Ok(()) -} - -#[test] -fn nan_reject() -> Result<()> { - let src_proj = LongitudeLatitude; - let tgt_proj = LambertConformalConic::builder() - .ref_lonlat(80., 24.) - .standard_parallels(12.472955, 35.1728044444444) - .ellipsoid(Ellipsoid::WGS84) - .initialize_projection()?; - - let source_bounds = - RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; - let target_bounds = RasterBoundsDefinition::new( - (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), - (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), - 10_000., - 10_000., - tgt_proj, - )?; - - let warper = Warper::initialize::( - &source_bounds, - &target_bounds, - )?; - - // should work - let source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - let ref_raster: Array2 = open_nc_data("./tests/data/waves_ref.nc")?; - let target_raster = warper.warp_reject_nodata(&source_raster)?; - - assert_eq!(target_raster.shape(), ref_raster.shape()); - Zip::from(&target_raster) - .and(&ref_raster) - .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); - - // should fail - let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - source_raster.slice_mut(s![14..15, 18..19]).fill(f64::NAN); - - let target_raster = warper.warp_reject_nodata(&source_raster); - assert!(target_raster.is_err()); - - #[cfg(feature = "multithreading")] - { - let par_target_raster = warper.warp_reject_nodata_parallel(&source_raster); - assert!(par_target_raster.is_err()); - } - - Ok(()) -} - -#[test] -fn nan_discard() -> Result<()> { - let src_proj = LongitudeLatitude; - let tgt_proj = LambertConformalConic::builder() - .ref_lonlat(80., 24.) - .standard_parallels(12.472955, 35.1728044444444) - .ellipsoid(Ellipsoid::WGS84) - .initialize_projection()?; - - let source_bounds = - RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; - let target_bounds = RasterBoundsDefinition::new( - (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), - (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), - 10_000., - 10_000., - tgt_proj, - )?; - - let warper = Warper::initialize::( - &source_bounds, - &target_bounds, - )?; - - let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); - source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); - source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); - source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); - - let target_raster = warper.warp_discard_nodata(&source_raster)?; - let ref_raster: Array2 = open_nc_data("./tests/data/waves_nan_discard_ref.nc")?; - - assert_eq!(target_raster.shape(), ref_raster.shape()); - Zip::from(&target_raster) - .and(&ref_raster) - .map_collect(|&f, &o| assert_approx_eq!(f64, f, o, epsilon = 1e-6)); - - #[cfg(feature = "multithreading")] - { - let par_target_raster = warper.warp_discard_nodata_parallel(&source_raster)?; - // rasters with nans must be compared this way because for f64 (NaN == NaN) -> false - Zip::from(&target_raster) - .and(&par_target_raster) - .for_each(|&ser, &par| assert_approx_eq!(f64, ser, par)); - } - - Ok(()) -} From aad1994576fe37af9b3426ff66f9607cbcd36f05 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:32:19 +0100 Subject: [PATCH 3/9] create a dedicated test for GH --- tests/aaa_gh_test.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/aaa_gh_test.rs diff --git a/tests/aaa_gh_test.rs b/tests/aaa_gh_test.rs new file mode 100644 index 0000000..b0d4370 --- /dev/null +++ b/tests/aaa_gh_test.rs @@ -0,0 +1,28 @@ +use ndarray::s; + +#[test] +fn gh_test(){ + mut_slice(f64::NAN); + indexing(f64::NAN); + + mut_slice(f64::INFINITY); + indexing(f64::INFINITY); + + panic!(); +} + +fn mut_slice(v: f64) { + let mut arr = ndarray::Array2::::zeros((5,5)); + dbg!(&arr); + + arr.slice_mut(s![0..1, 0..1]).fill(v); + dbg!(&arr); +} + +fn indexing(v: f64) { + let mut arr = ndarray::Array2::::zeros((5,5)); + dbg!(&arr); + + arr[[0,0]] = v; + dbg!(&arr); +} \ No newline at end of file From e94df3560af51da9f0cda411c795a9121ee110d1 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:37:54 +0100 Subject: [PATCH 4/9] add some check to gh test --- tests/aaa_gh_test.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/aaa_gh_test.rs b/tests/aaa_gh_test.rs index b0d4370..657472c 100644 --- a/tests/aaa_gh_test.rs +++ b/tests/aaa_gh_test.rs @@ -14,15 +14,23 @@ fn gh_test(){ fn mut_slice(v: f64) { let mut arr = ndarray::Array2::::zeros((5,5)); dbg!(&arr); + dbg!(&arr[[0,0]].is_nan()); + dbg!(&arr[[0,0]].is_finite()); arr.slice_mut(s![0..1, 0..1]).fill(v); dbg!(&arr); + dbg!(&arr[[0,0]].is_nan()); + dbg!(&arr[[0,0]].is_finite()); } fn indexing(v: f64) { let mut arr = ndarray::Array2::::zeros((5,5)); dbg!(&arr); + dbg!(&arr[[0,0]].is_nan()); + dbg!(&arr[[0,0]].is_finite()); arr[[0,0]] = v; dbg!(&arr); + dbg!(&arr[[0,0]].is_nan()); + dbg!(&arr[[0,0]].is_finite()); } \ No newline at end of file From 0d02bc1163d64da95e23bb2b8fa438d53ab3b566 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 13:46:12 +0100 Subject: [PATCH 5/9] remove gh test, add nan at 0,0 but also remove core::f64, focus on single nan only fo now --- tests/aaa_gh_test.rs | 36 ------------- tests/errcheck.rs | 125 ++++++++++++++++++++++--------------------- 2 files changed, 63 insertions(+), 98 deletions(-) delete mode 100644 tests/aaa_gh_test.rs diff --git a/tests/aaa_gh_test.rs b/tests/aaa_gh_test.rs deleted file mode 100644 index 657472c..0000000 --- a/tests/aaa_gh_test.rs +++ /dev/null @@ -1,36 +0,0 @@ -use ndarray::s; - -#[test] -fn gh_test(){ - mut_slice(f64::NAN); - indexing(f64::NAN); - - mut_slice(f64::INFINITY); - indexing(f64::INFINITY); - - panic!(); -} - -fn mut_slice(v: f64) { - let mut arr = ndarray::Array2::::zeros((5,5)); - dbg!(&arr); - dbg!(&arr[[0,0]].is_nan()); - dbg!(&arr[[0,0]].is_finite()); - - arr.slice_mut(s![0..1, 0..1]).fill(v); - dbg!(&arr); - dbg!(&arr[[0,0]].is_nan()); - dbg!(&arr[[0,0]].is_finite()); -} - -fn indexing(v: f64) { - let mut arr = ndarray::Array2::::zeros((5,5)); - dbg!(&arr); - dbg!(&arr[[0,0]].is_nan()); - dbg!(&arr[[0,0]].is_finite()); - - arr[[0,0]] = v; - dbg!(&arr); - dbg!(&arr[[0,0]].is_nan()); - dbg!(&arr[[0,0]].is_finite()); -} \ No newline at end of file diff --git a/tests/errcheck.rs b/tests/errcheck.rs index 865619d..2785bb2 100644 --- a/tests/errcheck.rs +++ b/tests/errcheck.rs @@ -1,5 +1,3 @@ -use core::f64; - use anyhow::Result; use float_cmp::assert_approx_eq; use mappers::{ @@ -118,67 +116,10 @@ fn single_nan_input_area() -> Result<()> { )?; let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - source_raster.slice_mut(s![14..15, 18..19]).fill(f64::NAN); - - let _ = warper.warp_unchecked(&source_raster); - - let target_raster = warper.warp_reject_nodata(&source_raster).unwrap_err(); - assert!(matches!(target_raster, WarperError::NanError)); - - let target_raster = warper.warp_ignore_nodata(&source_raster); - assert!(target_raster.is_ok()); - - let target_raster = warper.warp_discard_nodata(&source_raster); - assert!(target_raster.is_ok()); - - #[cfg(feature = "multithreading")] - { - let _ = warper.warp_unchecked_parallel(&source_raster); - - let target_raster = warper - .warp_reject_nodata_parallel(&source_raster) - .unwrap_err(); - assert!(matches!(target_raster, WarperError::NanError)); - - let target_raster = warper.warp_ignore_nodata_parallel(&source_raster); - assert!(target_raster.is_ok()); - - let target_raster = warper.warp_discard_nodata_parallel(&source_raster); - assert!(target_raster.is_ok()); - } + // source_raster.slice_mut(s![14..15, 18..19]).fill(f64::NAN); + source_raster[[0,0]] = f64::NAN; - Ok(()) -} - -#[test] -fn multi_nan_input_area() -> Result<()> { - let src_proj = LongitudeLatitude; - let tgt_proj = LambertConformalConic::builder() - .ref_lonlat(80., 24.) - .standard_parallels(12.472955, 35.1728044444444) - .ellipsoid(Ellipsoid::WGS84) - .initialize_projection()?; - - let source_bounds = - RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; - let target_bounds = RasterBoundsDefinition::new( - (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), - (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), - 10_000., - 10_000., - tgt_proj, - )?; - - let warper = Warper::initialize::( - &source_bounds, - &target_bounds, - )?; - - let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); - source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); - source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); - source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); + dbg!(&source_raster); let _ = warper.warp_unchecked(&source_raster); @@ -210,6 +151,66 @@ fn multi_nan_input_area() -> Result<()> { Ok(()) } +// #[test] +// fn multi_nan_input_area() -> Result<()> { +// let src_proj = LongitudeLatitude; +// let tgt_proj = LambertConformalConic::builder() +// .ref_lonlat(80., 24.) +// .standard_parallels(12.472955, 35.1728044444444) +// .ellipsoid(Ellipsoid::WGS84) +// .initialize_projection()?; + +// let source_bounds = +// RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; +// let target_bounds = RasterBoundsDefinition::new( +// (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), +// (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), +// 10_000., +// 10_000., +// tgt_proj, +// )?; + +// let warper = Warper::initialize::( +// &source_bounds, +// &target_bounds, +// )?; + +// let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; +// source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); +// source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); +// source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); +// source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); + +// let _ = warper.warp_unchecked(&source_raster); + +// let target_raster = warper.warp_reject_nodata(&source_raster).unwrap_err(); +// assert!(matches!(target_raster, WarperError::NanError)); + +// let target_raster = warper.warp_ignore_nodata(&source_raster); +// assert!(target_raster.is_ok()); + +// let target_raster = warper.warp_discard_nodata(&source_raster); +// assert!(target_raster.is_ok()); + +// #[cfg(feature = "multithreading")] +// { +// let _ = warper.warp_unchecked_parallel(&source_raster); + +// let target_raster = warper +// .warp_reject_nodata_parallel(&source_raster) +// .unwrap_err(); +// assert!(matches!(target_raster, WarperError::NanError)); + +// let target_raster = warper.warp_ignore_nodata_parallel(&source_raster); +// assert!(target_raster.is_ok()); + +// let target_raster = warper.warp_discard_nodata_parallel(&source_raster); +// assert!(target_raster.is_ok()); +// } + +// Ok(()) +// } + #[test] fn non_finite_result() -> Result<()> { let src_proj = LongitudeLatitude; From 241165fd1b2e121de04cf625c8c4d104e55fa17b Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:36:32 +0100 Subject: [PATCH 6/9] fix that hecking bug: it was caused by some unusual behaviour of rayons .fold() function, which groups values indeterministicly so (for unsure to me reason) it could sometimes weirdly combine the results and effectively miss nans present at some positions in case of serial, the nan check is actually only checking values involved in production of the resulting raster, so some positions around edges will be missed --- src/compute.rs | 34 +++++++------ tests/errcheck.rs | 123 ++++++++++++++++++++++------------------------ 2 files changed, 78 insertions(+), 79 deletions(-) diff --git a/src/compute.rs b/src/compute.rs index 86238f4..2dbc0f5 100644 --- a/src/compute.rs +++ b/src/compute.rs @@ -124,7 +124,11 @@ impl Warper { Ok(target_raster) } - /// This variant throws an error if there's any NaN in the data or result is not finite + /// This variant throws an error if there's any NaN in the (used) input data or result is not finite + /// + /// Note: the input NaN check checks only values that are actually used in computations, + /// so you can pass an input raster with NaNs and get `Ok` result as long as those values + /// do not appear in any computation kernel. pub fn warp_reject_nodata<'a, A: Into>>( &self, source_raster: A, @@ -356,35 +360,33 @@ impl Warper { &self, source_raster: A, ) -> Result, WarperError> { + use ndarray::parallel::prelude::{IntoParallelIterator, ParallelIterator}; + let source_raster: ArrayView2 = source_raster.into(); self.validate_source_raster_shape(&source_raster)?; - Zip::from(&source_raster).par_fold( - || Ok(()), - |_, v| { + Zip::from(&source_raster) + .into_par_iter() + .try_for_each(|(v,)| { if v.is_nan() { Err(WarperError::NanError) } else { Ok(()) } - }, - std::result::Result::and, - )?; + })?; let target_raster = self.warp_unchecked_parallel(source_raster); - Zip::from(&target_raster).par_fold( - || Ok(()), - |_, v| { - if v.is_finite() { - Ok(()) - } else { + Zip::from(&target_raster) + .into_par_iter() + .try_for_each(|(v,)| { + if !v.is_finite() { Err(WarperError::WarpingError) + } else { + Ok(()) } - }, - std::result::Result::and, - )?; + })?; Ok(target_raster) } diff --git a/tests/errcheck.rs b/tests/errcheck.rs index 2785bb2..5931172 100644 --- a/tests/errcheck.rs +++ b/tests/errcheck.rs @@ -116,10 +116,7 @@ fn single_nan_input_area() -> Result<()> { )?; let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; - // source_raster.slice_mut(s![14..15, 18..19]).fill(f64::NAN); - source_raster[[0,0]] = f64::NAN; - - dbg!(&source_raster); + source_raster.slice_mut(s![14..15, 18..19]).fill(f64::NAN); let _ = warper.warp_unchecked(&source_raster); @@ -151,65 +148,65 @@ fn single_nan_input_area() -> Result<()> { Ok(()) } -// #[test] -// fn multi_nan_input_area() -> Result<()> { -// let src_proj = LongitudeLatitude; -// let tgt_proj = LambertConformalConic::builder() -// .ref_lonlat(80., 24.) -// .standard_parallels(12.472955, 35.1728044444444) -// .ellipsoid(Ellipsoid::WGS84) -// .initialize_projection()?; - -// let source_bounds = -// RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; -// let target_bounds = RasterBoundsDefinition::new( -// (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), -// (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), -// 10_000., -// 10_000., -// tgt_proj, -// )?; - -// let warper = Warper::initialize::( -// &source_bounds, -// &target_bounds, -// )?; - -// let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; -// source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); -// source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); -// source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); -// source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); - -// let _ = warper.warp_unchecked(&source_raster); - -// let target_raster = warper.warp_reject_nodata(&source_raster).unwrap_err(); -// assert!(matches!(target_raster, WarperError::NanError)); - -// let target_raster = warper.warp_ignore_nodata(&source_raster); -// assert!(target_raster.is_ok()); - -// let target_raster = warper.warp_discard_nodata(&source_raster); -// assert!(target_raster.is_ok()); - -// #[cfg(feature = "multithreading")] -// { -// let _ = warper.warp_unchecked_parallel(&source_raster); - -// let target_raster = warper -// .warp_reject_nodata_parallel(&source_raster) -// .unwrap_err(); -// assert!(matches!(target_raster, WarperError::NanError)); - -// let target_raster = warper.warp_ignore_nodata_parallel(&source_raster); -// assert!(target_raster.is_ok()); - -// let target_raster = warper.warp_discard_nodata_parallel(&source_raster); -// assert!(target_raster.is_ok()); -// } - -// Ok(()) -// } +#[test] +fn multi_nan_input_area() -> Result<()> { + let src_proj = LongitudeLatitude; + let tgt_proj = LambertConformalConic::builder() + .ref_lonlat(80., 24.) + .standard_parallels(12.472955, 35.1728044444444) + .ellipsoid(Ellipsoid::WGS84) + .initialize_projection()?; + + let source_bounds = + RasterBoundsDefinition::new((60.00, 68.25), (31.75, 40.0), 0.25, 0.25, src_proj)?; + let target_bounds = RasterBoundsDefinition::new( + (2_320_000. - 4_000_000., 2_740_000. - 4_000_000.), + (5_090_000. - 4_000_000., 5_640_000. - 4_000_000.), + 10_000., + 10_000., + tgt_proj, + )?; + + let warper = Warper::initialize::( + &source_bounds, + &target_bounds, + )?; + + let mut source_raster: Array2 = open_nc_data("./tests/data/waves_34.nc")?; + source_raster.slice_mut(s![13..15, 13..15]).fill(f64::NAN); + source_raster.slice_mut(s![22..24, 18..20]).fill(f64::NAN); + source_raster.slice_mut(s![18..25, 19..24]).fill(f64::NAN); + source_raster.slice_mut(s![13..15, 21..23]).fill(f64::NAN); + + let _ = warper.warp_unchecked(&source_raster); + + let target_raster = warper.warp_reject_nodata(&source_raster).unwrap_err(); + assert!(matches!(target_raster, WarperError::NanError)); + + let target_raster = warper.warp_ignore_nodata(&source_raster); + assert!(target_raster.is_ok()); + + let target_raster = warper.warp_discard_nodata(&source_raster); + assert!(target_raster.is_ok()); + + #[cfg(feature = "multithreading")] + { + let _ = warper.warp_unchecked_parallel(&source_raster); + + let target_raster = warper + .warp_reject_nodata_parallel(&source_raster) + .unwrap_err(); + assert!(matches!(target_raster, WarperError::NanError)); + + let target_raster = warper.warp_ignore_nodata_parallel(&source_raster); + assert!(target_raster.is_ok()); + + let target_raster = warper.warp_discard_nodata_parallel(&source_raster); + assert!(target_raster.is_ok()); + } + + Ok(()) +} #[test] fn non_finite_result() -> Result<()> { From 192e8a7932d04b850ee5a1452f63835c7c4b666a Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:37:03 +0100 Subject: [PATCH 7/9] remove leftover debug macro --- tests/general.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/general.rs b/tests/general.rs index f5fdba5..6eb9e51 100644 --- a/tests/general.rs +++ b/tests/general.rs @@ -110,9 +110,6 @@ fn mitchell() -> Result<()> { target_raster.iter().for_each(|&v| assert!(v.is_finite())); - dbg!(target_raster.max()?); - dbg!(source_raster.max()?); - assert!(target_raster.max()? <= source_raster.max()?); assert!(target_raster.min()? >= source_raster.min()?); From 3c60057f992c60769ebbf9e972e6ad7a8e061e21 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:40:39 +0100 Subject: [PATCH 8/9] replace all problematic par_fold uses with try_for_each --- src/compute.rs | 162 +++++++++++++++++++++++----------------------- src/precompute.rs | 17 +++-- 2 files changed, 90 insertions(+), 89 deletions(-) diff --git a/src/compute.rs b/src/compute.rs index 2dbc0f5..ba8777a 100644 --- a/src/compute.rs +++ b/src/compute.rs @@ -287,13 +287,15 @@ impl Warper { } /// This implementation catches warp operation producing NaNs (that is nans resulting from computation error - /// not those resulting from nodata), but does not early return. + /// not those resulting from nodata) #[cfg(feature = "multithreading")] #[cfg_attr(docsrs, doc(cfg(feature = "multithreading")))] pub fn warp_ignore_nodata_parallel<'a, A: Into>>( &self, source_raster: A, ) -> Result, WarperError> { + use ndarray::parallel::prelude::{IntoParallelIterator, ParallelIterator}; + let source_raster: ArrayView2 = source_raster.into(); self.validate_source_raster_shape(&source_raster)?; @@ -302,53 +304,50 @@ impl Warper { Zip::from(&mut target_raster) .and(&self.internals) - .par_fold( - || Ok(()), - |_, v, intr| { - let values = source_raster.slice(s![ - (intr.anchor_idx.1 - 1)..(intr.anchor_idx.1 + 3), - (intr.anchor_idx.0 - 1)..(intr.anchor_idx.0 + 3) - ]); - - let mut weight_accum = 0.0; - let mut result_accum = 0.0; - - for j in 0..4 { - let mut inner_weight_accum = 0.0; - let mut inner_result_accum = 0.0; - - for i in 0..4 { - let value = values[[j, i]]; - - if !value.is_nan() { - let x_weight = intr.x_weights[i]; - inner_weight_accum += x_weight; - inner_result_accum += x_weight * value; - } - } + .into_par_iter() + .try_for_each(|(v, intr)| { + let values = source_raster.slice(s![ + (intr.anchor_idx.1 - 1)..(intr.anchor_idx.1 + 3), + (intr.anchor_idx.0 - 1)..(intr.anchor_idx.0 + 3) + ]); - let y_weight = intr.y_weights[j]; + let mut weight_accum = 0.0; + let mut result_accum = 0.0; - weight_accum += inner_weight_accum * y_weight; - result_accum += inner_result_accum * y_weight; - } + for j in 0..4 { + let mut inner_weight_accum = 0.0; + let mut inner_result_accum = 0.0; + + for i in 0..4 { + let value = values[[j, i]]; - if (weight_accum).abs() < f64::EPSILON { - *v = f64::NAN; - return Ok(()); + if !value.is_nan() { + let x_weight = intr.x_weights[i]; + inner_weight_accum += x_weight; + inner_result_accum += x_weight * value; + } } - let result = result_accum / weight_accum; + let y_weight = intr.y_weights[j]; - if result.is_finite() { - *v = result; - Ok(()) - } else { - Err(WarperError::WarpingError) - } - }, - std::result::Result::and, - )?; + weight_accum += inner_weight_accum * y_weight; + result_accum += inner_result_accum * y_weight; + } + + if (weight_accum).abs() < f64::EPSILON { + *v = f64::NAN; + return Ok(()); + } + + let result = result_accum / weight_accum; + + if result.is_finite() { + *v = result; + Ok(()) + } else { + Err(WarperError::WarpingError) + } + })?; Ok(target_raster) } @@ -392,13 +391,15 @@ impl Warper { } /// This implementation catches warp operation producing NaNs (that is nans resulting from computation error - /// not those resulting from nodata), but does not early return. + /// not those resulting from nodata) #[cfg(feature = "multithreading")] #[cfg_attr(docsrs, doc(cfg(feature = "multithreading")))] pub fn warp_discard_nodata_parallel<'a, A: Into>>( &self, source_raster: A, ) -> Result, WarperError> { + use ndarray::parallel::prelude::{IntoParallelIterator, ParallelIterator}; + let source_raster: ArrayView2 = source_raster.into(); self.validate_source_raster_shape(&source_raster)?; @@ -407,50 +408,47 @@ impl Warper { Zip::from(&mut target_raster) .and(&self.internals) - .par_fold( - || Ok(()), - |_, v, intr| { - let values = source_raster.slice(s![ - (intr.anchor_idx.1 - 1)..(intr.anchor_idx.1 + 3), - (intr.anchor_idx.0 - 1)..(intr.anchor_idx.0 + 3) - ]); - - let mut weight_accum = 0.0; - let mut result_accum = 0.0; - - for j in 0..4 { - let mut inner_weight_accum = 0.0; - let mut inner_result_accum = 0.0; - - for i in 0..4 { - let value = values[[j, i]]; - - if value.is_nan() { - *v = f64::NAN; - return Ok(()); - } - let x_weight = intr.x_weights[i]; - inner_weight_accum += x_weight; - inner_result_accum += x_weight * value; - } + .into_par_iter() + .try_for_each(|(v, intr)| { + let values = source_raster.slice(s![ + (intr.anchor_idx.1 - 1)..(intr.anchor_idx.1 + 3), + (intr.anchor_idx.0 - 1)..(intr.anchor_idx.0 + 3) + ]); - let y_weight = intr.y_weights[j]; + let mut weight_accum = 0.0; + let mut result_accum = 0.0; - weight_accum += inner_weight_accum * y_weight; - result_accum += inner_result_accum * y_weight; - } + for j in 0..4 { + let mut inner_weight_accum = 0.0; + let mut inner_result_accum = 0.0; - let result = result_accum / weight_accum; + for i in 0..4 { + let value = values[[j, i]]; - if result.is_finite() { - *v = result; - Ok(()) - } else { - Err(WarperError::WarpingError) + if value.is_nan() { + *v = f64::NAN; + return Ok(()); + } + let x_weight = intr.x_weights[i]; + inner_weight_accum += x_weight; + inner_result_accum += x_weight * value; } - }, - std::result::Result::and, - )?; + + let y_weight = intr.y_weights[j]; + + weight_accum += inner_weight_accum * y_weight; + result_accum += inner_result_accum * y_weight; + } + + let result = result_accum / weight_accum; + + if result.is_finite() { + *v = result; + Ok(()) + } else { + Err(WarperError::WarpingError) + } + })?; Ok(target_raster) } diff --git a/src/precompute.rs b/src/precompute.rs index c3cc544..65b6cc9 100644 --- a/src/precompute.rs +++ b/src/precompute.rs @@ -61,7 +61,10 @@ pub fn precompute_ixs_jys_parallel( source_bounds: &RasterBounds, target_bounds: &RasterBounds, ) -> Result, WarperError> { - use ndarray::Zip; + use ndarray::{ + Zip, + parallel::prelude::{IntoParallelIterator, ParallelIterator}, + }; let tgt_ul_edge_corner = SourceXYPair { x: 0.5f64.mul_add(-target_bounds.spacing.x, target_bounds.min.x), @@ -98,17 +101,17 @@ pub fn precompute_ixs_jys_parallel( ) }); - Zip::from(&precomputed_coords).par_fold( - || Ok(()), - |_, v| { + // ndarray uses into_par_iter() under the hood so we are not loosing performance + // by going to rayon here, possibly even gaining something + Zip::from(&precomputed_coords) + .into_par_iter() + .try_for_each(|(v,)| { if !v.ix.is_finite() || !v.jy.is_finite() { Err(WarperError::ConversionError) } else { Ok(()) } - }, - std::result::Result::and, - )?; + })?; Ok(precomputed_coords) } From 6614a2adbba46b547edc855f8d093d6826c0b410 Mon Sep 17 00:00:00 2001 From: Quba1 <22771850+Quba1@users.noreply.github.com> Date: Thu, 15 Jan 2026 14:44:01 +0100 Subject: [PATCH 9/9] clippy fix --- src/compute.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compute.rs b/src/compute.rs index ba8777a..77f5e1b 100644 --- a/src/compute.rs +++ b/src/compute.rs @@ -380,10 +380,10 @@ impl Warper { Zip::from(&target_raster) .into_par_iter() .try_for_each(|(v,)| { - if !v.is_finite() { - Err(WarperError::WarpingError) - } else { + if v.is_finite() { Ok(()) + } else { + Err(WarperError::WarpingError) } })?;