diff --git a/Changelog.rst b/Changelog.rst index a1589f3cfb..2bee5eaa55 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -1,3 +1,14 @@ +Version NEXTVERSION +-------------- + +**2025-??-??** + +* New methods to allow changing units in a chain: `cf.Field.to_units`, + `cf.Data.to_units` + (https://github.com/NCAS-CMS/cf-python/issues/874) + +---- + Version 3.18.0 -------------- diff --git a/cf/data/data.py b/cf/data/data.py index 7fcd08024f..cf4b50c167 100644 --- a/cf/data/data.py +++ b/cf/data/data.py @@ -5651,13 +5651,21 @@ def change_calendar(self, calendar, inplace=False, i=False): @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def override_units(self, units, inplace=False, i=False): - """Override the data array units. + """Override the units. - Not to be confused with setting the `Units` attribute to units - which are equivalent to the original units. This is different - because in this case the new units need not be equivalent to the - original ones and the data array elements will not be changed to - reflect the new units. + The new units need not be equivalent to the original ones, and + the data array elements will not be changed to reflect the new + units. Therefore, this method should only be used when it is + known that the data array values are correct but the units + have incorrectly encoded. + + Not to be confused with changing to equivalent units with the + `to_units` method or the `Units`, `units`, or `calendar` + attributes. These approaches also convert the data to have the + new units. + + .. seealso:: `override_calendar`, `to_units`, `Units`, + `units`, `calendar` :Parameters: @@ -5695,13 +5703,20 @@ def override_units(self, units, inplace=False, i=False): @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def override_calendar(self, calendar, inplace=False, i=False): - """Override the calendar of the data array elements. + """Override the calendar of date-time units. + + The new calendar need not be equivalent to the original one, + and the data array elements will not be changed to reflect the + new calendar. Therefore, this method should only be used when + it is known that the data array values are correct but the + calendar has been incorrectly encoded. + + Not to be confused with changing to an equivalent calendar + with the `to_units` method or the `Units` or `calendar` + attributes. - Not to be confused with using the `change_calendar` method or - setting the `d.Units.calendar`. `override_calendar` is different - because the new calendar need not be equivalent to the original - ones and the data array elements will not be changed to reflect - the new units. + .. seealso:: `override_units`, `to_units`, `Units`, `units`, + `calendar` :Parameters: @@ -7365,6 +7380,57 @@ def to_memory(self): "Consider using 'Data.persist' instead." ) + @_inplace_enabled(default=False) + def to_units(self, units, inplace=False): + """Change the data array units. + + Changing the units causes the data values to be changed + to match the new units, therefore the new units must be + equivalent to the existing ones. + + Not to be confused with overriding the units with + `override_units` + + .. versionadded:: NEXTVERSION + + .. seealso:: `override_units`, `override_calendar`, `Units`, + `units`, `calendar` + + :Parameters: + + units: `str` or `Units` + The new units for the data array. + + {{inplace: `bool`, optional}} + + :Returns: + + `Data` or `None` + The new data, or `None` if the operation was in-place. + + **Examples** + + >>> d = cf.Data([1, 2], 'km') + >>> e = d.to_units('metre') + >>> print(e.Units) + 'metre' + >>> print(e.array) + [1000. 2000.] + >>> e.to_units('miles', inplace=True) + >>> print(e.Units) + 'miles' + >>> print(e.array) + [0.62137119 1.24274238] + >>> e.to_units('degC') + Traceback (most recent call last) + ... + ValueError: Can't set Units to that are not equivalent to the current units . Consider using the override_units method instead. + + """ + d = _inplace_enabled_define_and_cleanup(self) + d.Units = self._Units_class(units) + return d + @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def trunc(self, inplace=False, i=False): diff --git a/cf/mixin/propertiesdata.py b/cf/mixin/propertiesdata.py index 158bd2dc17..e58ee361c9 100644 --- a/cf/mixin/propertiesdata.py +++ b/cf/mixin/propertiesdata.py @@ -4431,6 +4431,71 @@ def to_dask_array(self): return data.to_dask_array() + @_inplace_enabled(default=False) + def to_units(self, units, inplace=False): + """Change the data array units. + + Changing the units causes the data values to be changed + to match the new units, therefore the new units must be + equivalent to the existing ones. + + Not to be confused with overriding the units with + `override_units` + + .. versionadded:: NEXTVERSION + + .. seealso:: `override_units`, `override_calendar`, `Units`, + `units`, `calendar` + + :Parameters: + + units: `str` or `Units` + The new units for the data array. + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + The construct with data converted to the new units, or + `None` if the operation was in-place. + + **Examples** + + >>> print(f.Units) + 'km' + >>> print(f.array) + [1 2] + >>> g = f.to_units('metre') + >>> print(g.Units) + 'metre' + >>> print(g.array) + [1000. 2000.] + >>> g.to_units('miles', inplace=True) + >>> print(g.array) + [0.62137119 1.24274238] + >>> g.to_units('degC') + Traceback (most recent call last) + ... + ValueError: Can't set Units to that are not equivalent to the current units . Consider using the override_units method instead. + + """ + f = _inplace_enabled_define_and_cleanup(self) + + data = f.get_data(None) + if data is not None: + data.to_units(units, inplace=True) + f.set_data(data, copy=False) + else: + f._custom["Units"] = Units(units) + + # Change the Units on the period + period = f.period() + if period is not None: + f.period(period=period.to_units(units)) + + return f + @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def trunc(self, inplace=False, i=False): @@ -4954,15 +5019,16 @@ def override_calendar(self, calendar, inplace=False, i=False): The new calendar need not be equivalent to the original one, and the data array elements will not be changed to reflect the - new units. Therefore, this method should only be used when it - is known that the data array values are correct but the + new calendar. Therefore, this method should only be used when + it is known that the data array values are correct but the calendar has been incorrectly encoded. - Not to be confused with setting the `calendar` or `Units` - attributes to a calendar which is equivalent to the original - calendar + Not to be confused with changing to an equivalent calendar + with the `to_units` method or the `Units` or `calendar` + attributes. - .. seealso:: `calendar`, `override_units`, `units`, `Units` + .. seealso:: `override_units`, `to_units`, `units`, `Units`, + `calendar` :Parameters: @@ -4976,13 +5042,20 @@ def override_calendar(self, calendar, inplace=False, i=False): :Returns: `{{class}}` or `None` - TODO + The construct with data converted to the new units, or + `None` if the operation was in-place. **Examples** - TODO - + >>> f.Units + + >>> print(f.array) + 1 >>> g = f.override_calendar('noleap') + >>> g.Units + + >>> print(f.array) + 1 """ v = _inplace_enabled_define_and_cleanup(self) @@ -5015,11 +5088,13 @@ def override_units(self, units, inplace=False, i=False): known that the data array values are correct but the units have incorrectly encoded. - Not to be confused with setting the `units` or `Units` - attributes to units which are equivalent to the original - units. + Not to be confused with changing to equivalent units with the + `to_units` method or the `Units`, `units`, or `calendar` + attributes. These approaches also convert the data to have the + new units. - .. seealso:: `calendar`, `override_calendar`, `units`, `Units` + .. seealso:: `override_calendar`, `to_units`, `units`, + `Units`,`calendar` :Parameters: @@ -5033,8 +5108,8 @@ def override_units(self, units, inplace=False, i=False): :Returns: `{{class}}` or `None` - - TODO + The construct with data converted to the new units, or + `None` if the operation was in-place. **Examples** diff --git a/cf/mixin/propertiesdatabounds.py b/cf/mixin/propertiesdatabounds.py index 21e8b9f803..33c2df6d40 100644 --- a/cf/mixin/propertiesdatabounds.py +++ b/cf/mixin/propertiesdatabounds.py @@ -2182,17 +2182,18 @@ def match_by_identity(self, *identities): def override_calendar(self, calendar, inplace=False, i=False): """Override the calendar of date-time units. - The new calendar **need not** be equivalent to the original one - and the data array elements will not be changed to reflect the new - units. Therefore, this method should only be used when it is known - that the data array values are correct but the calendar has been - incorrectly encoded. + The new calendar need not be equivalent to the original one, + and the data array elements will not be changed to reflect the + new calendar. Therefore, this method should only be used when + it is known that the data array values are correct but the + calendar has been incorrectly encoded. - Not to be confused with setting the `calendar` or `Units` - attributes to a calendar which is equivalent to the original - calendar + Not to be confused with changing to an equivalent calendar + with the `to_units` method or the `Units` or `calendar` + attributes. - .. seealso:: `calendar`, `override_units`, `units`, `Units` + .. seealso:: `override_units`, `to_units`, `Units`, `units`, + `calendar` :Parameters: @@ -2205,13 +2206,21 @@ def override_calendar(self, calendar, inplace=False, i=False): :Returns: - TODO + `{{class}}` or `None` + The construct with data converted to the new units, or + `None` if the operation was in-place. **Examples** - TODO - + >>> f.Units + + >>> print(f.array) + 1 >>> g = f.override_calendar('noleap') + >>> g.Units + + >>> print(f.array) + 1 """ return self._apply_superclass_data_oper( @@ -2229,16 +2238,19 @@ def override_calendar(self, calendar, inplace=False, i=False): def override_units(self, units, inplace=False, i=False): """Override the units. - The new units need not be equivalent to the original ones, and the - data array elements will not be changed to reflect the new - units. Therefore, this method should only be used when it is known - that the data array values are correct but the units have - incorrectly encoded. + The new units need not be equivalent to the original ones, and + the data array elements will not be changed to reflect the new + units. Therefore, this method should only be used when it is + known that the data array values are correct but the units + have incorrectly encoded. - Not to be confused with setting the `units` or `Units` attribute - to units which are equivalent to the original units. + Not to be confused with changing to equivalent units with the + `to_units` method or the `Units`, `units`, or `calendar` + attributes. These approaches also convert the data values to conform + with the new units. - .. seealso:: `calendar`, `override_calendar`, `units`, `Units` + .. seealso:: `override_calendar`, `to_units`, `Units`, + `units`, `calendar` :Parameters: @@ -2251,7 +2263,9 @@ def override_units(self, units, inplace=False, i=False): :Returns: - TODO + `{{class}}` or `None` + The construct with data converted to the new units, or + `None` if the operation was in-place. **Examples** @@ -3306,6 +3320,64 @@ def log(self, base=None, bounds=True, inplace=False, i=False): i=i, ) + @_inplace_enabled(default=False) + def to_units(self, units, inplace=False): + """Change the data array units. + + Changing the units causes the data values to be changed + to match the new units, therefore the new units must be + equivalent to the existing ones. + + Not to be confused with overriding the units with + `override_units` + + .. versionadded:: NEXTVERSION + + .. seealso:: `override_units`, `override_calendar`, `Units`, + `units`, `calendar` + + :Parameters: + + units: `str` or `Units` + The new units for the data array. + + {{inplace: `bool`, optional}} + + :Returns: + + `{{class}}` or `None` + The construct with data converted to the new units, or + `None` if the operation was in-place. + + **Examples** + + >>> print(f.Units) + 'km' + >>> print(f.array) + [1 2] + >>> g = f.to_units('metre') + >>> print(g.Units) + 'metre' + >>> print(g.array) + [1000. 2000.] + >>> g.to_units('miles', inplace=True) + >>> print(g.array) + [0.62137119 1.24274238] + >>> g.to_units('degC') + Traceback (most recent call last) + ... + ValueError: Can't set Units to that are not equivalent to the current units . Consider using the override_units method instead. + + """ + return self._apply_superclass_data_oper( + _inplace_enabled_define_and_cleanup(self), + "to_units", + (units,), + bounds=True, + interior_ring=False, + inplace=inplace, + ) + @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def trunc(self, bounds=True, inplace=False, i=False): diff --git a/cf/test/test_Data.py b/cf/test/test_Data.py index 026ad9e2c8..5918fb79b6 100644 --- a/cf/test/test_Data.py +++ b/cf/test/test_Data.py @@ -4684,6 +4684,23 @@ def test_Data_collapse_axes_hdf_chunks(self): self.assertEqual(e._axes, d._axes[1:]) self.assertEqual(e.nc_dataset_chunksizes(), chunks) + def test_Data_to_units(self): + """Test cf.Data.to_units.""" + d = cf.Data([1, 2], "km") + e = d.to_units("m") + + self.assertIsInstance(e, d.__class__) + self.assertEqual(e.Units, cf.Units("m")) + self.assertTrue(np.allclose(e.array, [1000.0, 2000.0])) + + self.assertIsNone(e.to_units("miles", inplace=True)) + self.assertEqual(e.Units, cf.Units("miles")) + self.assertTrue(np.allclose(e.array, [0.62137119, 1.24274238])) + + # Non-equivalent units + with self.assertRaises(ValueError): + e.to_units("degC") + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_DimensionCoordinate.py b/cf/test/test_DimensionCoordinate.py index 3fbcb22a92..c727e2d1e0 100644 --- a/cf/test/test_DimensionCoordinate.py +++ b/cf/test/test_DimensionCoordinate.py @@ -840,6 +840,28 @@ def test_DimensionCoordinate_direction(self): d._custom["direction"] = None self.assertTrue(d[0].direction()) + def test_DimensionCoordinate_to_units(self): + """Test DimensionCoordinate.to_units.""" + f = cf.example_field(0) + d = f.dimension_coordinate("X") + d = d[:2] + self.assertEqual(d.period().Units, cf.Units("degrees_east")) + + e = d.to_units("rad") + self.assertIsInstance(e, d.__class__) + self.assertEqual(e.Units, cf.Units("rad")) + self.assertTrue(np.allclose(e.array, [0.39269908, 1.17809725])) + self.assertEqual(e.period().Units, cf.Units("rad")) + + self.assertIsNone(e.to_units("degrees_east", inplace=True)) + self.assertEqual(e.Units, cf.Units("degrees_east")) + self.assertTrue(np.allclose(e.array, [22.5, 67.5])) + self.assertTrue(np.allclose(e.period().array, d.period().array)) + + # Non-equivalent units + with self.assertRaises(ValueError): + e.to_units("degC") + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/cf/test/test_Field.py b/cf/test/test_Field.py index 03f87c7a46..3e0f74979f 100644 --- a/cf/test/test_Field.py +++ b/cf/test/test_Field.py @@ -3035,6 +3035,24 @@ def test_Field_filled(self): self.assertEqual(values[0], -999) self.assertEqual(counts[0], 5) + def test_Field_to_units(self): + """Test Field.to_units.""" + f = cf.example_field(0) + f = f[0, :2] + + g = f.to_units("g/kg") + self.assertIsInstance(f, g.__class__) + self.assertEqual(g.Units, cf.Units("g/kg")) + self.assertTrue(np.allclose(g.array, [7.0, 34.0])) + + self.assertIsNone(g.to_units("1", inplace=True)) + self.assertEqual(g.Units, cf.Units("1")) + self.assertTrue(np.allclose(g.array, [0.007, 0.034])) + + # Non-equivalent units + with self.assertRaises(ValueError): + g.to_units("degC") + if __name__ == "__main__": print("Run date:", datetime.datetime.now()) diff --git a/docs/source/class/cf.AuxiliaryCoordinate.rst b/docs/source/class/cf.AuxiliaryCoordinate.rst index 6216de643f..2e7c8bf0b0 100644 --- a/docs/source/class/cf.AuxiliaryCoordinate.rst +++ b/docs/source/class/cf.AuxiliaryCoordinate.rst @@ -192,6 +192,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.AuxiliaryCoordinate.to_units ~cf.AuxiliaryCoordinate.override_units ~cf.AuxiliaryCoordinate.override_calendar diff --git a/docs/source/class/cf.Bounds.rst b/docs/source/class/cf.Bounds.rst index 27a29b1010..4e1601b16a 100644 --- a/docs/source/class/cf.Bounds.rst +++ b/docs/source/class/cf.Bounds.rst @@ -106,6 +106,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.Bounds.to_units ~cf.Bounds.override_units ~cf.Bounds.override_calendar diff --git a/docs/source/class/cf.CellConnectivity.rst b/docs/source/class/cf.CellConnectivity.rst index 791479de51..b7e4db1270 100644 --- a/docs/source/class/cf.CellConnectivity.rst +++ b/docs/source/class/cf.CellConnectivity.rst @@ -129,6 +129,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.CellConnectivity.to_units ~cf.CellConnectivity.override_units ~cf.CellConnectivity.override_calendar diff --git a/docs/source/class/cf.CellMeasure.rst b/docs/source/class/cf.CellMeasure.rst index 8e61434557..f10d0e2f6b 100644 --- a/docs/source/class/cf.CellMeasure.rst +++ b/docs/source/class/cf.CellMeasure.rst @@ -129,6 +129,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.CellMeasure.to_units ~cf.CellMeasure.override_units ~cf.CellMeasure.override_calendar diff --git a/docs/source/class/cf.Count.rst b/docs/source/class/cf.Count.rst index 27d04c6cf0..8431fb1c45 100644 --- a/docs/source/class/cf.Count.rst +++ b/docs/source/class/cf.Count.rst @@ -102,6 +102,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.Count.to_units ~cf.Count.override_units ~cf.Count.override_calendar diff --git a/docs/source/class/cf.Data.rst b/docs/source/class/cf.Data.rst index 488ea75df8..b89f262de9 100644 --- a/docs/source/class/cf.Data.rst +++ b/docs/source/class/cf.Data.rst @@ -42,6 +42,7 @@ Units ~cf.Data.get_units ~cf.Data.has_units ~cf.Data.set_units + ~cf.Data.to_units ~cf.Data.override_units ~cf.Data.del_calendar ~cf.Data.get_calendar diff --git a/docs/source/class/cf.DimensionCoordinate.rst b/docs/source/class/cf.DimensionCoordinate.rst index 7defc250e6..1878f43de2 100644 --- a/docs/source/class/cf.DimensionCoordinate.rst +++ b/docs/source/class/cf.DimensionCoordinate.rst @@ -196,6 +196,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.DimensionCoordinate.to_units ~cf.DimensionCoordinate.override_units ~cf.DimensionCoordinate.override_calendar diff --git a/docs/source/class/cf.DomainAncillary.rst b/docs/source/class/cf.DomainAncillary.rst index 61d8f795e6..4d4edba246 100644 --- a/docs/source/class/cf.DomainAncillary.rst +++ b/docs/source/class/cf.DomainAncillary.rst @@ -175,6 +175,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.DomainAncillary.to_units ~cf.DomainAncillary.override_units ~cf.DomainAncillary.override_calendar diff --git a/docs/source/class/cf.DomainTopology.rst b/docs/source/class/cf.DomainTopology.rst index a651bd72ad..2002c849d1 100644 --- a/docs/source/class/cf.DomainTopology.rst +++ b/docs/source/class/cf.DomainTopology.rst @@ -130,6 +130,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.DomainTopology.to_units ~cf.DomainTopology.override_units ~cf.DomainTopology.override_calendar diff --git a/docs/source/class/cf.Field.rst b/docs/source/class/cf.Field.rst index 8183b61434..1c307f362e 100644 --- a/docs/source/class/cf.Field.rst +++ b/docs/source/class/cf.Field.rst @@ -122,6 +122,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.Field.to_units ~cf.Field.override_units ~cf.Field.override_calendar diff --git a/docs/source/class/cf.FieldAncillary.rst b/docs/source/class/cf.FieldAncillary.rst index 62bdbb2381..34271875f6 100644 --- a/docs/source/class/cf.FieldAncillary.rst +++ b/docs/source/class/cf.FieldAncillary.rst @@ -105,6 +105,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.FieldAncillary.to_units ~cf.FieldAncillary.override_units ~cf.FieldAncillary.override_calendar diff --git a/docs/source/class/cf.Index.rst b/docs/source/class/cf.Index.rst index 2717ebba13..c8246f1c0e 100644 --- a/docs/source/class/cf.Index.rst +++ b/docs/source/class/cf.Index.rst @@ -102,6 +102,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.Index.to_units ~cf.Index.override_units ~cf.Index.override_calendar diff --git a/docs/source/class/cf.List.rst b/docs/source/class/cf.List.rst index d96f2f48a5..51dde4b8c9 100644 --- a/docs/source/class/cf.List.rst +++ b/docs/source/class/cf.List.rst @@ -102,6 +102,7 @@ Units :toctree: ../method/ :template: method.rst + ~cf.List.to_units ~cf.List.override_units ~cf.List.override_calendar