Logarithmic scales
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.Gain
— Typestruct 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.
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 Level
s for short:
Unitful.Level
— Typestruct 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.
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
:
Unitful.LogScaled
— Typeabstract type LogScaled{L<:LogInfo} <: Number end
Abstract supertype of Unitful.Level
and Unitful.Gain
. It is only used in promotion to put levels and gains onto a common log scale.
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.
* | 10 | Hz^-1 | dB | dBm | 1/Hz | 1mW | 3dB | 3dBm |
---|---|---|---|---|---|---|---|---|
10 | 100 | 10 Hz^-1 | 10 dB | 10.0 dBm | 10 Hz^-1 | 10 mW | 30 dB | 13.0 dBm |
Hz^-1 | Hz^-2 | dB Hz^-1 | dBm Hz^-1 | 1 Hz^-2 | 1 mW Hz^-1 | [3 dB] Hz^-1 | [3.0 dBm] Hz^-1 | |
dB | † | † | † | † | † | † | ||
dBm | † | † | † | † | † | |||
1/Hz | 1 Hz^-2 | 1 mW Hz^-1 | † ‡ | 1.99526 mW Hz^-1 | ||||
1mW | 1 mW^2 | 1.99526 mW | 1.99526 mW^2 | |||||
3dB | 6 dB | 6.0 dBm | ||||||
3dBm | 3.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 (Gain
s):
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.
+ | 100 | 20dB | 1Np | 10.0dBm | 10.0dBV | 1mW |
---|---|---|---|---|---|---|
100 | 200 | † | † | † | † | † |
20dB | 40 dB | † | 30.0 dBm | 30.0 dBV | † | |
1Np | 2 Np | 18.6859 dBm | 18.6859 dBV | † | ||
10.0dBm | 13.0103 dBm | † | 11.0 mW | |||
10.0dBV | 16.0206 dBV | † | ||||
1mW | 2 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.@logscale
— Macro@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)
API
Unitful.linear
— Functionlinear(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.
Unitful.reflevel
— Functionreflevel(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
Unitful.uconvertp
— Functionuconvertp(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
Unitful.uconvertrp
— Functionuconvertrp(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.