Repository: hgrecco/pint

Issue:
Enable Mixed Unit Operations with Log Units [0dBm -10 dB = -10dBm] 
In electrical engineering, quantities with logarithmic units such as dB and dBm are "summed" or "subtracted" although having different dimensionalities (which is forbidden for base units in pint). It would be nice if pint could support such operations.

**Example 1: dB and dBm**
Let's consider a transmitter that generates a signal of 10 dBm. This signal goes through a channel that has an attenuation of -20 dB. The output power we expect is:

`Q(10, ureg.dBm) - Q(20, ureg.dB) = Q(-10, ureg.dBm)`

"Under the hood", the algebraic sum between dBm and dB is a product of two quantities  in linear units (power and transmission probability), so the "difference" does not present a problem.
When converting the logarithmic units to base units, the operation becomes:

`Q(10, ureg.dBm) -> 1e1 mW`
`Q(-20, ureg.dB) -> 1e-2`

the sum in logarithmic units becomes a product in linear units, and the result is a power:

`Q(10, ureg.dBm) + Q(-20, ureg.dB) -> 1e1 mW * 1e-2 = 1e-1 mW`

The output power is converted back into a logarithmic unit, if that unit is available:

`1e-1 mW -> Q(-10, ureg.dBm)`

Here you see that the sum of dBm and dB gives a result in dBm and the result looks like a difference.

**Example 2: dB/km * km**
A related operation could pop-up when dealing with a channel with -10 dB/km losses, which attenuates the power of a signal by a factor of 10% for every km of propagation. If the channel is 3 km long one would get.

`Q(-10, ureg.dB / ureg.km) * Q(3, ureg.km)`

Pint can handle the computation with linear units in three steps:

_Step#1:_ convert the log unit into base unit, which is from dB/km to dimensionless/km:
`Q(-10, ureg.dBm/ ureg.km) -> 1e-1 ureg.km^-1`

_Step#2:_ change the product into an exponentiation
`Q(-10, ureg.dB / ureg.km) * Q(3, ureg.km) -> (1e-1) ** (Q(3, ureg.km) / ureg.km)= 1e-3`

_Step#3:_ go back into a logarithmic unit, if that is available:
    `1e-3 -> Q(-30, ureg.dB)`

Also here the exponentiation in linear units looks as a product in log units.


Discussion:
Notice that at if pint should support operations between log units, it should do so in a generic way. Let us make one example.

`Q(10, ureg.dBm) + Q(20, ureg.dBm)`

Let's try to see what this operation means in linear lunits:

_Step#1:_ convert the units to base units:
`Q(10, ureg.dBm) -> 10 mW`
`Q(20, ureg.dBm) -> 100 mW`

_Step#2:_ change the sum into a product and compute the result
`Q(10, ureg.dBm) + Q(20, ureg.dBm) -> 10 mW * 100 mW -> 1000 mW^2`

_Step#3:_ go back into a logarithmic unit, if that is available
`1000 mW^2 -> 30 dBmW^2`

As it is know dBmW^2 is not defined in the list of units, so the final conversion cannot be done.
Just ran into this issue with pint 0.19.

```python
>>> Q(30, ureg.dBm) - Q(20, ureg.dBm)
<Quantity(10, 'delta_decibelmilliwatt')>
>>> (Q(30, ureg.dBm).to_base_units() - Q(20, ureg.dBm).to_base_units()).to("dB")
pint.errors.DimensionalityError: Cannot convert from 'kilogram * meter ** 2 / second ** 3' ([length] ** 2 * [mass] / [time] ** 3) to 'decibel' (dimensionless)
```

I can get my desired output with

```python
>>> Q(Q(30, ureg.dBm).magnitude - Q(20, ureg.dBm).magnitude, ureg.dB)
<Quantity(10, 'decibel')>
```

but that's kind of working around `pint`, not with it.
Would this be ok for you to do? You get your desired output with the ratio operator in base units.

>>> (Q(30, ureg.dBm).to_base_units() / Q(20, ureg.dBm).to_base_units()).to(ureg.dB)

I would use pint only to express a quantity with log units, use base units conversion for operations, and conversion back to log unit if that unit is defined and available.
That does work, thanks!

It isn't until just now that I recognize why subtracting and adding logarithmic units poses a problem for `pint` and how it causes problems for determining dimensions.

I read through https://pint.readthedocs.io/en/stable/log_units.html and https://pint.readthedocs.io/en/stable/log_units.html#compound-log-units, but these articles assume you have a good understanding of what logarithmic units are, which is a reasonable assumption.
The entire point of logarithms is that they were invented to simplify multiplication by instead adding logarithms.

For this library to make sense for dB units it must support adding dB together, and adding dB to referenced logarithmic units, i.e. dB + dBW, etc. 

Converting to base units, multiplying, and converting back is both cumbersome and really defeats the purpose of having logarithmic units to begin with
yes, this should be added. PRs adding this feature are welcomed!
Yup am submitting one today
@andrewgsavage https://github.com/hgrecco/pint/pull/2165
I have modified https://github.com/hgrecco/pint/pull/2165 to give a partial fix for this issue. complex units with more than one unit involved will not work



Patch:
diff --git a/CHANGES b/CHANGES
index 9142478b5..fdb5ffe86 100644
--- a/CHANGES
+++ b/CHANGES
@@ -23,6 +23,7 @@ Pint Changelog
 - Fix handling of extra arguments in conversion with enabled contexts (#2172)
 - Fix swapped left and right arguments in interp (#2187)
 - Fix formatted scientific notation bug in Python 3.13 (#2168)
+- Fix ability to add dB units, and to add dB (dimensionless) to referenced dB units, such as dBm or dBW (#1160)
 
 
 0.24.4 (2024-11-07)
diff --git a/pint/facets/nonmultiplicative/objects.py b/pint/facets/nonmultiplicative/objects.py
index 229b32c5f..d9fd88d40 100644
--- a/pint/facets/nonmultiplicative/objects.py
+++ b/pint/facets/nonmultiplicative/objects.py
@@ -27,6 +27,16 @@ def _get_non_multiplicative_units(self) -> list[str]:
             if not self._get_unit_definition(unit).is_multiplicative
         ]
 
+    @property
+    def _is_logarithmic(self) -> bool:
+        """Check if the PlainQuantity object has logarithmic units."""
+        if (
+            len(self._units) == 1
+            and self._get_unit_definition(next(iter(self._units))).is_logarithmic
+        ):
+            return True
+        return False
+
     def _get_delta_units(self) -> list[str]:
         """Return list of delta units ot the PlainQuantity object."""
         return [u for u in self._units if u.startswith("delta_")]
diff --git a/pint/facets/plain/quantity.py b/pint/facets/plain/quantity.py
index 7a40957bf..2b5b24354 100644
--- a/pint/facets/plain/quantity.py
+++ b/pint/facets/plain/quantity.py
@@ -746,21 +746,44 @@ def _add_sub(self, other, op):
                 raise DimensionalityError(self._units, "dimensionless")
             return self.__class__(magnitude, units)
 
-        if not self.dimensionality == other.dimensionality:
-            raise DimensionalityError(
-                self._units, other._units, self.dimensionality, other.dimensionality
-            )
+        # Special case for logarithmic units: dB can be added to dBm, dBW, etc.
+        # Get non-multiplicative units before checking dimensionality
+        self_non_mul_units = self._get_non_multiplicative_units()
+        other_non_mul_units = other._get_non_multiplicative_units()
 
         # Next we define some variables to make if-clauses more readable.
-        self_non_mul_units = self._get_non_multiplicative_units()
+        # We already have self_non_mul_units and other_non_mul_units from our logarithmic check
         is_self_multiplicative = len(self_non_mul_units) == 0
+        is_other_multiplicative = len(other_non_mul_units) == 0
+
         if len(self_non_mul_units) == 1:
             self_non_mul_unit = self_non_mul_units[0]
-        other_non_mul_units = other._get_non_multiplicative_units()
-        is_other_multiplicative = len(other_non_mul_units) == 0
         if len(other_non_mul_units) == 1:
             other_non_mul_unit = other_non_mul_units[0]
 
+        # Check if we're dealing with logarithmic units that can be added (dB + dBm, etc.)
+        if self._is_logarithmic and other._is_logarithmic:
+            # Case 1: both are the same logarithmic unit (dB + dB)
+            if self.units == other.units:
+                # Add the magnitudes directly when the same logarithmic unit
+                magnitude = op(self._magnitude, other._magnitude)
+                return self.__class__(magnitude, self._units)
+
+            # Case 2: Special handling for adding dimensionless dB to other logarithmic units
+            elif other.dimensionless and self.dimensionality != other.dimensionality:
+                # Add the magnitude directly - this assumes both are in logarithmic scale
+                magnitude = op(self._magnitude, other._magnitude)
+                return self.__class__(magnitude, self._units)
+            elif self.dimensionless and self.dimensionality != other.dimensionality:
+                # Add the magnitude directly - this assumes both are in logarithmic scale
+                magnitude = op(other._magnitude, self._magnitude)
+                return self.__class__(magnitude, other._units)
+
+        if not self.dimensionality == other.dimensionality:
+            raise DimensionalityError(
+                self._units, other._units, self.dimensionality, other.dimensionality
+            )
+
         # Presence of non-multiplicative units gives rise to several cases.
         if is_self_multiplicative and is_other_multiplicative:
             if self._units == other._units:
@@ -1455,6 +1478,11 @@ def _get_non_multiplicative_units(self) -> list[str]:
         """Return a list of the of non-multiplicative units of the PlainQuantity object."""
         return []
 
+    @property
+    def _is_logarithmic(self) -> bool:
+        """Check if the PlainQuantity object has logarithmic units."""
+        return False
+
     def _get_delta_units(self) -> list[str]:
         """Return list of delta units ot the PlainQuantity object."""
         return [u for u in self._units if u.startswith("delta_")]


Test patch:
diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py
index 5f1b0be49..725d18701 100644
--- a/pint/testsuite/test_log_units.py
+++ b/pint/testsuite/test_log_units.py
@@ -255,7 +255,12 @@ def test_compound_log_unit_parse_expr(module_registry_auto_offset):
     assert canonical_def == parse_def
 
 
-@pytest.mark.xfail
+def test_db_db_addition(module_registry_auto_offset):
+    """Test a dB value can be added to a dB and the answer is correct."""
+    ratio = (5 * module_registry_auto_offset.dB) + (10 * module_registry_auto_offset.dB)
+    assert ratio.to("dB").magnitude == pytest.approx(15)
+
+
 def test_dbm_db_addition(module_registry_auto_offset):
     """Test a dB value can be added to a dBm and the answer is correct."""
     power = (5 * module_registry_auto_offset.dBm) + (