Repository: yt-project/unyt (python)

Issue:
add two temperature values in celcius and Kelvin
* unyt version:  2.2.2
* Python version:   3.7.1
* Operating System: Windows

I just add two temperature values in celcius and Kelvin.
But the result in celcius is wrong

In [16]: from unyt import celcius, K
In [17]: temp1 = 10*celcius
In [18]: temp2 = 1*K
In [19]: temp1+temp2
Out[19]: unyt_quantity(11, 'degC')

Result expected: unyt_quantity(-262,15, 'degC')



Pull request:
Add temperature units delta_degC and delta_degF
Closes #99

Patch:
diff --git a/docs/usage.rst b/docs/usage.rst
index f962e4e..85f757b 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -335,8 +335,8 @@ using the :meth:`@accepts <unyt.dimensions.accepts>` and :meth:`@returns <unyt.d
 Temperature Units
 -----------------
 
-The temperature unit degree Celsius has the symbol °C, but since the degree character
-is an invalid Python identifier, Uynt uses the symbol `degC`. Printing a degree Celsius
+The temperature unit degree Celsius has the symbol ``°C``, but since the degree character
+is an invalid Python identifier, :mod:`unyt` uses the symbol ``degC``. Printing a degree Celsius
 quantity will show the correct symbol.
 
   >>> from unyt import degC
@@ -344,7 +344,7 @@ quantity will show the correct symbol.
   >>> print(Ta)
   23 °C
 
-The `degC` symbol has alternative names `degree_Celsius`, `Celsius` and `°C`.
+The ``degC`` symbol has alternative names ``degree_Celsius``, ``Celsius`` and ``°C``.
 
   >>> from unyt import degree_Celsius, unyt_array
   >>> Ta = 23*degree_Celsius
@@ -356,6 +356,20 @@ The `degC` symbol has alternative names `degree_Celsius`, `Celsius` and `°C`.
 
 These comments also apply to degree Fahrenheit.
 
+Performing arithmetic with temperature quantities can be ambiguous. To clarify intent,
+:mod:`unyt` has the convenience units ``delta_degC`` and ``delta_degF``.
+
+  >>> from unyt import degC, delta_degC, V
+  >>> t1 = 23*degC
+  >>> t2 = 1*delta_degC
+  >>> print(t1 + t2)
+  24 °C
+  >>> print(t2 - t1)
+  -22 °C
+  >>> tempco = 10.0*V/delta_degC
+  >>> print(tempco*2*delta_degC)
+  20.0 V
+
 Unit Conversions and Unit Systems
 +++++++++++++++++++++++++++++++++
 
diff --git a/unyt/_unit_lookup_table.py b/unyt/_unit_lookup_table.py
index abaaa79..58c1f66 100644
--- a/unyt/_unit_lookup_table.py
+++ b/unyt/_unit_lookup_table.py
@@ -124,6 +124,7 @@ default_unit_symbol_lut = OrderedDict(
             (1.0, dimensions.luminous_flux / dimensions.area, 0.0, r"\rm{lx}", True),
         ),
         ("degC", (1.0, dimensions.temperature, -273.15, r"^\circ\rm{C}", True)),
+        ("delta_degC", (1.0, dimensions.temperature, 0, r"\Delta^\circ\rm{C}", True)),
         # Imperial and other non-metric units
         ("inch", (m_per_inch, dimensions.length, 0.0, r"\rm{in}", False)),
         ("ft", (m_per_ft, dimensions.length, 0.0, r"\rm{ft}", False)),
@@ -140,6 +141,16 @@ default_unit_symbol_lut = OrderedDict(
                 False,
             ),
         ),
+        (
+            "delta_degF",
+            (
+                kelvin_per_rankine,
+                dimensions.temperature,
+                0.0,
+                r"\Delta^\circ\rm{F}",
+                False,
+            ),
+        ),
         (
             "R",
             (kelvin_per_rankine, dimensions.temperature, 0.0, r"^\circ\rm{R}", False),
diff --git a/unyt/array.py b/unyt/array.py
index d685f29..c5064b2 100644
--- a/unyt/array.py
+++ b/unyt/array.py
@@ -164,7 +164,22 @@ def _multiply_units(unit1, unit2):
     return ret.as_coeff_unit()
 
 
+TEMPERATURE_WARNING = """
+    Ambiguous operation with heterogeneous temperature units.
+    In the future, such operations will generate UnitOperationError.
+    Use delta_degC or delta_degF to avoid the ambiguity.
+"""
+
+
+@lru_cache(maxsize=128, typed=False)
 def _preserve_units(unit1, unit2=None):
+    if unit2 is None or unit1.dimensions is not temperature:
+        return 1, unit1
+    if unit1.base_offset == 0.0 and unit2.base_offset != 0.0:
+        if str(unit1.expr) in ["K", "R"]:
+            warnings.warn(TEMPERATURE_WARNING, FutureWarning, stacklevel=3)
+            return 1, unit1
+        return 1, unit2
     return 1, unit1
 
 
diff --git a/unyt/unit_object.py b/unyt/unit_object.py
index 44d48cb..54f74ed 100644
--- a/unyt/unit_object.py
+++ b/unyt/unit_object.py
@@ -334,8 +334,12 @@ class Unit(object):
         unit_str = self.expr.__str__()
         if unit_str == "degC":
             return "°C"
+        if unit_str == "delta_degC":
+            return "Δ°C"
         if unit_str == "degF":
             return "°F"
+        if unit_str == "delta_degF":
+            return "Δ°F"
         # @todo: don't use dunder method?
         return unit_str
 
diff --git a/unyt/unit_registry.py b/unyt/unit_registry.py
index 50c44da..c0cdbf8 100644
--- a/unyt/unit_registry.py
+++ b/unyt/unit_registry.py
@@ -340,10 +340,7 @@ def _correct_old_unit_registry(data, sympify=False):
             unsan_v[1] = cached_sympify(v[1])
         if len(unsan_v) == 4:
             # old unit registry so we need to add SI-prefixability to the registry
-            # entry, correct the base_value to be in MKS units, and swap dimensions to
-            # use unyt's dimension singletons
-
-            # add SI-prefixability to LUT entry
+            # entry and correct the base_value to be in MKS units
             if k in default_unit_symbol_lut:
                 unsan_v.append(default_unit_symbol_lut[k][4])
             else:
@@ -351,27 +348,10 @@ def _correct_old_unit_registry(data, sympify=False):
             dims = unsan_v[1]
             for dim_factor in dims.as_ordered_factors():
                 dim, power = dim_factor.as_base_exp()
-
-                # Swap dimensions in the LUT entry to use unyt's dimension singletons
-                for base_dim in unyt_dims.base_dimensions:
-                    # If they're *equal* but not *identical*, swap them
-                    if base_dim == dim and base_dim is not dim:
-                        if power != 1:
-                            unsan_v[1] /= dim ** power
-                            unsan_v[1] *= base_dim ** power
-                        else:
-                            # need a special case for power == 1 because id(symbol ** 1)
-                            # is not necessarily the same as id(symbol)
-                            unsan_v[1] /= dim
-                            unsan_v[1] *= base_dim
-                        break
-
-                # correct base value to be in MKS units
                 if dim == unyt_dims.mass:
                     unsan_v[0] /= 1000 ** float(power)
                 if dim == unyt_dims.length:
                     unsan_v[0] /= 100 ** float(power)
-
         lut[k] = tuple(unsan_v)
     for k in default_unit_symbol_lut:
         if k not in lut:


Test patch:
diff --git a/unyt/tests/test_unit_registry.py b/unyt/tests/test_unit_registry.py
index 83a5944..1051875 100644
--- a/unyt/tests/test_unit_registry.py
+++ b/unyt/tests/test_unit_registry.py
@@ -19,11 +19,10 @@ import pytest
 
 from numpy.testing import assert_allclose
 
-from unyt.dimensions import length, energy, mass, time, temperature
+from unyt.dimensions import length, energy
 from unyt.exceptions import SymbolNotFoundError, UnitParseError
 from unyt.unit_object import Unit
 from unyt.unit_registry import UnitRegistry
-from unyt.unit_systems import UnitSystem
 
 
 def test_add_modify_error():
@@ -93,46 +92,9 @@ def test_registry_json():
     assert reg.lut["tayne"][1] is length
 
 
-OLD_JSON_PATH = os.sep.join([os.path.dirname(__file__), "data/old_json_registry.txt"])
-
-
-def test_old_registry_multiple_load():
-    # See Issue #157 for details
-    reg1 = UnitRegistry()
-    reg1.add("code_length", 1.0, length)
-    reg1.add("code_mass", 1.0, mass)
-    reg1.add("code_time", 1.0, time)
-    reg1.add("code_temperature", 1.0, temperature)
-    UnitSystem(
-        reg1.unit_system_id,
-        "code_length",
-        "code_mass",
-        "code_time",
-        "code_temperature",
-        registry=reg1,
-    )
-
-    cm = Unit("code_mass", registry=reg1)
-    cl = Unit("code_length", registry=reg1)
-
-    (cm / cl).latex_representation()
-
-    with open(OLD_JSON_PATH) as f:
-        json_data = f.read()
-
-    reg2 = UnitRegistry.from_json(json_data)
-    UnitSystem(
-        reg2.unit_system_id,
-        "code_length",
-        "code_mass",
-        "code_time",
-        "code_temperature",
-        registry=reg2,
-    )
-
-
 def test_old_registry_json():
-    with open(OLD_JSON_PATH) as f:
+    path = os.sep.join([os.path.dirname(__file__), "data/old_json_registry.txt"])
+    with open(path) as f:
         json_text = f.read()
     reg = UnitRegistry.from_json(json_text)
     default_reg = UnitRegistry()
diff --git a/unyt/tests/test_units.py b/unyt/tests/test_units.py
index 4cf89f1..461f2f6 100644
--- a/unyt/tests/test_units.py
+++ b/unyt/tests/test_units.py
@@ -857,6 +857,11 @@ def test_degC():
     assert str(a) == "1 °C"
 
 
+def test_delta_degC():
+    a = 1 * Unit("delta_degC")
+    assert str(a) == "1 Δ°C"
+
+
 def test_degF():
     assert Unit("degree_fahrenheit") == Unit("degF")
     assert Unit("degree_Fahrenheit") == Unit("degF")
@@ -864,3 +869,8 @@ def test_degF():
     assert Unit("°F") == Unit("degF")
     a = 1 * Unit("degF")
     assert str(a) == "1 °F"
+
+
+def test_delta_degF():
+    a = 1 * Unit("delta_degF")
+    assert str(a) == "1 Δ°F"
diff --git a/unyt/tests/test_unyt_array.py b/unyt/tests/test_unyt_array.py
index 98cbaf8..e338748 100644
--- a/unyt/tests/test_unyt_array.py
+++ b/unyt/tests/test_unyt_array.py
@@ -63,7 +63,7 @@ from unyt.unit_symbols import cm, m, g, degree
 from unyt.unit_registry import UnitRegistry
 from unyt._on_demand_imports import _astropy, _h5py, _pint, NotAModule
 from unyt._physical_ratios import metallicity_sun, speed_of_light_cm_per_s
-from unyt import dimensions, Unit
+from unyt import dimensions, Unit, degC, K, delta_degC, degF, R, delta_degF
 
 
 def operate_and_compare(a, b, op, answer):
@@ -2379,3 +2379,35 @@ def test_neper_bel():
         unyt_array([0, 20], "dB") ** 2
     with pytest.raises(InvalidUnitOperation):
         np.power(unyt_array([0, 20], "dB"), -2)
+
+
+def test_delta_degC():
+    t1 = 10 * degC
+    t2 = 1 * K
+    assert t1 + t2 == 11 * degC
+    # In the current implementation 1*K + 10*degC = 11*K
+    # In the future, this will generate UnitOperationError
+    with pytest.warns(FutureWarning):
+        t2 + t1
+    assert t2 + t1 == 11 * K
+    t3 = 1 * delta_degC
+    assert t1 + t3 == 11 * degC
+    assert t3 + t1 == 11 * degC
+    assert 1 * delta_degC + 2 * delta_degC == 3 * delta_degC
+    assert 2 * delta_degC == unyt_quantity(2, "delta_degC")
+
+
+def test_delta_degF():
+    t1 = 10 * degF
+    t2 = 1 * R
+    assert t1 + t2 == 11 * degF
+    # In the current implementation 1*R + 10*degF = 11*R
+    # In the future, this will generate UnitOperationError
+    with pytest.warns(FutureWarning):
+        t2 + t1
+    assert t2 + t1 == 11 * R
+    t3 = 1 * delta_degF
+    assert t1 + t3 == 11 * degF
+    assert t3 + t1 == 11 * degF
+    assert 1 * delta_degF + 2 * delta_degF == 3 * delta_degF
+    assert 2 * delta_degF == unyt_quantity(2, "delta_degF")