diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index 5bbd8e00..256b0aad 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -82,13 +82,10 @@ 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. +With a few exceptions, dimensionful logarithmic units, such as `dBm` behave just like the underlying +linear unit for purposes of arithmetic (i.e. arithmetic operations commute with `linear`). However, +note that they will still be displayed logarithmically. In contrast, arithmetic on dimensionless +logarithmic units (i.e. gains/attenuations) such as `dB` behaves logarithmically. This will be explored in more detail below. 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 @@ -134,11 +131,11 @@ logarithmic quantity is: Unitful.Gain ``` -One might expect that any gain / attenuation factor should be convertible to a pure number, +One might expect that any gain / attenuation factor should be convertible to a scalar, 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 +quantities is defined as `20*log10(y/z)`. Clearly, converting back from decibels to a scalar +is ambiguous, and so we have not implemented automatic promotion to avoid incorrect results. You can use [`Unitful.uconvertp`](@ref) to interpret a `Gain` as a ratio of power quantities (hence the `p` in `uconvertp`), or [`Unitful.uconvertrp`](@ref) to interpret as a ratio of root-power (field) quantities. @@ -167,34 +164,55 @@ Finally, for completeness we note that both `Level` and `Gain` are subtypes of ` Unitful.LogScaled ``` -## Multiplication rules +## Addition and multiplication rules -Multiplying a dimensionless logarithmic quantity by a pure number acts as like it does for -linear quantities: +For dimensionless logarithmic quantities, addition behaves as one might expect: ```jldoctest -julia> 3u"dB" * 2 -6 dB +julia> 10u"dB" + 10u"dB" +20 dB +``` -julia> 2 * 0u"dB" -0 dB +I.e. the gains add. However, as hinted at above, dimensionful logarithmic quantities +behave as their corresponding linear quantity: + +``` +julia> 10u"dBm" + 10u"dBm" +13.010299956639813 dBm + +julia> linear(10u"dBm") + linear(10u"dBm") +20.0 mW + +julia> uconvert(u"dBm", ans) +13.010299956639813 dBm ``` -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. +Note that this may seem strange from an arithmetic perspective, as written, but +the arithmetic is entirely consistent. It can be helpful to think of the arithmetic +as being performed on the linear units, with the logarithmic units simply being a +display hint (although the quantity being stored is indeed the displayed logarithmic +value). -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: +Multiplication by a scalar is consistent with the addition rules above: ```jldoctest +julia> 3u"dB" * 2 +6 dB + +julia> 3u"dB" + 3u"dB" +6 dB + +julia> 2 * 0u"dB" +0 dB + julia> 0u"dBm" * 2 3.010299956639812 dBm + +julia> 0u"dBm" + 0u"dBm" +3.010299956639812 dBm ``` -Logarithmic quantities can only be multiplied by pure numbers, linear units, or quantities, +Logarithmic quantities can only be multiplied by scalar, 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: @@ -228,14 +246,29 @@ 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`). +Since dimensionful logarithmic quantities still behave as their corresponding linear quantities, +working with dimensionful units is entirely consistent. -The behavior of multiplication is summarized in the following table, with entries marked by +The behavior of addition and multiplication is summarized in the following tables, with entries marked by † indicate prohibited operations. This table is populated automatically whenever the docs are built. +```@eval +using Latexify, Unitful +head = ["100", "20dB", "1Np", "10.0dBm", "10.0dBV", "1mW"] +side = ["+"; "**" .* head .* "**"] +quantities = uparse.(head) +tab = fill("", length(head), length(head)) +for col = eachindex(head), row = 1:col + try + tab[row, col] = string(quantities[row] + quantities[col]) + catch + tab[row, col] = "†" + end +end +mdtable(tab, latex=false, head=head, side=side) +``` + ```@eval using Latexify, Unitful head = ["10", "Hz^-1", "dB", "dBm", "1/Hz", "1mW", "3dB", "3dBm"] @@ -276,88 +309,34 @@ julia> 1u"V" * 20u"dB" 10.0 V ``` -## Addition rules +## Mixed Arithmetic -We can add logarithmic quantities without reference levels specified (`Gain`s): +One final question to answer is how arithmetic behaves when it involves both dimensionless +and dimensionful logarithmic units. The answer here is that in mixed arithmetic, both +dimensionless and dimensionful units are treated logarithmically. This is done for +convenience and can break commutativity and associativity, so should be probably avoided in generic +code. -```jldoctest -julia> 20u"dB" + 20u"dB" -40 dB ``` +julia> 10u"dBm" + 20u"dB" +30.0 dBm -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: - -```jldoctest -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: - -```jldoctest -julia> 10u"dBm" - 1u"Np" -1.3141103619349632 dBm -``` +julia> (10u"dBm" + 10u"dBm") + 20u"dB" +33.01029995663981 dBm -For logarithmic quantities with the same reference levels, the numbers out in front do not -simply add: +julia> 10u"dBm" + (10u"dBm" + 20u"dB") +30.043213737826427 dBm -```jldoctest -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: - -```jldoctest -julia> 20u"dBm" + @dB 1u"W"/u"W" -1.1 kg m^2 s^-3 -``` -i.e. `1.1 W`. +julia> 10u"dBm" * 20u"dB" +ERROR: ArgumentError: Multiplying a level by a Gain is disallowed. Use addition, or `linear` depending on context. -Rules for addition are summarized in the following table, with entries marked by † -indicating prohibited operations. This table is populated automatically whenever the docs -are built. +julia> 10u"mW" * 20u"dB" +1000.0 mW -```@eval -using Latexify, Unitful -head = ["100", "20dB", "1Np", "10.0dBm", "10.0dBV", "1mW"] -side = ["+"; "**" .* head .* "**"] -quantities = uparse.(head) -tab = fill("", length(head), length(head)) -for col = eachindex(head), row = 1:col - try - tab[row, col] = string(quantities[row] + quantities[col]) - catch - tab[row, col] = "†" - end -end -mdtable(tab, latex=false, head=head, side=side) +julia> 10u"mW" + 20u"dB" +ERROR: ArgumentError: Adding a gain to a linear quantity is disallowed. Use multiplication or convert to `Level` first ``` -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 diff --git a/src/logarithm.jl b/src/logarithm.jl index 58d76beb..a4968683 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -183,6 +183,7 @@ Base.hash(x::Level, h::UInt) = hash(x.val, h) for op in (:+, :-) @eval Base. $op(x::Level{L,S}, y::Level{L,S}) where {L,S} = Level{L,S}(($op)(x.val, y.val)) @eval Base. $op(x::Gain{L,S}, y::Gain{L,S}) where {L,S} = Gain{L,S}(($op)(x.val, y.val)) + @eval Base. $op(x::Gain{L,S}) where {L,S} = Gain{L,S}(($op)(x.val)) @eval function Base. $op(x::Gain{L,S1}, y::Gain{L,S2}) where {L,S1,S2} if S1 == :? return Gain{L,S2}(($op)(x.val, y.val)) @@ -196,7 +197,7 @@ for op in (:+, :-) Level{L,S}(fromlog(L, S, ($op)(ustrip(x), y.val))) end Base. +(x::Gain, y::Level) = +(y,x) -Base. -(x::Gain, y::Level) = throw(ArgumentError("cannot subtract a level from a gain.")) +Base. +(x::Level) = x # Multiplication and division leveltype(x::Level{L,S}) where {L,S} = Level{L,S} @@ -204,7 +205,6 @@ Base. *(x::Level, y::Number) = (leveltype(x))(x.val * y) Base. *(x::Level, y::Bool) = (leveltype(x))(x.val * y) # for method ambiguity Base. *(x::Level, y::Quantity) = *(x.val, y) Base. *(x::Level, y::Level) = *(x.val, y.val) -Base. *(x::Level, y::Gain) = *(promote(x,y)...) Base. *(x::Number, y::Level) = *(y,x) Base. *(x::Bool, y::Level) = *(y,x) # for method ambiguity @@ -214,32 +214,12 @@ gaintype(::Gain{L,S}) where {L,S} = Gain{L,S} Base. *(x::Gain, y::Number) = (gaintype(x))(x.val * y) Base. *(x::Gain, y::Bool) = (gaintype(x))(x.val * y) # for method ambiguity Base. *(x::Gain, y::Quantity) = *(y,x) -Base. *(x::Gain, y::Level) = *(promote(x,y)...) -Base. *(x::Gain, y::Gain) = *(promote(x,y)...) Base. *(x::Number, y::Gain) = *(y,x) Base. *(x::Bool, y::Gain) = *(y,x) # for method ambiguity Base. *(x::Quantity, y::Gain) = isrootpower(x) ? uconvertrp(NoUnits, y) * x : uconvertp(NoUnits, y) * x -for (op1,op2) in ((:*, :+), (:/, :-)) - @eval Base. $op1(x::Gain{L,S}, y::Gain{L,S}) where {L,S} = Gain{L,S}(($op2)(x.val, y.val)) - @eval function Base. $op1(x::Gain{L,S1}, y::Gain{L,S2}) where {L,S1,S2} - if S1 == :? - return Gain{L,S2}(($op2)(x.val, y.val)) - elseif S2 == :? - return Gain{L,S1}(($op2)(x.val, y.val)) - else - return Gain{L,:?}(($op2)(x.val, y.val)) - end - end - @eval Base. $op1(x::Level{L,S}, y::Gain{L}) where {L,S} = - Level{L,S}(fromlog(L, S, ($op2)(ustrip(x), y.val))) -end - -Base. *(x::Gain{L}, y::Level{L,S}) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(y)+x.val)) -Base. /(x::Gain, y::Level) = throw(ArgumentError("cannot divide a gain by a level.")) - Base. /(x::Level, y::Number) = (leveltype(x))(linear(x) / y) Base. //(x::Level, y::Number) = (leveltype(x))(linear(x) // y) Base. /(x::Level, y::Quantity) = linear(x) / y @@ -263,6 +243,18 @@ Base. //(x::Units, y::Gain) = x//linear(y) Base. isless(x::T, y::T) where {T<:LogScaled} = isless(x.val, y.val) +# Explicitly disallowed operations +Base. *(a::Level, b::Gain) = + throw(ArgumentError("Multiplying a level by a Gain is disallowed. Use addition, or `linear` depending on context.")) +Base. *(a::Gain, b::Gain) = + throw(ArgumentError("Multiplying gains is disallowed. Use addition to multiply the linear quantity.")) +Base. +(a::Quantity, b::Gain) = + throw(ArgumentError("Adding a gain to a linear quantity is disallowed. Use multiplication or convert to `Level` first")) +Base. /(x::Gain, y::Quantity) = throw(ArgumentError("Dividing a gain by a quantity is disallowed.")) +Base. -(x::Gain, y::Level) = throw(ArgumentError("cannot subtract a level from a gain.")) +Base. -(x::Level) = throw(ArgumentError("Levels cannot represent negative power. Negation not provided.")) + + function (Base.promote_rule(::Type{Level{L1,S1,T1}}, ::Type{Level{L2,S2,T2}}) where {L1,L2,S1,S2,T1,T2}) if L1 == L2 diff --git a/test/runtests.jl b/test/runtests.jl index c0803185..3ceea642 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1529,58 +1529,61 @@ end @test 1Np + 1.5Np == 2.5Np @test_throws DimensionError (1dBm + 1dBV) @test_throws DimensionError (1dBm + 1V) + @test +(1dBm) == 1dBm + @test_throws ArgumentError -(1dBm) end @testset ">> Gain" begin - for op in (:+, :*) - @test @eval ($op)(20dB, 10dB) === 30dB - @test @eval ($op)(20dB_rp, 10dB) === 30dB_rp - @test @eval ($op)(20dB, 10dB_rp) === 30dB_rp - @test @eval ($op)(20dB_p, 10dB) === 30dB_p - @test @eval ($op)(20dB, 10dB_p) === 30dB_p - @test @eval ($op)(20dB_rp, 10dB_p) === 30dB - @test @eval ($op)(20dB_p, 10dB_rp) === 30dB - @test_throws ErrorException @eval ($op)(1dB, 1Np) # no promotion - @test_throws ErrorException @eval ($op)(1dB_rp, 1Np) - end - for op in (:-, :/) - @test @eval ($op)(20dB, 10dB) === 10dB - @test @eval ($op)(20dB_rp, 10dB) === 10dB_rp - @test @eval ($op)(20dB, 10dB_rp) === 10dB_rp - @test @eval ($op)(20dB_p, 10dB) === 10dB_p - @test @eval ($op)(20dB, 10dB_p) === 10dB_p - @test @eval ($op)(20dB_rp, 10dB_p) === 10dB - @test @eval ($op)(20dB_p, 10dB_rp) === 10dB - @test_throws ErrorException @eval ($op)(1dB, 1Np) # no promotion - @test_throws ErrorException @eval ($op)(1dB_rp, 1Np) - end + @test +(20dB, 10dB) === 30dB + @test +(20dB_rp, 10dB) === 30dB_rp + @test +(20dB, 10dB_rp) === 30dB_rp + @test +(20dB_p, 10dB) === 30dB_p + @test +(20dB, 10dB_p) === 30dB_p + @test +(20dB_rp, 10dB_p) === 30dB + @test +(20dB_p, 10dB_rp) === 30dB + @test_throws ErrorException +(1dB, 1Np) # no promotion + @test_throws ErrorException +(1dB_rp, 1Np) + + @test -(20dB, 10dB) === 10dB + @test -(20dB_rp, 10dB) === 10dB_rp + @test -(20dB, 10dB_rp) === 10dB_rp + @test -(20dB_p, 10dB) === 10dB_p + @test -(20dB, 10dB_p) === 10dB_p + @test -(20dB_rp, 10dB_p) === 10dB + @test -(20dB_p, 10dB_rp) === 10dB + @test_throws ErrorException -(1dB, 1Np) # no promotion + @test_throws ErrorException -(1dB_rp, 1Np) + + @test -(10dB) === (-10)dB + @test +(10dB) === (10)dB end + linear_if_level(x) = isa(x, Level) ? linear(x) : x @testset ">> Level, meet Gain" begin - for op in (:+, :*) - @test @eval ($op)(10dBm, 30dB) == 40dBm - @test @eval ($op)(30dB, 10dBm) == 40dBm - @test @eval ($op)(10dBm, 30dB_rp) == 40dBm - @test @eval ($op)(30dB_rp, 10dBm) == 40dBm - @test @eval ($op)(10dBm, 30dB_p) == 40dBm - @test @eval ($op)(30dB_p, 10dBm) == 40dBm - @test @eval ($op)(0Np, 3dBm) == 3dBm + for op in (+, (a,b)->linear_if_level(a)*linear_if_level(b)) + @test op(10dBm, 30dB) == 40dBm + @test op(30dB, 10dBm) == 40dBm + @test op(10dBm, 30dB_rp) == 40dBm + @test op(30dB_rp, 10dBm) == 40dBm + @test op(10dBm, 30dB_p) == 40dBm + @test op(30dB_p, 10dBm) == 40dBm + @test op(0Np, 3dBm) == 3dBm end - for op in (:-, :/) - @test @eval ($op)(10dBm, 30dB) == -20dBm - @test @eval ($op)(10dBm, 30dB_rp) == -20dBm - @test @eval ($op)(10dBm, 30dB_p) == -20dBm - @test @eval isapprox(($op)(10dBm, 1Np), 1.314dBm; atol=0.001dBm) - @test @eval isapprox(($op)(10dBm, 1Np_rp), 1.314dBm; atol=0.001dBm) - @test @eval isapprox(($op)(10dBm, 1Np_p), 1.314dBm; atol=0.001dBm) + for op in (-, (a,b)->linear_if_level(a)/linear_if_level(b)) + @test op(10dBm, 30dB) == -20dBm + @test op(10dBm, 30dB_rp) == -20dBm + @test op(10dBm, 30dB_p) == -20dBm + @test isapprox(op(10dBm, 1Np), 1.314dBm; atol=0.001dBm) + @test isapprox(op(10dBm, 1Np_rp), 1.314dBm; atol=0.001dBm) + @test isapprox(op(10dBm, 1Np_p), 1.314dBm; atol=0.001dBm) # cannot subtract Levels from Gains - @test_throws ArgumentError @eval ($op)(10dB, 30dBm) - @test_throws ArgumentError @eval ($op)(10dB_rp, 30dBm) - @test_throws ArgumentError @eval ($op)(10dB_p, 30dBm) - @test_throws ArgumentError @eval ($op)(1Np, 10dBm) - @test_throws ArgumentError @eval ($op)(1Np_rp, 10dBm) - @test_throws ArgumentError @eval ($op)(1Np_p, 10dBm) + @test_throws ArgumentError op(10dB, 30dBm) + @test_throws ArgumentError op(10dB_rp, 30dBm) + @test_throws ArgumentError op(10dB_p, 30dBm) + @test_throws ArgumentError op(1Np, 10dBm) + @test_throws ArgumentError op(1Np_rp, 10dBm) + @test_throws ArgumentError op(1Np_p, 10dBm) end end end @@ -1603,7 +1606,8 @@ end @test false*3dBm == -Inf*dBm @test 3dBm*true == 3dBm @test 3dBm*false == -Inf*dBm - @test (0dBV)*(1Np) ≈ 8.685889638dBV + @test_throws ArgumentError (0dBV)*(1Np) + @test linear(0dBV)*(1Np) ≈ 8.685889638dBV @test dBm/5 ≈ 0.2dBm @test linear((@dB 3W/W)//3) === 1W//1 @test (@dB 3W/W)//(@dB 3W/W) === 1//1