Repository: hgrecco/pint

Issue:
log and exponential unit conversions
In optics, the power of light is often measured in dBm. The definition of dBm is:

dBm = 10 \* log(mW)

It means that 0 dBm is 1 mW, 10 dBm is 10 mW and -10 dBm is 0.1 mW.

The opposite conversion is:

mW = 10**(dBm / 10)


Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index 8128807..68fac8d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -4,7 +4,8 @@ Pint Changelog
 0.16 (unreleased)
 -----------------
 
-- Nothing changed yet.
+- Implements Logarithmic Units like dBm, dB or decade
+  (Issue #71, Thanks Dima Pustakhod, Clark Willison, Giorgio Signorello, Steven Casagrande, Jonathan Wheeler)
 
 
 0.15 (2020-08-22)
@@ -22,6 +23,8 @@ Pint Changelog
 - Fixed right operand power for dimensionless Quantity to reflect numpy behavior. (Issue #1136)
 - Eliminated warning when setting a masked value on an underlying MaskedArray.
 - Add `sort` option to `formatting.formatter` to permit disabling sorting of component units in format string
+- Implements Logarithmic Units like dBm, dB or decade
+  (Issue #71, Thanks Dima Pustakhod, Giorgio Signorello, Jonathan Wheeler)
 
 
 0.14 (2020-07-01)
diff --git a/docs/index.rst b/docs/index.rst
index 5bbbd51..831ab6a 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -122,6 +122,7 @@ User Guide
     defining-quantities
     numpy
     nonmult
+    log_units
     wrapping
     plotting
     serialization
diff --git a/docs/log_units.rst b/docs/log_units.rst
new file mode 100644
index 0000000..03e0079
--- /dev/null
+++ b/docs/log_units.rst
@@ -0,0 +1,127 @@
+.. _log_units:
+
+
+Logarithmic Units
+=================
+
+.. warning::
+
+    Support for logarithmic units in Pint is currently in Beta. Please take
+    careful note of the information below, particularly around `compound log units`_
+    to avoid calculation errors. Bug reports and pull requests are always
+    welcome, please see :doc:`contributing` for more information on
+    how you can help improve this feature (and Pint in general).
+
+Pint supports some logarithmic units, including `dB`, `dBm`, `octave`, and `decade`
+as well as some conversions between them and their base units where applicable.
+These units behave much like those described in :ref:`nonmult`, so many of
+the recommendations there apply here as well.
+
+Setting up the ``UnitRegistry()``
+---------------------------------
+
+Many of the examples below will fail without supplying the
+``autoconvert_offset_to_baseunit=True`` flag. To use logarithmic units,
+intialize your ``UnitRegistry()`` like so:
+
+.. doctest::
+
+    >>> from pint import UnitRegistry
+    >>> ureg = UnitRegistry(autoconvert_offset_to_baseunit=True)
+    >>> Q_ = ureg.Quantity
+
+If you can't pass that flag you will need to define all logarithmic units
+:ref:`using the Quantity() constructor<Using the constructor>`, and you will
+be restricted in the kinds of operations you can do without explicitly calling
+`.to_base_units()` first.
+
+Defining log quantities
+-----------------------
+
+After you've set up your ``UnitRegistry()`` with the ``autoconvert...`` flag,
+you can define simple logarithmic quantities like most others:
+
+.. doctest::
+
+    >>> 20.0 * ureg.dBm
+    <Quantity(20.0, 'decibelmilliwatt')>
+    >>> ureg('20.0 dBm')
+    <Quantity(20.0, 'decibelmilliwatt')>
+    >>> ureg('20 dB')
+    <Quantity(20, 'decibel')>
+
+
+Converting to and from base units
+---------------------------------
+
+Get a sense of how logarithmic units are handled by using the `.to()` and
+`.to_base_units()` methods:
+
+.. doctest::
+
+    >>> ureg('20 dBm').to('mW')
+    <Quantity(100.0, 'milliwatt')>
+    >>> ureg('20 dB').to_base_units()
+    <Quantity(100.0, 'dimensionless')>
+
+.. note::
+
+    Notice in the above example how the `dB` unit is defined for
+    power quantities (10*log(p/p0)) not field (amplitude) quantities
+    (20*log(v/v0)). Take care that you're only using it to multiply power
+    levels, and not e.g. Voltages.
+
+Convert back from a base unit to a logarithmic unit using the `.to()` method:
+
+.. doctest::
+
+    >>> (100.0 * ureg('mW')).to('dBm')
+    <Quantity(20.0, 'decibelmilliwatt')>
+    >>> shift = Q_(4, '')
+    >>> shift
+    <Quantity(4, 'dimensionless')>
+    >>> shift.to('octave')
+    <Quantity(2.0, 'octave')>
+
+Compound log units
+------------------
+
+.. warning::
+
+    Support for compound logarithmic units is not comprehensive. The following
+    examples work, but many others will not. Consider converting the logarithmic
+    portion to base units before adding more units.
+
+Pint sometimes works with mixtures of logarithmic and other units. Below is an
+example of computing RMS noise from a noise density and a bandwidth:
+
+.. doctest::
+
+    >>> noise_density = -161.0 * ureg.dBm / ureg.Hz
+    >>> bandwidth = 10.0 * ureg.kHz
+    >>> noise_power = noise_density * bandwidth
+    >>> noise_power.to('dBm')
+    <Quantity(-121.0, 'decibelmilliwatt')>
+    >>> noise_power.to('mW')
+    <Quantity(7.94328235e-13, 'milliwatt')>
+
+There are still issues with parsing compound units, so for now the following
+will not work:
+
+.. doctest::
+
+    >>> -161.0 * ureg('dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz)
+    False
+
+But this will:
+
+.. doctest::
+
+    >>> ureg('-161.0 dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz)
+    True
+    >>> Q_(-161.0, 'dBm') / ureg.Hz == (-161.0 * ureg.dBm / ureg.Hz)
+    True
+
+To begin using this feature while avoiding problems, define logarithmic units
+as single-unit quantities and convert them to their base units as quickly as
+possible.
diff --git a/pint/__init__.py b/pint/__init__.py
index 83f5420..8bea195 100644
--- a/pint/__init__.py
+++ b/pint/__init__.py
@@ -14,9 +14,10 @@
 import sys
 
 from .context import Context
-from .errors import (
+from .errors import (  # noqa: F401
     DefinitionSyntaxError,
     DimensionalityError,
+    LogarithmicUnitCalculusError,
     OffsetUnitCalculusError,
     RedefinitionError,
     UndefinedUnitError,
diff --git a/pint/compat.py b/pint/compat.py
index 8b318af..1209030 100644
--- a/pint/compat.py
+++ b/pint/compat.py
@@ -127,6 +127,14 @@ try:
 except ImportError:
     HAS_BABEL = False
 
+# Defines Logarithm and Exponential for Logarithmic Converter
+if HAS_NUMPY:
+    from numpy import exp  # noqa: F401
+    from numpy import log  # noqa: F401
+else:
+    from math import exp  # noqa: F401
+    from math import log  # noqa: F401
+
 if not HAS_BABEL:
     babel_parse = babel_units = missing_dependency("Babel")  # noqa: F811
 
diff --git a/pint/constants_en.txt b/pint/constants_en.txt
index c3ecbf1..9737b7c 100644
--- a/pint/constants_en.txt
+++ b/pint/constants_en.txt
@@ -13,6 +13,7 @@ tansec = 4.8481368111333441675396429478852851658848753880815e-6   # tangent of 1
 ln10   = 2.3025850929940456840179914546843642076011014886288      # natural logarithm of 10
 wien_x = 4.9651142317442763036987591313228939440555849867973      # solution to (x-5)*exp(x)+5 = 0 => x = W(5/exp(5))+5
 wien_u = 2.8214393721220788934031913302944851953458817440731      # solution to (u-3)*exp(u)+3 = 0 => u = W(3/exp(3))+3
+eulers_number = 2.71828182845904523536028747135266249775724709369995
 
 #### DEFINED EXACT CONSTANTS ####
 
diff --git a/pint/converters.py b/pint/converters.py
index 9273350..1f6c094 100644
--- a/pint/converters.py
+++ b/pint/converters.py
@@ -9,10 +9,19 @@
 """
 
 
+from .compat import HAS_NUMPY, exp, log  # noqa: F401
+
+
 class Converter:
     """Base class for value converters."""
 
-    is_multiplicative = True
+    @property
+    def is_multiplicative(self):
+        return True
+
+    @property
+    def is_logarithmic(self):
+        return False
 
     def to_reference(self, value, inplace=False):
         return value
@@ -24,8 +33,6 @@ class Converter:
 class ScaleConverter(Converter):
     """A linear transformation."""
 
-    is_multiplicative = True
-
     def __init__(self, scale):
         self.scale = scale
 
@@ -74,3 +81,81 @@ class OffsetConverter(Converter):
             value = (value - self.offset) / self.scale
 
         return value
+
+
+class LogarithmicConverter(Converter):
+    """ Converts between linear units and logarithmic units, such as dB, octave, neper or pH.
+    Q_log = logfactor * log( Q_lin / scale ) / log(log_base)
+
+    Parameters
+    ----------
+    scale : float
+        unit of reference at denominator for logarithmic unit conversion
+    logbase : float
+        base of logarithm used in the logarithmic unit conversion
+    logfactor : float
+        factor multupled to logarithm for unit conversion
+    inplace : bool
+        controls if computation is done in place
+    """
+
+    def __init__(self, scale, logbase, logfactor):
+        """
+        Parameters
+        ----------
+        scale : float
+            unit of reference at denominator inside logarithm for unit conversion
+        logbase: float
+            base of logarithm used in unit conversion
+        logfactor: float
+            factor multiplied to logarithm for unit conversion
+        """
+
+        self.scale = scale
+        self.logbase = logbase
+        self.logfactor = logfactor
+
+    @property
+    def is_multiplicative(self):
+        return False
+
+    @property
+    def is_logarithmic(self):
+        return True
+
+    def from_reference(self, value, inplace=False):
+        """Converts value from the reference unit to the logarithmic unit
+
+            dBm   <------   mW
+            y dBm = 10 log10( x / 1mW )
+        """
+        if inplace:
+            value /= self.scale
+            if HAS_NUMPY:
+                log(value, value)
+            else:
+                value = log(value)
+            value *= self.logfactor / log(self.logbase)
+        else:
+            value = self.logfactor * log(value / self.scale) / log(self.logbase)
+
+        return value
+
+    def to_reference(self, value, inplace=False):
+        """Converts value to the reference unit from the logarithmic unit
+
+            dBm   ------>   mW
+            y dBm = 10 log10( x / 1mW )
+        """
+        if inplace:
+            value /= self.logfactor
+            value *= log(self.logbase)
+            if HAS_NUMPY:
+                exp(value, value)
+            else:
+                value = exp(value)
+            value *= self.scale
+        else:
+            value = self.scale * exp(log(self.logbase) * (value / self.logfactor))
+
+        return value
diff --git a/pint/default_en.txt b/pint/default_en.txt
index 916e924..7e8c690 100644
--- a/pint/default_en.txt
+++ b/pint/default_en.txt
@@ -108,7 +108,6 @@ gram = [mass] = g
 mole = [substance] = mol
 kelvin = [temperature]; offset: 0 = K = degK = °K = degree_Kelvin = degreeK  # older names supported for compatibility
 radian = [] = rad
-neper = [] = Np
 bit = []
 count = []
 
@@ -136,13 +135,13 @@ mil = π / 32000 * radian
 steradian = radian ** 2 = sr
 square_degree = (π / 180) ** 2 * sr = sq_deg = sqdeg
 
-# Logarithmic ratio
-bel = 0.5 * ln10 * neper
-
 # Information
-byte = 8 * bit = B = octet
 baud = bit / second = Bd = bps
 
+byte = 8 * bit = B = octet
+# byte = 8 * bit = _ = octet
+## NOTE: B (byte) symbol can conflict with Bell
+
 # Length
 angstrom = 1e-10 * meter = Å = ångström = Å
 micron = micrometer = µ
@@ -174,7 +173,10 @@ week = 7 * day
 fortnight = 2 * week
 year = 365.25 * day = a = yr = julian_year
 month = year / 12
-decade = 10 * year
+
+# decade = 10 * year
+## NOTE: decade [time] can conflict with decade [dimensionless]
+
 century = 100 * year = _ = centuries
 millennium = 1e3 * year = _ = millennia
 eon = 1e9 * year
@@ -308,6 +310,7 @@ inch_H2O_39F = inch * water_39F * g_0
 inch_H2O_60F = inch * water_60F * g_0
 foot_H2O = foot * water * g_0 = ftH2O = feet_H2O
 centimeter_H2O = centimeter * water * g_0 = cmH2O = cm_H2O
+sound_pressure_level = 20e-6 * pascal = SPL
 
 # Torque
 [torque] = [force] * [length]
@@ -470,6 +473,26 @@ buckingham = debye * angstrom
 bohr_magneton = e * hbar / (2 * m_e) = µ_B = mu_B
 nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N
 
+# Logaritmic Unit Definition
+#  Unit = scale; logbase; logfactor 
+#  x_dB = [logfactor] * log( x_lin / [scale] ) / log( [logbase] )
+ 
+# Logaritmic Units of dimensionless quantity: [ https://en.wikipedia.org/wiki/Level_(logarithmic_quantity) ]
+
+decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm
+decibelmicrowatt = 1e-6 watt; logbase: 10; logfactor: 10 = dBu
+
+decibel = 1 ; logbase: 10; logfactor: 10 = dB
+# bell = 1 ; logbase: 10; logfactor: = B
+## NOTE: B (Bell) symbol conflicts with byte
+
+decade = 1 ; logbase: 10; logfactor: 1
+## NOTE: decade [time] can conflict with decade [dimensionless]
+
+octave = 1 ; logbase: 2; logfactor: 1 = oct
+
+neper = 1 ; logbase: 2.71828182845904523536028747135266249775724709369995; logfactor: 0.5 = Np
+# neper = 1 ; logbase: eulers_number; logfactor: 0.5 = Np
 
 #### UNIT GROUPS ####
 # Mostly for length, area, volume, mass, force
diff --git a/pint/definitions.py b/pint/definitions.py
index 8b7e581..67adca1 100644
--- a/pint/definitions.py
+++ b/pint/definitions.py
@@ -10,7 +10,7 @@
 
 from collections import namedtuple
 
-from .converters import OffsetConverter, ScaleConverter
+from .converters import LogarithmicConverter, OffsetConverter, ScaleConverter
 from .errors import DefinitionSyntaxError
 from .util import ParserHelper, UnitsContainer, _is_dim
 
@@ -120,6 +120,10 @@ class Definition:
     def is_multiplicative(self):
         return self._converter.is_multiplicative
 
+    @property
+    def is_logarithmic(self):
+        return self._converter.is_logarithmic
+
     @classmethod
     def from_string(cls, definition, non_int_type=float):
         """Parse a definition.
@@ -235,7 +239,7 @@ class UnitDefinition(Definition):
             definition = PreprocessedDefinition.from_string(definition)
 
         if ";" in definition.value:
-            [converter, modifiers] = definition.value.split(";", 2)
+            [converter, modifiers] = definition.value.split(";", 1)
 
             try:
                 modifiers = dict(
@@ -265,11 +269,23 @@ class UnitDefinition(Definition):
             )
         reference = UnitsContainer(converter)
 
-        if modifiers.get("offset", 0) != 0:
-            converter = OffsetConverter(converter.scale, modifiers["offset"])
-        else:
+        if not modifiers:
             converter = ScaleConverter(converter.scale)
 
+        elif "offset" in modifiers:
+            if modifiers.get("offset", 0.0) != 0.0:
+                converter = OffsetConverter(converter.scale, modifiers["offset"])
+            else:
+                converter = ScaleConverter(converter.scale)
+
+        elif "logbase" in modifiers and "logfactor" in modifiers:
+            converter = LogarithmicConverter(
+                converter.scale, modifiers["logbase"], modifiers["logfactor"]
+            )
+
+        else:
+            raise DefinitionSyntaxError("Unable to assing a converter to the unit")
+
         return cls(
             definition.name,
             definition.symbol,
diff --git a/pint/errors.py b/pint/errors.py
index 4ce1eaf..8376c4f 100644
--- a/pint/errors.py
+++ b/pint/errors.py
@@ -8,6 +8,9 @@
     :license: BSD, see LICENSE for more details.
 """
 
+OFFSET_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html"
+LOG_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html"
+
 
 def _file_prefix(filename=None, lineno=None):
     if filename and lineno is not None:
@@ -111,7 +114,22 @@ class OffsetUnitCalculusError(PintTypeError):
         return (
             "Ambiguous operation with offset unit (%s)."
             % ", ".join(str(u) for u in self.args)
-            + " See https://pint.readthedocs.io/en/latest/nonmult.html for guidance."
+            + " See "
+            + OFFSET_ERROR_DOCS_HTML
+            + " for guidance."
+        )
+
+
+class LogarithmicUnitCalculusError(PintTypeError):
+    """Raised on inappropriate operations with logarithmic units."""
+
+    def __str__(self):
+        return (
+            "Ambiguous operation with logarithmic unit (%s)."
+            % ", ".join(str(u) for u in self.args)
+            + " See "
+            + LOG_ERROR_DOCS_HTML
+            + " for guidance."
         )
 
 
diff --git a/pint/registry.py b/pint/registry.py
index 2686ee9..15786d0 100644
--- a/pint/registry.py
+++ b/pint/registry.py
@@ -51,7 +51,7 @@ import pkg_resources
 from . import registry_helpers, systems
 from .compat import babel_parse, tokenizer
 from .context import Context, ContextChain
-from .converters import ScaleConverter
+from .converters import LogarithmicConverter, ScaleConverter
 from .definitions import (
     AliasDefinition,
     Definition,
@@ -1352,7 +1352,7 @@ class NonMultiplicativeRegistry(BaseRegistry):
             raise UndefinedUnitError(u)
 
     def _validate_and_extract(self, units):
-
+        # u is for unit, e is for exponent
         nonmult_units = [
             (u, e) for u, e in units.items() if not self._is_multiplicative(u)
         ]
@@ -1379,6 +1379,21 @@ class NonMultiplicativeRegistry(BaseRegistry):
 
         return None
 
+    def _add_ref_of_log_unit(self, offset_unit, all_units):
+
+        slct_unit = self._units[offset_unit]
+        if isinstance(slct_unit.converter, LogarithmicConverter):
+            # Extract reference unit
+            slct_ref = slct_unit.reference
+            # If reference unit is not dimensionless
+            if slct_ref != UnitsContainer():
+                # Extract reference unit
+                (u, e) = [(u, e) for u, e in slct_ref.items()].pop()
+                # Add it back to the unit list
+                return all_units.add(u, e)
+        # Otherwise, return the units unmodified
+        return all_units
+
     def _convert(self, value, src, dst, inplace=False):
         """Convert value from some source to destination units.
 
@@ -1434,10 +1449,14 @@ class NonMultiplicativeRegistry(BaseRegistry):
         if src_offset_unit:
             value = self._units[src_offset_unit].converter.to_reference(value, inplace)
             src = src.remove([src_offset_unit])
+            # Add reference unit for multiplicative section
+            src = self._add_ref_of_log_unit(src_offset_unit, src)
 
         # clean dst units from offset units
         if dst_offset_unit:
             dst = dst.remove([dst_offset_unit])
+            # Add reference unit for multiplicative section
+            dst = self._add_ref_of_log_unit(dst_offset_unit, dst)
 
         # Convert non multiplicative units to the dst.
         value = super()._convert(value, src, dst, inplace, False)
diff --git a/pip-wheel-metadata/Pint.dist-info/AUTHORS b/pip-wheel-metadata/Pint.dist-info/AUTHORS
new file mode 100644
index 0000000..b09c14c
--- /dev/null
+++ b/pip-wheel-metadata/Pint.dist-info/AUTHORS
@@ -0,0 +1,51 @@
+Pint is written and maintained by Hernan E. Grecco <hernan.grecco@gmail.com>.
+
+Other contributors, listed alphabetically, are:
+
+* Aaron Coleman
+* Alexander Böhn <fish2000@gmail.com>
+* Ana Krivokapic <akrivokapic1@gmail.com>
+* Andrea Zonca <code@andreazonca.com>
+* Andrew Savage <andrewgsavage@gmail.com>
+* Brend Wanders <b.wanders@utwente.nl>
+* choloepus
+* coutinho <coutinho@esrf.fr>
+* Clément Pit-Claudel <clement.pitclaudel@live.com>
+* Daniel Sokolowski <daniel.sokolowski@danols.com>
+* Dave Brooks <dave@bcs.co.nz>
+* David Linke
+* Ed Schofield <ed@pythoncharmers.com>
+* Eduard Bopp <eduard.bopp@aepsil0n.de>
+* Eli <elifab@gmail.com>
+* Felix Hummel <felix@felixhummel.de>
+* Francisco Couzo <franciscouzo@gmail.com>
+* Giel van Schijndel <me@mortis.eu>
+* Giorgio Signorello
+* Guido Imperiale <crusaderky@gmail.com>
+* Ignacio Fdez. Galván <jellby@yahoo.com>
+* James Rowe <jnrowe@gmail.com>
+* Jim Turner <jturner314@gmail.com>
+* Joel B. Mohler <joel@kiwistrawberry.us>
+* John David Reaver <jdreaver@adlerhorst.com>
+* Jonas Olson <jolson@kth.se>
+* Jules Chéron <me@julescheron.com>
+* Kaido Kert <kaidokert@gmail.com>
+* Kenneth D. Mankoff <mankoff@gmail.com>
+* Kevin Davies <kdavies4@gmail.com>
+* Luke Campbell <luke.s.campbell@gmail.com>
+* Matthieu Dartiailh <marul@laposte.net>
+* Nate Bogdanowicz <natezb@gmail.com>
+* Peter Grayson <jpgrayson@gmail.com>
+* Richard Barnes <rbarnes@umn.edu>
+* Ryan Dwyer <ryanpdwyer@gmail.com>
+* Ryan Kingsbury <RyanSKingsbury@alumni.unc.edu>
+* Ryan May
+* Sigvald Marholm <sigvald@marebakken.com>
+* Sundar Raman <cybertoast@gmail.com>
+* Tiago Coutinho <coutinho@esrf.fr>
+* Thomas Kluyver <takowl@gmail.com>
+* Tom Ritchford <tom@swirly.com>
+* Virgil Dupras <virgil.dupras@savoirfairelinux.com>
+* Zebedee Nicholls <zebedee.nicholls@climate-energy-college.org>
+
+(If you think that your name belongs here, please let the maintainer know)


Test patch:
diff --git a/pint/testsuite/test_converters.py b/pint/testsuite/test_converters.py
index 061b05d..460b3e5 100644
--- a/pint/testsuite/test_converters.py
+++ b/pint/testsuite/test_converters.py
@@ -1,7 +1,12 @@
 import itertools
 
 from pint.compat import np
-from pint.converters import Converter, OffsetConverter, ScaleConverter
+from pint.converters import (
+    Converter,
+    LogarithmicConverter,
+    OffsetConverter,
+    ScaleConverter,
+)
 from pint.testsuite import BaseTestCase, helpers
 
 
@@ -9,19 +14,38 @@ class TestConverter(BaseTestCase):
     def test_converter(self):
         c = Converter()
         self.assertTrue(c.is_multiplicative)
+        self.assertFalse(c.is_logarithmic)
         self.assertTrue(c.to_reference(8))
         self.assertTrue(c.from_reference(8))
 
     def test_multiplicative_converter(self):
         c = ScaleConverter(20.0)
+        self.assertTrue(c.is_multiplicative)
+        self.assertFalse(c.is_logarithmic)
         self.assertEqual(c.from_reference(c.to_reference(100)), 100)
         self.assertEqual(c.to_reference(c.from_reference(100)), 100)
 
     def test_offset_converter(self):
         c = OffsetConverter(20.0, 2)
+        self.assertFalse(c.is_multiplicative)
+        self.assertFalse(c.is_logarithmic)
         self.assertEqual(c.from_reference(c.to_reference(100)), 100)
         self.assertEqual(c.to_reference(c.from_reference(100)), 100)
 
+    def test_log_converter(self):
+        c = LogarithmicConverter(scale=1, logbase=10, logfactor=1)
+        self.assertFalse(c.is_multiplicative)
+        self.assertTrue(c.is_logarithmic)
+        self.assertAlmostEqual(c.to_reference(0), 1)
+        self.assertAlmostEqual(c.to_reference(1), 10)
+        self.assertAlmostEqual(c.to_reference(2), 100)
+        self.assertAlmostEqual(c.from_reference(1), 0)
+        self.assertAlmostEqual(c.from_reference(10), 1)
+        self.assertAlmostEqual(c.from_reference(100), 2)
+        arb_value = 20.0
+        self.assertAlmostEqual(c.from_reference(c.to_reference(arb_value)), arb_value)
+        self.assertAlmostEqual(c.to_reference(c.from_reference(arb_value)), arb_value)
+
     @helpers.requires_numpy()
     def test_converter_inplace(self):
         for c in (ScaleConverter(20.0), OffsetConverter(20.0, 2)):
@@ -35,3 +59,24 @@ class TestConverter(BaseTestCase):
                 r = fun(a, inplace)
                 np.testing.assert_allclose(r, ac)
                 comp(a, r)
+
+    @helpers.requires_numpy()
+    def test_log_converter_inplace(self):
+        arb_value = 3.14
+        c = LogarithmicConverter(scale=1, logbase=10, logfactor=1)
+
+        from_to = lambda value, inplace: c.from_reference(
+            c.to_reference(value, inplace), inplace
+        )
+
+        to_from = lambda value, inplace: c.to_reference(
+            c.from_reference(value, inplace), inplace
+        )
+
+        for fun, (inplace, comp) in itertools.product(
+            (from_to, to_from), ((True, self.assertIs), (False, self.assertIsNot))
+        ):
+            arb_array = arb_value * np.ones((1, 10))
+            result = fun(arb_array, inplace)
+            np.testing.assert_allclose(result, arb_array)
+            comp(arb_array, result)
diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py
index ac8e472..21ba401 100644
--- a/pint/testsuite/test_definitions.py
+++ b/pint/testsuite/test_definitions.py
@@ -1,4 +1,4 @@
-from pint.converters import OffsetConverter, ScaleConverter
+from pint.converters import LogarithmicConverter, OffsetConverter, ScaleConverter
 from pint.definitions import (
     AliasDefinition,
     Definition,
@@ -94,6 +94,67 @@ class TestDefinition(BaseTestCase):
             "degF = 9 / 5 * kelvin; offset: 255.372222 bla",
         )
 
+    def test_log_unit_definition(self):
+
+        x = Definition.from_string(
+            "decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm"
+        )
+        self.assertIsInstance(x, UnitDefinition)
+        self.assertFalse(x.is_base)
+        self.assertIsInstance(x.converter, LogarithmicConverter)
+        self.assertEqual(x.converter.scale, 1e-3)
+        self.assertEqual(x.converter.logbase, 10)
+        self.assertEqual(x.converter.logfactor, 10)
+        self.assertEqual(x.reference, UnitsContainer(watt=1))
+
+        x = Definition.from_string("decibel = 1 ; logbase: 10; logfactor: 10 = dB")
+        self.assertIsInstance(x, UnitDefinition)
+        self.assertFalse(x.is_base)
+        self.assertIsInstance(x.converter, LogarithmicConverter)
+        self.assertEqual(x.converter.scale, 1)
+        self.assertEqual(x.converter.logbase, 10)
+        self.assertEqual(x.converter.logfactor, 10)
+        self.assertEqual(x.reference, UnitsContainer())
+
+        x = Definition.from_string("bell = 1 ; logbase: 10; logfactor: 1 = B")
+        self.assertIsInstance(x, UnitDefinition)
+        self.assertFalse(x.is_base)
+        self.assertIsInstance(x.converter, LogarithmicConverter)
+        self.assertEqual(x.converter.scale, 1)
+        self.assertEqual(x.converter.logbase, 10)
+        self.assertEqual(x.converter.logfactor, 1)
+        self.assertEqual(x.reference, UnitsContainer())
+
+        x = Definition.from_string("decade = 1 ; logbase: 10; logfactor: 1")
+        self.assertIsInstance(x, UnitDefinition)
+        self.assertFalse(x.is_base)
+        self.assertIsInstance(x.converter, LogarithmicConverter)
+        self.assertEqual(x.converter.scale, 1)
+        self.assertEqual(x.converter.logbase, 10)
+        self.assertEqual(x.converter.logfactor, 1)
+        self.assertEqual(x.reference, UnitsContainer())
+
+        eulersnumber = 2.71828182845904523536028747135266249775724709369995
+        x = Definition.from_string(
+            "neper = 1 ; logbase: %1.50f; logfactor: 0.5 = Np" % eulersnumber
+        )
+        self.assertIsInstance(x, UnitDefinition)
+        self.assertFalse(x.is_base)
+        self.assertIsInstance(x.converter, LogarithmicConverter)
+        self.assertEqual(x.converter.scale, 1)
+        self.assertEqual(x.converter.logbase, eulersnumber)
+        self.assertEqual(x.converter.logfactor, 0.5)
+        self.assertEqual(x.reference, UnitsContainer())
+
+        x = Definition.from_string("octave = 1 ; logbase: 2; logfactor: 1 = oct")
+        self.assertIsInstance(x, UnitDefinition)
+        self.assertFalse(x.is_base)
+        self.assertIsInstance(x.converter, LogarithmicConverter)
+        self.assertEqual(x.converter.scale, 1)
+        self.assertEqual(x.converter.logbase, 2)
+        self.assertEqual(x.converter.logfactor, 1)
+        self.assertEqual(x.reference, UnitsContainer())
+
     def test_dimension_definition(self):
         x = DimensionDefinition("[time]", "", (), None, is_base=True)
         self.assertTrue(x.is_base)
diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py
index 758ba97..3e65c61 100644
--- a/pint/testsuite/test_errors.py
+++ b/pint/testsuite/test_errors.py
@@ -3,12 +3,14 @@ import pickle
 from pint import (
     DefinitionSyntaxError,
     DimensionalityError,
+    LogarithmicUnitCalculusError,
     OffsetUnitCalculusError,
     Quantity,
     RedefinitionError,
     UndefinedUnitError,
     UnitRegistry,
 )
+from pint.errors import LOG_ERROR_DOCS_HTML, OFFSET_ERROR_DOCS_HTML
 from pint.testsuite import BaseTestCase
 
 
@@ -79,13 +81,34 @@ class TestErrors(BaseTestCase):
         self.assertEqual(
             str(ex),
             "Ambiguous operation with offset unit (kilogram). See "
-            "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.",
+            + OFFSET_ERROR_DOCS_HTML
+            + " for guidance.",
         )
         ex = OffsetUnitCalculusError(Quantity("1 kg")._units, Quantity("1 s")._units)
         self.assertEqual(
             str(ex),
             "Ambiguous operation with offset unit (kilogram, second). See "
-            "https://pint.readthedocs.io/en/latest/nonmult.html for guidance.",
+            + OFFSET_ERROR_DOCS_HTML
+            + " for guidance.",
+        )
+
+    def test_logarithmic_unit_calculus_error(self):
+        Quantity = UnitRegistry(autoconvert_offset_to_baseunit=True).Quantity
+        ex = LogarithmicUnitCalculusError(Quantity("1 dB")._units)
+        self.assertEqual(
+            str(ex),
+            "Ambiguous operation with logarithmic unit (decibel). See "
+            + LOG_ERROR_DOCS_HTML
+            + " for guidance.",
+        )
+        ex = LogarithmicUnitCalculusError(
+            Quantity("1 dB")._units, Quantity("1 octave")._units
+        )
+        self.assertEqual(
+            str(ex),
+            "Ambiguous operation with logarithmic unit (decibel, octave). See "
+            + LOG_ERROR_DOCS_HTML
+            + " for guidance.",
         )
 
     def test_pickle_definition_syntax_error(self):
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index bc63bea..9881e67 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -520,7 +520,7 @@ class TestIssues(QuantityTestCase):
 
         lunar_module_height = Q_(10, "m")
         t1 = calculate_time_to_fall(lunar_module_height)
-        print(t1)
+        # print(t1)
         self.assertAlmostEqual(t1, Q_(1.4285714285714286, "s"))
 
         moon_gravity = Q_(1.625, "m/s^2")
@@ -580,7 +580,7 @@ class TestIssues(QuantityTestCase):
 
         @ureg.check("[length]", "[length]/[time]^2")
         def pendulum_period(length, G=Q_(1, "standard_gravity")):
-            print(length)
+            # print(length)
             return (2 * math.pi * (length / G) ** 0.5).to("s")
 
         length = Q_(1, ureg.m)
diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py
new file mode 100644
index 0000000..7d0928c
--- /dev/null
+++ b/pint/testsuite/test_log_units.py
@@ -0,0 +1,276 @@
+import math
+
+import pytest
+
+from pint import OffsetUnitCalculusError, UnitRegistry
+from pint.testsuite import QuantityTestCase
+from pint.unit import Unit, UnitsContainer
+
+
+@pytest.fixture(scope="module")
+def auto_ureg():
+    return UnitRegistry(autoconvert_offset_to_baseunit=True)
+
+
+@pytest.fixture(scope="module")
+def ureg():
+    return UnitRegistry()
+
+
+class TestLogarithmicQuantity(QuantityTestCase):
+
+    FORCE_NDARRAY = False
+
+    def test_log_quantity_creation(self):
+
+        # Following Quantity Creation Pattern
+        for args in (
+            (4.2, "dBm"),
+            (4.2, UnitsContainer(decibelmilliwatt=1)),
+            (4.2, self.ureg.dBm),
+        ):
+            x = self.Q_(*args)
+            self.assertEqual(x.magnitude, 4.2)
+            self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1))
+
+        x = self.Q_(self.Q_(4.2, "dBm"))
+        self.assertEqual(x.magnitude, 4.2)
+        self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1))
+
+        x = self.Q_(4.2, UnitsContainer(decibelmilliwatt=1))
+        y = self.Q_(x)
+        self.assertEqual(x.magnitude, y.magnitude)
+        self.assertEqual(x.units, y.units)
+        self.assertIsNot(x, y)
+
+        # Using multiplications for dB units requires autoconversion to baseunits
+        new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True)
+        x = new_reg.Quantity("4.2 * dBm")
+        self.assertEqual(x.magnitude, 4.2)
+        self.assertEqual(x.units, UnitsContainer(decibelmilliwatt=1))
+
+        with self.capture_log() as buffer:
+            self.assertEqual(4.2 * new_reg.dBm, new_reg.Quantity(4.2, 2 * new_reg.dBm))
+            self.assertEqual(len(buffer), 1)
+
+    def test_log_convert(self):
+        # # 1 dB = 1/10 * bel
+        # self.assertQuantityAlmostEqual(self.Q_(1.0, "dB").to("dimensionless"), self.Q_(1, "bell") / 10)
+        # # Uncomment Bell unit in default_en.txt
+
+        # ## Test dB to dB units octave - decade
+        # 1 decade = log2(10) octave
+        self.assertQuantityAlmostEqual(
+            self.Q_(1.0, "decade"), self.Q_(math.log(10, 2), "octave")
+        )
+        # ## Test dB to dB units dBm - dBu
+        # 0 dBm = 1mW = 1e3 uW = 30 dBu
+        self.assertAlmostEqual(self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu"))
+
+    def test_mix_regular_log_units(self):
+        # Test regular-logarithmic mixed definition, such as dB/km or dB/cm
+
+        # Multiplications and divisions with a mix of Logarithmic Units and regular Units is normally not possible.
+        # The reason is that dB are considered by pint like offset units.
+        # Multiplications and divisions that involve offset units are badly defined, so pint raises an error
+        with self.assertRaises(OffsetUnitCalculusError):
+            (-10.0 * self.ureg.dB) / (1 * self.ureg.cm)
+
+        # However, if the flag autoconvert_offset_to_baseunit=True is given to UnitRegistry, then pint converts the unit to base.
+        # With this flag on multiplications and divisions are now possible:
+        new_reg = UnitRegistry(autoconvert_offset_to_baseunit=True)
+        self.assertQuantityAlmostEqual(-10 * new_reg.dB / new_reg.cm, 0.1 / new_reg.cm)
+
+
+log_unit_names = [
+    "decibelmilliwatt",
+    "dBm",
+    "decibelmicrowatt",
+    "dBu",
+    "decibel",
+    "dB",
+    "decade",
+    "octave",
+    "oct",
+]
+
+
+@pytest.mark.parametrize("unit_name", log_unit_names)
+def test_unit_by_attribute(ureg, unit_name):
+    """Can the logarithmic units be accessed by attribute lookups?"""
+    unit = getattr(ureg, unit_name)
+    assert isinstance(unit, Unit)
+
+
+@pytest.mark.parametrize("unit_name", log_unit_names)
+def test_unit_parsing(ureg, unit_name):
+    """Can the logarithmic units be understood by the parser?"""
+    unit = ureg.parse_units(unit_name)
+    assert isinstance(unit, Unit)
+
+
+@pytest.mark.parametrize("mag", [1.0, 4.2])
+@pytest.mark.parametrize("unit_name", log_unit_names)
+def test_quantity_by_constructor(ureg, unit_name, mag):
+    """Can Quantity() objects be constructed using logarithmic units?"""
+    q = ureg.Quantity(mag, unit_name)
+    assert q.magnitude == pytest.approx(mag)
+    assert q.units == getattr(ureg, unit_name)
+
+
+@pytest.mark.parametrize("mag", [1.0, 4.2])
+@pytest.mark.parametrize("unit_name", log_unit_names)
+def test_quantity_by_multiplication(auto_ureg, unit_name, mag):
+    """Test that logarithmic units can be defined with multiplication
+
+    Requires setting `autoconvert_offset_to_baseunit` to True
+    """
+    unit = getattr(auto_ureg, unit_name)
+    q = mag * unit
+    assert q.magnitude == pytest.approx(mag)
+    assert q.units == unit
+
+
+@pytest.mark.parametrize(
+    "unit1,unit2",
+    [
+        ("decibelmilliwatt", "dBm"),
+        ("decibelmicrowatt", "dBu"),
+        ("decibel", "dB"),
+        ("octave", "oct"),
+    ],
+)
+def test_unit_equivalence(ureg, unit1, unit2):
+    """Are certain pairs of units equivalent?"""
+    assert getattr(ureg, unit1) == getattr(ureg, unit2)
+
+
+@pytest.mark.parametrize(
+    "db_value,scalar",
+    [
+        (0.0, 1.0),  # 0 dB == 1x
+        (-10.0, 0.1),  # -10 dB == 0.1x
+        (10.0, 10.0),
+        (30.0, 1e3),
+        (60.0, 1e6),
+    ],
+)
+def test_db_conversion(ureg, db_value, scalar):
+    """Test that a dB value can be converted to a scalar and back.
+    """
+    Q_ = ureg.Quantity
+    assert Q_(db_value, "dB").to("dimensionless").magnitude == pytest.approx(scalar)
+    assert Q_(scalar, "dimensionless").to("dB").magnitude == pytest.approx(db_value)
+
+
+@pytest.mark.parametrize(
+    "octave,scalar",
+    [
+        (2.0, 4.0),  # 2 octave == 4x
+        (1.0, 2.0),  # 1 octave == 2x
+        (0.0, 1.0),
+        (-1.0, 0.5),
+        (-2.0, 0.25),
+    ],
+)
+def test_octave_conversion(ureg, octave, scalar):
+    """Test that an octave can be converted to a scalar and back.
+    """
+    Q_ = ureg.Quantity
+    assert Q_(octave, "octave").to("dimensionless").magnitude == pytest.approx(scalar)
+    assert Q_(scalar, "dimensionless").to("octave").magnitude == pytest.approx(octave)
+
+
+@pytest.mark.parametrize(
+    "decade,scalar",
+    [
+        (2.0, 100.0),  # 2 decades == 100x
+        (1.0, 10.0),  # 1 octave == 2x
+        (0.0, 1.0),
+        (-1.0, 0.1),
+        (-2.0, 0.01),
+    ],
+)
+def test_decade_conversion(ureg, decade, scalar):
+    """Test that a decade can be converted to a scalar and back.
+    """
+    Q_ = ureg.Quantity
+    assert Q_(decade, "decade").to("dimensionless").magnitude == pytest.approx(scalar)
+    assert Q_(scalar, "dimensionless").to("decade").magnitude == pytest.approx(decade)
+
+
+@pytest.mark.parametrize(
+    "dbm_value,mw_value",
+    [
+        (0.0, 1.0),  # 0.0 dBm == 1.0 mW
+        (10.0, 10.0),
+        (20.0, 100.0),
+        (-10.0, 0.1),
+        (-20.0, 0.01),
+    ],
+)
+def test_dbm_mw_conversion(ureg, dbm_value, mw_value):
+    """Test dBm values can convert to mW and back.
+    """
+    Q_ = ureg.Quantity
+    assert Q_(dbm_value, "dBm").to("mW").magnitude == pytest.approx(mw_value)
+    assert Q_(mw_value, "mW").to("dBm").magnitude == pytest.approx(dbm_value)
+
+
+@pytest.mark.xfail
+def test_compound_log_unit_multiply_definition(auto_ureg):
+    """Check that compound log units can be defined using multiply.
+    """
+    Q_ = auto_ureg.Quantity
+    canonical_def = Q_(-161, "dBm") / auto_ureg.Hz
+    mult_def = -161 * auto_ureg("dBm/Hz")
+    assert mult_def == canonical_def
+
+
+@pytest.mark.xfail
+def test_compound_log_unit_quantity_definition(auto_ureg):
+    """Check that compound log units can be defined using ``Quantity()``.
+    """
+    Q_ = auto_ureg.Quantity
+    canonical_def = Q_(-161, "dBm") / auto_ureg.Hz
+    quantity_def = Q_(-161, "dBm/Hz")
+    assert quantity_def == canonical_def
+
+
+def test_compound_log_unit_parse_definition(auto_ureg):
+    Q_ = auto_ureg.Quantity
+    canonical_def = Q_(-161, "dBm") / auto_ureg.Hz
+    parse_def = auto_ureg("-161 dBm/Hz")
+    assert parse_def == canonical_def
+
+
+def test_compound_log_unit_parse_expr(auto_ureg):
+    """Check that compound log units can be defined using ``parse_expression()``.
+    """
+    Q_ = auto_ureg.Quantity
+    canonical_def = Q_(-161, "dBm") / auto_ureg.Hz
+    parse_def = auto_ureg.parse_expression("-161 dBm/Hz")
+    assert canonical_def == parse_def
+
+
+@pytest.mark.xfail
+def test_dbm_db_addition(auto_ureg):
+    """Test a dB value can be added to a dBm and the answer is correct.
+    """
+    power = (5 * auto_ureg.dBm) + (10 * auto_ureg.dB)
+    assert power.to("dBm").magnitude == pytest.approx(15)
+
+
+@pytest.mark.xfail
+@pytest.mark.parametrize(
+    "freq1,octaves,freq2",
+    [(100, 2.0, 400), (50, 1.0, 100), (200, 0.0, 200),],  # noqa: E231
+)
+def test_frequency_octave_addition(auto_ureg, freq1, octaves, freq2):
+    """Test an Octave can be added to a frequency correctly
+    """
+    freq1 = freq1 * auto_ureg.Hz
+    shift = octaves * auto_ureg.Octave
+    new_freq = freq1 + shift
+    assert new_freq.units == freq1.units
+    assert new_freq.magnitude == pytest.approx(freq2)