From e4aab7263db402d1acde396e696df7c75789cb2f Mon Sep 17 00:00:00 2001 From: Vincent Gao Date: Mon, 1 Jun 2026 13:36:55 +0200 Subject: [PATCH] Preserve floating-point dtype in wrap_lons iris.analysis.cartography.wrap_lons always cast its result to float64, so float32 (or float16) longitudes were silently promoted to float64. Preserve the original dtype for floating-point inputs while still returning float64 for integer inputs (matching the documented example), and keep laziness and masking intact. Fixes #4119. --- docs/src/whatsnew/latest.rst | 5 +- lib/iris/analysis/cartography.py | 10 +++- .../analysis/cartography/test_wrap_lons.py | 53 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 lib/iris/tests/unit/analysis/cartography/test_wrap_lons.py diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 825508dca3..be2fd9b0d1 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -36,7 +36,10 @@ This document explains the changes made to Iris for this release 🐛 Bugs Fixed ============= -#. N/A +#. :user:`gaoflow` fixed :func:`iris.analysis.cartography.wrap_lons` so that it + preserves the floating-point dtype of its input (e.g. ``float32``) instead of + always promoting the result to ``float64``. Integer inputs are still returned + as ``float64``. (:issue:`4119`) 💣 Incompatible Changes diff --git a/lib/iris/analysis/cartography.py b/lib/iris/analysis/cartography.py index e88711d51f..c6ce8faf54 100644 --- a/lib/iris/analysis/cartography.py +++ b/lib/iris/analysis/cartography.py @@ -84,9 +84,15 @@ def wrap_lons(lons, base, period): See more at :doc:`/user_manual/explanation/real_and_lazy_data`. """ # It is important to use 64bit floating precision when changing a floats - # numbers range. + # numbers range, but the original floating-point dtype is preserved so that + # e.g. float32 longitudes are not promoted to float64 (see #4119). Integer + # (and other non-floating) inputs still return float64. + orig_dtype = lons.dtype lons = lons.astype(np.float64) - return ((lons - base) % period) + base + result = ((lons - base) % period) + base + if orig_dtype.kind == "f": + result = result.astype(orig_dtype) + return result def unrotate_pole(rotated_lons, rotated_lats, pole_lon, pole_lat): diff --git a/lib/iris/tests/unit/analysis/cartography/test_wrap_lons.py b/lib/iris/tests/unit/analysis/cartography/test_wrap_lons.py new file mode 100644 index 0000000000..8b7632ddf5 --- /dev/null +++ b/lib/iris/tests/unit/analysis/cartography/test_wrap_lons.py @@ -0,0 +1,53 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the BSD license. +# See LICENSE in the root of the repository for full licensing details. + +"""Unit tests for :func:`iris.analysis.cartography.wrap_lons`.""" + +import dask.array as da +import numpy as np +import pytest + +from iris.analysis.cartography import wrap_lons + + +class Test: + def test_values(self): + # The documented behaviour (matches the docstring example). + result = wrap_lons(np.array([185, 30, -200, 75]), -180, 360) + np.testing.assert_array_equal(result, [-175.0, 30.0, 160.0, 75.0]) + + @pytest.mark.parametrize("dtype", [np.float16, np.float32, np.float64]) + def test_floating_dtype_preserved(self, dtype): + # A floating-point input keeps its dtype rather than being promoted to + # float64 (see #4119). + lons = np.array([185, 30, -200, 75], dtype=dtype) + result = wrap_lons(lons, -180, 360) + assert result.dtype == dtype + np.testing.assert_array_equal(result, [-175.0, 30.0, 160.0, 75.0]) + + @pytest.mark.parametrize("dtype", [np.int32, np.int64]) + def test_integer_dtype_returns_float64(self, dtype): + # Integer (and other non-floating) inputs are still returned as float64, + # because wrapping a discrete range generally yields fractional results. + lons = np.array([185, 30, -200, 75], dtype=dtype) + result = wrap_lons(lons, -180, 360) + assert result.dtype == np.float64 + np.testing.assert_array_equal(result, [-175.0, 30.0, 160.0, 75.0]) + + def test_masked_array_preserved(self): + lons = np.ma.masked_array( + [185.0, 30.0, -200.0, 75.0], mask=[0, 1, 0, 0], dtype=np.float32 + ) + result = wrap_lons(lons, -180, 360) + assert isinstance(result, np.ma.MaskedArray) + assert result.dtype == np.float32 + np.testing.assert_array_equal(result.mask, [False, True, False, False]) + + def test_lazy_input_stays_lazy(self): + lons = da.from_array(np.array([185, 30, -200, 75], dtype=np.float32)) + result = wrap_lons(lons, -180, 360) + assert isinstance(result, da.Array) + assert result.dtype == np.float32 + np.testing.assert_array_equal(result.compute(), [-175.0, 30.0, 160.0, 75.0])