Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tweak Gain arithmetic #463

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 80 additions & 101 deletions docs/src/logarithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

linear is not yet introduced at this point, so users will not know what this means.

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
Expand Down Expand Up @@ -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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that “pure number” is not a good term here, but I also don’t like “scalar”. To me, scalar means “not a vector”, and even though that is technically correct (dimensionful quantities form a vector space), I think it doesn’t make this more clear. I would write “real number”, but I see that you replaced that as well. Can you explain why you prefer “scalar” over “real number”?

If we agree on “scalar” here, it should also be used in the first sentence in the “Constructing logarithmic quantities” section.

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.
Expand Down Expand Up @@ -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
giordano marked this conversation as resolved.
Show resolved Hide resolved

Multiplying a dimensionless logarithmic quantity by a pure number acts as like it does for
linear quantities:
For dimensionaless logarithmic units, addition behaves as one might expect:
giordano marked this conversation as resolved.
Show resolved Hide resolved

```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 units,
behave as their corresponding linear unit:
giordano marked this conversation as resolved.
Show resolved Hide resolved

```
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).
Comment on lines +193 to +194
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true. Level does actually store the linear value:

julia> @dB 100u"mW"/1u"mW"
20.0 dBm

julia> ans.val
100 mW


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:
Expand Down Expand Up @@ -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 units still behave as their corresponding linear unit,
giordano marked this conversation as resolved.
Show resolved Hide resolved
working with dimensionful units is entirely consistnet.
giordano marked this conversation as resolved.
Show resolved Hide resolved

The behavior of multiplication is summarized in the following table, with entries marked by
The behavior of addition and multiplication are summarized in the following table, with entries marked by
giordano marked this conversation as resolved.
Show resolved Hide resolved
† 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"]
Expand Down Expand Up @@ -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> linear(10u"dBm") * 20u"dB"
giordano marked this conversation as resolved.
Show resolved Hide resolved
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> linear(10u"dBm") + 20u"dB"
giordano marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Down
36 changes: 14 additions & 22 deletions src/logarithm.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -196,15 +197,14 @@ 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}
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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading