Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions lib/iris/analysis/cartography.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
53 changes: 53 additions & 0 deletions lib/iris/tests/unit/analysis/cartography/test_wrap_lons.py
Original file line number Diff line number Diff line change
@@ -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])
Loading