Logarithmic scales

Note

Logarithmic scales should be considered experimental because they break some of the basic assumptions about equality and hashing (see #402)

Unitful provides a way to use logarithmically-scaled quantities. Some compromises have been made in striving for logarithmic quantities to be both usable and consistent. In the following discussion, for pedagogical purposes, we will assume prior familiarity with the definitions of dB and dBm.

Constructing logarithmic quantities

Left- or right-multiplying a pure number by a logarithmic "unit", whether dimensionful or dimensionless, is short-hand for constructing a logarithmic quantity.

julia> 3u"dB"
3 dB

julia> 3u"dBm"
3.0 dBm

julia> u"dB"*3 === 3u"dB"
true

Currently implemented are dB, B, dBm, dBV, dBu, dBμV, dBSPL, dBFS, cNp, Np.

One can also construct logarithmic quantities using the @dB, @B, @cNp, @Np macros to use an arbitrary reference level:

julia> using Unitful: mW, V

julia> @dB 10mW/mW
10.0 dBm

julia> @dB 10V/V
20.0 dBV

julia> @dB 3V/4V
-2.498774732165999 dB (4 V)

julia> @Np ℯ*V/V    # ℯ = 2.71828...
1.0 Np (1 V)

These macros are exported by default since empirically macros are defined less often than variables and generic functions. When using the macros, the levels are constructed at parse time. The scales themselves are callable as functions if you need to construct a level that way (they are not exported):

julia> using Unitful: dB, mW, V

julia> dB(10mW,mW)
10.0 dBm

In calculating the logarithms, the log function appropriate to the scale in question is used (log10 for decibels, log for nepers).

There is an important difference in these two approaches to constructing logarithmic quantities. When we construct 0dBm, the power in mW is calculated and stored, resulting in a lossy floating-point conversion. This can be avoided by constructing 0 dBm as @dB 1mW/mW.

It is important to keep in mind that the reference level is just used to calculate the logarithms, and nothing more. When there is ambiguity about what to do, we fall back to the underlying linear quantities, paying no mind to the reference levels:

julia> using Unitful: mW

julia> (@dB 10mW/1mW) + (@dB 10mW/2mW)
20 mW

Addition will be discussed more later.

Note that logarithmic "units" can only multiply or be multiplied by pure numbers and linear units, not other logarithmic units or quantities. This is done to avoid issues with commutativity and associativity, e.g. 3*dB*m^-1 == (3dB)/m, but 3*m^-1*dB == (3m^-1)*dB does not make much sense. This is because dB acts more like a constructor than a proper unit.

The @dB and @Np macros will fail if either a dimensionless number or a ratio of dimensionless numbers is used. This is because the ratio could be of power quantities or of root-power quantities, leading to ambiguities. After all, usually it is the ratio that is dimensionless, not the numerator and denominator that make up the ratio. In some cases it may nonetheless be convenient to have a dimensionless reference level. By providing an extra Bool argument to these macros, you can explicitly choose whether the resulting ratio should be considered a "root-power" or "power" ratio. You can only do this for dimensionless numbers:

julia> @dB 10/1 true   # is a root-power (amplitude) ratio
20.0 dBFS

julia> @dB 10/1 false  # is not a root-power ratio; is a power ratio
10.0 dB (power ratio with reference 1)

Note that dBFS is defined to represent amplitudes relative to 1 in dB, hence the custom display logic.

Also, you can of course use functions instead of macros:

julia> using Unitful: dB, mW

julia> dB(10,1,true)
20.0 dBFS

julia> dB(10mW,mW,true)
ERROR: ArgumentError: when passing a final Bool argument, this can only be used with dimensionless numbers.
[...]

Logarithmic quantities with no reference level specified

Logarithmic quantities with no reference level specified typically represent some amount of gain or attenuation, i.e. a ratio which is dimensionless. These can be constructed as, for example, 10*dB, which displays similarly (10 dB). The type of this kind of logarithmic quantity is:

Unitful.GainType
struct Gain{L, S, T<:Real} <: LogScaled{L}

A logarithmic scale-based gain or attenuation factor. This type has one field, val::T. For example, given a gain of 20dB, we have val===20. This type differs from Unitful.Level in that val is stored after computing the logarithm.

source

One might expect that any gain / attenuation factor should be convertible to a pure number, that is, to x == y/z if you had 10*log10(x) dB. However, it turns out that in dB, a ratio of powers is defined as 10*log10(y/z), but a ratio of voltages or other root-power quantities is defined as 20*log10(y/z). Clearly, converting back from decibels to a real number is ambiguous, and so we have not implemented automatic promotion to avoid incorrect results. You can use Unitful.uconvertp to interpret a Gain as a ratio of power quantities (hence the p in uconvertp), or Unitful.uconvertrp to interpret as a ratio of root-power (field) quantities.

"Dimensionful" logarithmic quantities?

In this package, quantities with units like dBm are considered to have the dimension of power, even though the expression P(dBm) = 10*log10(P/1mW) is dimensionless and formed from a dimensionless ratio. Practically speaking, these kinds of logarithmic quantities are fungible whenever they share the same dimensions, so it is more convenient to adopt this convention (people refer to dBm/Hz as a power spectral density, etc.) Presumably, one would like to have 10dBm isa Unitful.Power for dispatch too. Therefore, in the following discussion, we will shamelessly (okay, with some shame) speak of dimensionful logarithmic quantities, or Levels for short:

Unitful.LevelType
struct Level{L, S, T<:Union{Real, AbstractQuantity{<:Real}}} <: LogScaled{L}

A logarithmic scale-based level. Details about the logarithmic scale are encoded in L <: LogInfo. S is a reference quantity for the level, not a type. This type has one field, val::T, and the log of the ratio val/S is taken. This type differs from Unitful.Gain in that val is a linear quantity.

source

Actually, the defining characteristic of a Level is that it has a reference level, which may or may not be dimensionful. It usually is, but is not in the case of e.g. dBFS.

Finally, for completeness we note that both Level and Gain are subtypes of LogScaled:

Multiplication rules

Multiplying a dimensionless logarithmic quantity by a pure number acts as like it does for linear quantities:

julia> 3u"dB" * 2
6 dB

julia> 2 * 0u"dB"
0 dB

Justification by example: consider the example of the exponential attenuation of a signal on a lossy transmission line. If the attenuation goes like $10^{-kx}$, then the (power) attenuation in dB is $-10kx$. We see that the attenuation in dB is linear in length. For an attenuation constant of 3dB/m, we better calculate 6dB for a length of 2m.

Multiplying a dimensionful logarithmic quantity by a pure number acts differently than multiplying a gain/attenuation by a pure number. Since 0dBm == 1mW, we better have that 0dBm * 2 == 2mW, implying:

julia> 0u"dBm" * 2
3.010299956639812 dBm

Logarithmic quantities can only be multiplied by pure numbers, linear units, or quantities, but not logarithmic "units" or quantities. When a logarithmic quantity is multiplied by a linear quantity, the logarithmic quantity is linearized and multiplication proceeds as usual:

julia> (0u"dBm") * (1u"W")
1.0 mW W

The previous example returns a floating point value because in constructing the level 0 dBm, the power in mW is calculated and stored, entailing a floating point conversion. This can be avoided by constructing 0 dBm as @dB 1mW/mW:

julia> (@dB 1u"mW"/u"mW") * (1u"W")
1 mW W

We refer to a quantity with both logarithmic "units" and linear units as a mixed quantity. For mixed quantities, the numeric value associates with the logarithmic unit, and the quantity is displayed in a way that makes this explicit:

julia> (0u"dBm")/u"Hz"
[0.0 dBm] Hz^-1

julia> (0u"dB")/u"Hz"
[0 dB] Hz^-1

julia> 0u"dB/Hz"
[0 dB] Hz^-1

Mathematical operations are forwarded to the logarithmic part, so that for example, 100*((0dBm)/s) == (20dBm)/s. We allow linear units to commute with logarithmic quantities for convenience, though the association is understood (e.g. s^-1*(3dBm) == (3dBm)/s).

The behavior of multiplication is summarized in the following table, with entries marked by † indicating prohibited operations.

*10Hz^-1dBdBm1/Hz1mW3dB3dBm
1010010 Hz^-110 dB10.0 dBm10 Hz^-110 mW30 dB13.0 dBm
Hz^-1Hz^-2dB Hz^-1dBm Hz^-11 Hz^-21 mW Hz^-1[3 dB] Hz^-1[3.0 dBm] Hz^-1
dB
dBm
1/Hz1 Hz^-21 mW Hz^-1† ‡1.99526 mW Hz^-1
1mW1 mW^21.99526 mW1.99526 mW^2
3dB6 dB6.0 dBm
3dBm3.98107 mW^2

‡: 1/Hz * 3dB could be allowed, technically, but we throw an error if it's unclear whether a quantity is a root-power or power quantity:

julia> u"1/Hz" * u"3dB"
ERROR: undefined behavior. Please file an issue with the code needed to reproduce.

On the other hand, if it can be determined that a power quantity or root-power quantity is being multiplied by a gain, then the gain is interpreted as a power ratio or root-power ratio, respectively:

julia> 1u"mW" * 20u"dB"
100.0 mW

julia> 1u"V" * 20u"dB"
10.0 V

Addition rules

We can add logarithmic quantities without reference levels specified (Gains):

julia> 20u"dB" + 20u"dB"
40 dB

The numbers out front of the dB just add: when we talk about gain or attenuation, we work in logarithmic units so that we can add rather than multiply gain factors. The same behavior holds when we add a Gain to a Level or vice versa:

julia> 20u"dBm" + 20u"dB"
40.0 dBm

In the case where you have differing logarithmic scales for the Level and the Gain, the logarithmic scale of the Level is used for the result:

julia> 10u"dBm" - 1u"Np"
1.3141103619349632 dBm

For logarithmic quantities with the same reference levels, the numbers out in front do not simply add:

julia> 20u"dBm" + 20u"dBm"
23.010299956639813 dBm

julia> 2 * 20u"dBm"
23.010299956639813 dBm

This is because dBm represents a power, ultimately. If we have some amount of power and we double it, we'd better get roughly 3 dB more power. Note that the juxtaposition 20dBm will ensure that 20 dBm is constructed before multiplication by 2 in the above example. If you were to type 2*20*dBm, you'd get 40 dBm.

If the reference levels differ but both levels represent a power, we fall back to linear quantities:

julia> 20u"dBm" + @dB 1u"W"/u"W"
1.1 kg m^2 s^-3

i.e. 1.1 W.

Rules for addition are summarized in the following table, with entries marked by † indicating prohibited operations.

+10020dB1Np10.0dBm10.0dBV1mW
100200
20dB40 dB30.0 dBm30.0 dBV
1Np2 Np18.6859 dBm18.6859 dBV
10.0dBm13.0103 dBm11.0 mW
10.0dBV16.0206 dBV
1mW2 mW

Notice that we disallow implicit conversions between dimensionless logarithmic quantities and real numbers. This is because the results can depend on promotion rules in addition to being ambiguous because of the root-power vs. power ratio issue. If 100 + 10dB were evaluated as 20dB + 10dB == 30dB, then we'd get 1000, but if it were evaluated as 100+10, we'd get 110.

Also, although it is possible in principle to add e.g. 20dB + 1Np, notice that we have not implemented that because it is unclear whether the result should be in nepers or decibels, and it is also unclear how to handle that question more generally as other logarithmic scales are introduced.

Conversion

As alluded to earlier, conversions can be tricky because so-called logarithmic units are not units in the conventional sense.

You may use linear to convert to a linear scale when you have a Level or Quantity{<:Level} type. There is a fallback for Number, which just returns the number.

julia> linear(@dB 10u"mW"/u"mW")
10 mW

julia> linear(20u"dBm/Hz")
100.0 mW Hz^-1

julia> linear(30u"W")
30 W

julia> linear(12)
12

Linearizing a Quantity{<:Gain} or a Gain to a real number is ambiguous, because the real number may represent a ratio of powers or a ratio of root-power (field) quantities. We implement Unitful.uconvertp and Unitful.uconvertrp which may be thought of as disambiguated uconvert functions. There is a one argument version that assumes you are converting to a unitless number. These functions can take either a Gain or a Real so that they may be used somewhat generically.

julia> uconvertrp(NoUnits, 20u"dB")
10.0

julia> uconvertp(NoUnits, 20u"dB")  
100.0

julia> uconvertp(u"dB", 100)
20.0 dB

julia> uconvertp(u"Np", ℯ^2)
1.0 Np

julia> uconvertrp(u"Np", ℯ)
1//1 Np

Notation

This package displays logarithmic quantities using shorthand like dBm where available. This should probably not be done in polite company. To quote "Guide for the Use of the International System of Units (SI)," NIST Special Pub. 811 (2008):

The rules of Ref. [5: IEC 60027-3] preclude, for example, the use of the symbol dBm to indicate a reference level of power of 1 mW. This restriction is based on the rule of Sec. 7.4, which does not permit attachments to unit symbols.

The authorities say the reference level should always specified. In practice, this hasn't stopped the use of dBm and the like on commercially available test equipment. Dealing with these units is unavoidable in practice. When no shorthand exists, we follow NIST's advice in displaying logarithmic quantities:

When such data are presented in a table or in a figure, the following condensed notation may be used instead: -0.58 Np (1 μV/m); 25 dB (20 μPa).

Custom logarithmic scales

Unitful.@logscaleMacro
@logscale(symb,abbr,name,base,prefactor,irp)

Define a logarithmic scale. Unlike with units, there is no special treatment for power-of-ten prefixes (decibels and bels are defined separately). However, arbitrary bases are possible, and computationally appropriate log and exp functions are used in calculations when available (e.g. log2, log10 for base 2 and base 10, respectively).

This macro defines a MixedUnits object identified by symbol symb. This can be used to construct Gain types, e.g. 3*dB. Additionally, two other MixedUnits objects are defined, with symbols Symbol(symb,"_rp") and Symbol(symb,"_p"), e.g. dB_rp and dB_p, respectively. These objects serve nearly the same purpose, but have extra information in their type that indicates whether they should be considered as root-power ratios or power ratios upon conversion to pure numbers.

This macro also defines another macro available as @symb. For example, @dB in the case of decibels. This can be used to construct Level objects at parse time. Usage is like @dB 3V/1V.

prefactor is the prefactor out in front of the logarithm for this log scale. In all cases it is defined with respect to taking ratios of power quantities. Just divide by two if you want to refer to root-power / field quantities instead.

irp (short for "is root power?") specifies whether the logarithmic scale is defined with respect to ratios of power or root-power quantities. In short: use false if your scale is decibel-like, or true if your scale is neper-like.

Examples:

julia> using Unitful: V, W

julia> @logscale dΠ "dΠ" Decipies π 10 false
dΠ

julia> @dΠ π*V/1V
20.0 dΠ (1 V)

julia> dΠ(π*V, 1V)
20.0 dΠ (1 V)

julia> @dΠ π^2*V/1V
40.0 dΠ (1 V)

julia> @dΠ π*W/1W
10.0 dΠ (1 W)
source

API

Unitful.linearFunction
linear(x::Quantity)
linear(x::Level)
linear(x::Number) = x

Returns a quantity equivalent to x but without any logarithmic scales.

It is important to note that this operation will error for Quantity{<:Gain} types. This is for two reasons:

  • 20dB could be interpreted as either a power or root-power ratio.
  • Even if -20dB/m were interpreted as, say, 0.01/m, this means something fundamentally different than -20dB/m. 0.01/m cannot be used to calculate exponential attenuation.
source
Unitful.reflevelFunction
reflevel(x::Level{L,S})
reflevel(::Type{Level{L,S}})
reflevel(::Type{Level{L,S,T}})

Returns the reference level, e.g.

julia> reflevel(3u"dBm")
1 mW
source
Unitful.uconvertpFunction
uconvertp(u::Units, x)
uconvertp(u::MixedUnits, x)

Generically, this is the same as Unitful.uconvert. In cases where unit conversion would be ambiguous without further information (e.g. uconvert(dB, 10)), uconvertp presumes ratios are of power quantities.

It is important to note that careless use of this function can lead to erroneous calculations. Consider Quantity{<:Gain} types: it is tempting to use this to transform -20dB/m into 0.1/m, however this means something fundamentally different than -20dB/m. Consider what happens when you try to compute exponential attenuation by multiplying 0.1/m by a length.

Examples:

julia> using Unitful

julia> uconvertp(u"dB", 10)
10.0 dB

julia> uconvertp(NoUnits, 20u"dB")
100.0
source
Unitful.uconvertrpFunction
uconvertrp(u::Units, x)
uconvertrp(u::MixedUnits, x)

In most cases, this is the same as Unitful.uconvert. In cases where unit conversion would be ambiguous without further information (e.g. uconvert(dB, 10)), uconvertrp presumes ratios are of root-power quantities.

It is important to note that careless use of this function can lead to erroneous calculations. Consider Quantity{<:Gain} types: it is tempting to use this to transform -20dB/m into 0.01/m, however this means something fundamentally different than -20dB/m. Consider what happens when you try to compute exponential attenuation by multiplying 0.01/m by a length.

source