Repository: Pyomo/pyomo

Issue:
units error when multiple terms are involved causing unit consistency tests to fail
## Summary
The following error is reported in evaluating units for expressions with multiple terms involved. 

In my model, there are expressions involving several dimensionless terms such as Nusselt number, Reynolds number, and Prandtl number with fractional powers. For one such expressions the units are `joule ** 0.4 * kilogram ** 0.2 * watt ** 0.6 / kelvin / meter ** 2.2 / pascal ** 0.2 / second ** 0.8`, which when resolved in the basic units are equivalent to `kilogram / kelvin / second ** 3`. However, a test on unit consistency for this expression is raising the following assertion error:
`UnitsError: Units between joule ** 0.4 * kilogram ** 0.2 * watt ** 0.6 / kelvin / meter ** 2.2 / pascal ** 0.2 / second ** 0.8 and kilogram / kelvin / second ** 3 are not consistent. `
<!-- Explain, in a clear and concise way, the command you ran and the result you were trying to achieve.
Example: "I ran `pyomo solve` to optimize a model and ..." -->

### Steps to reproduce the issue
A test script is attached which simply contains 2 variables with units shown above and when `assert_units_equivalent `method is called for the two variables the aforementioned error can be seen.

<!-- Please include the command run and/or provide an example script that demonstrates the problem. -->

[units_consistency_failure_example.zip](https://github.com/Pyomo/pyomo/files/8673656/units_consistency_failure_example.zip)

### Error Message
`UnitsError: Units between joule ** 0.4 * kilogram ** 0.2 * watt ** 0.6 / kelvin / meter ** 2.2 / pascal ** 0.2 / second ** 0.8 and kilogram / kelvin / second ** 3 are not consistent. `
<!-- If Pyomo reported an error, provide the error message. PLEASE INCLUDE THE ENTIRE STACK TRACE. -->

<!-- If it did not report an error but the output appears incorrect, provide the incorrect output. If there was no error message and no output but the result is incorrect, describe how it does not match what you expect. -->
```console
$ # Output message here, including entire stack trace, if available

  File "c:\gmlc\source_code\dispatches\dispatches\models\fossil_case\ultra_supercritical_plant\storage\units_consistency_failure_example.py", line 51, in <module>
    assert_units_equivalent(m.var_1, m.var_2)

  File "C:\Users\susarlan\Anaconda3\envs\gmlc\lib\site-packages\pyomo\util\check_units.py", line 73, in assert_units_equivalent
    raise UnitsError(

UnitsError: Units between joule ** 0.4 * kilogram ** 0.2 * watt ** 0.6 / kelvin / meter ** 2.2 / pascal ** 0.2 / second ** 0.8 and kilogram / kelvin / second ** 3 are not consistent.
```

### Information on your system

Pyomo version: Pyomo 6.4.0 (CPython 3.9.12 on Windows 10)
Python version:
Operating system:
How Pyomo was installed (PyPI, conda, source): This pyomo version comes with IDAES/Dispatches
Solver (if applicable):


### Additional information

<!-- If you have any additional information, please list it here. -->


<!-- We encourage you to try, as much as possible, to reduce your problem to the minimal example that still reproduces the issue. That would help us a lot in fixing it quickly and effectively!

If you want to ask a question (how to use Pyomo, what it can do, etc.), please refer to our [online documentation](https://github.com/Pyomo/pyomo#getting-help) on ways to get help. -->




Discussion:

Resolving patch:
diff --git a/pyomo/core/base/units_container.py b/pyomo/core/base/units_container.py
--- a/pyomo/core/base/units_container.py
+++ b/pyomo/core/base/units_container.py
@@ -1082,14 +1082,24 @@ def __getattr__(self, item):
     #                                                                  float(conv_offset))
     #     self._pint_registry.define(defn_str)
 
+    def _rel_diff(self, a, b):
+        scale = min(abs(a), abs(b))
+        if scale < 1.:
+            scale = 1.
+        return abs(a - b) / scale
+
     def _equivalent_pint_units(self, a, b, TOL=1e-12):
         if a is b or a == b:
             return True
         base_a = self._pint_registry.get_base_units(a)
         base_b = self._pint_registry.get_base_units(b)
         if base_a[1] != base_b[1]:
-            return False
-        return abs(base_a[0] - base_b[0]) / min(base_a[0], base_b[0]) <= TOL
+            uc_a = base_a[1].dimensionality
+            uc_b = base_b[1].dimensionality
+            for key in uc_a.keys() | uc_b.keys():
+                if self._rel_diff(uc_a.get(key, 0), uc_b.get(key, 0)) >= TOL:
+                    return False
+        return self._rel_diff(base_a[0], base_b[0]) <= TOL
 
     def _equivalent_to_dimensionless(self, a, TOL=1e-12):
         if a is self._pint_dimensionless or a == self._pint_dimensionless:
@@ -1097,7 +1107,7 @@ def _equivalent_to_dimensionless(self, a, TOL=1e-12):
         base_a = self._pint_registry.get_base_units(a)
         if not base_a[1].dimensionless:
             return False
-        return abs(base_a[0] - 1.) / min(base_a[0], 1.) <= TOL
+        return self._rel_diff(base_a[0], 1.) <= TOL
 
     def _get_pint_units(self, expr):
         """


Test patch:
diff --git a/pyomo/util/tests/test_check_units.py b/pyomo/util/tests/test_check_units.py
--- a/pyomo/util/tests/test_check_units.py
+++ b/pyomo/util/tests/test_check_units.py
@@ -198,5 +198,31 @@ def arcrule(m):
 
         assert_units_consistent(m)
 
+    def test_units_roundoff_error(self):
+        # Issue 2393: this example resulted in roundoff error where the
+        # computed units for var_1 were
+        #(0.9999999999999986,
+        # <Unit('kilogram ** 1 / kelvin / meter ** 1.66533e-16 / second ** 3')>
+        #)
+        m = ConcreteModel()
+        m.var_1 = Var(
+            initialize=400,
+            units=((units.J**0.4) *
+                   (units.kg**0.2) *
+                   (units.W**0.6) /
+                   units.K /
+                   (units.m**2.2) /
+                   (units.Pa**0.2) /
+                   (units.s**0.8))
+        )
+        m.var_1.fix()
+        m.var_2 = Var(
+            initialize=400,
+            units=units.kg/units.s**3/units.K
+        )
+        assert_units_equivalent(m.var_1, m.var_2)
+
+
+
 if __name__ == "__main__":
     unittest.main()