From 36aa3a56bb77b57fbcf36ad89a3d779e0584dea2 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Mon, 7 Aug 2017 21:28:58 -0700 Subject: [PATCH 01/12] Implement logarithmic quantities. Reorganize code. --- .travis.yml | 4 +- NEWS.md | 11 + docs/make.jl | 2 +- docs/mkdocs.yml | 5 +- docs/src/highlights.md | 29 +- docs/src/index.md | 12 +- docs/src/logarithm.md | 367 +++++++++++++++ docs/src/newunits.md | 5 + docs/src/trouble.md | 3 +- src/Unitful.jl | 1016 +--------------------------------------- src/conversion.jl | 2 +- src/dimensions.jl | 87 ++++ src/logarithm.jl | 397 ++++++++++++++++ src/pkgdefaults.jl | 2 + src/promotion.jl | 59 +++ src/quantities.jl | 357 ++++++++++++++ src/range.jl | 4 + src/temperature.jl | 7 + src/types.jl | 95 +++- src/units.jl | 235 ++++++++++ src/user.jl | 1 + src/utils.jl | 191 ++++++++ test/runtests.jl | 220 ++++++++- 23 files changed, 2046 insertions(+), 1065 deletions(-) create mode 100644 docs/src/logarithm.md create mode 100644 src/dimensions.jl create mode 100644 src/logarithm.jl create mode 100644 src/quantities.jl create mode 100644 src/units.jl create mode 100644 src/utils.jl diff --git a/.travis.yml b/.travis.yml index 2ae7b12d..d9deb6d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: julia os: - linux - - osx +# - osx # locally test on my MacBook Pro; OS X tests take too long on Travis CI julia: - 0.6 - nightly @@ -19,6 +19,6 @@ after_success: # push coverage results to Coveralls - julia -e 'cd(Pkg.dir("Unitful")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder())' # push coverage results to Codecov - - julia -e 'cd(Pkg.dir("Unitful")); Pkg.add("Coverage"); using Coverage; Codecov.submit(process_folder())' + - julia -e 'cd(Pkg.dir("Unitful")); using Coverage; Codecov.submit(process_folder())' - julia -e 'Pkg.add("Documenter")' - julia -e 'cd(Pkg.dir("Unitful")); include(joinpath("docs", "make.jl"))' diff --git a/NEWS.md b/NEWS.md index 8ead687d..39e6065f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,14 @@ +- v0.4.0 + - Introduce logarithmic quantities. + - Update syntax for Julia 0.6 and reorganize code for clarity. + - Redefine `ustrip(x::Quantity) = ustrip(x.val)`. In most cases, this is unlikely to + affect user code. The generic fallback `ustrip(x::Number)` remains unchanged. + - `isapprox(1.0u"m",5)` returns `false` instead of throwing a `DimensionError`, + in keeping with the behavior of an equality check (`==`). + - Deprecated `dimension(x::AbstractArray{T}) where T<:Number`, use broadcasting instead. + - Deprecated `dimension(x::AbstractArray{T}) where T<:Units`, use broadcasting instead. + - Deprecated `ustrip(A::AbstractArray{T}) where T<:Number`, use broadcasting instead. + - Deprecated `ustrip(A::AbstractArray{T}) where T<:Quantity`, use broadcasting instead. - v0.3.0 - Require Julia 0.6 - Adds overloads for `rand` and `ones` [#96](https://github.com/ajkeller34/Unitful.jl/issues/96). diff --git a/docs/make.jl b/docs/make.jl index dc7861e9..e8c78bf3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -4,7 +4,7 @@ makedocs() deploydocs( deps = Deps.pip("mkdocs", "mkdocs-material", "python-markdown-math"), - julia = "nightly", + julia = "0.6", osname = "linux", repo = "github.com/ajkeller34/Unitful.jl.git" ) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 4ad18989..43ed8fc5 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,7 +1,7 @@ site_name: Unitful.jl repo_url: https://github.com/ajkeller34/Unitful.jl -site_description: Package for units. -site_author: Andrew Keller +site_description: Performant and expressive calculations with arbitrary units. +site_author: Andrew J. Keller theme: material extra: @@ -33,6 +33,7 @@ pages: - Conversion / promotion: conversion.md - Manipulating units: manipulations.md - How units are displayed: display.md +- Logarithmic scales: logarithm.md - Extending Unitful: extending.md - Troubleshooting: trouble.md - License: LICENSE.md diff --git a/docs/src/highlights.md b/docs/src/highlights.md index f38f2fe3..68a90bdc 100644 --- a/docs/src/highlights.md +++ b/docs/src/highlights.md @@ -9,21 +9,21 @@ end Consider the following toy example, converting from voltage or power ratios to decibels: ```jldoctest -julia> dB(num::Unitful.Voltage, den::Unitful.Voltage) = 20*log10(num/den) - dB (generic function with 1 method) +julia> whatsit(x::Unitful.Voltage) = "voltage!" + whatsit (generic function with 1 method) -julia> dB(num::Unitful.Power, den::Unitful.Power) = 10*log10(num/den) - dB (generic function with 2 methods) +julia> whatsit(x::Unitful.Length) = "length!" + whatsit (generic function with 2 methods) -julia> dB(1u"mV", 1u"V") --60.0 +julia> whatsit(1u"mm") +"length!" -julia> dB(1u"mW", 1u"W") --30.0 -``` +julia> whatsit(1u"kV") +"voltage!" -We don't currently implement dB as a unit because the log scale would require -special treatment, but it is under consideration. +julia> whatsit(1u"A" * 2.5u"Ω") +"voltage!" +``` ### Dimensions in a type definition @@ -79,6 +79,13 @@ julia> Diagonal([-1.0u"c^2", 1.0, 1.0, 1.0]) ⋅ ⋅ ⋅ 1.0 ``` +## Logarithmic units + +```jldoctest +julia> uconvert(u"mW*s", 20u"dBm/Hz") +100.0 s mW +``` + ## Units with rational exponents ```jldoctest diff --git a/docs/src/index.md b/docs/src/index.md index 8abc2876..d73ee1e8 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,19 +16,11 @@ We want to support not only SI units but also any other unit system. We also want to minimize or in some cases eliminate the run-time penalty of units. There should be facilities for dimensional analysis. All of this should integrate easily with the usual mathematical operations and collections -that are found in Julia base. - -!!! note - If you have been previously using Unitful 0.0.4 or earlier, the defaults - mechanism has been completely rewritten. This means you will now need to - define new units in your `.juliarc.jl` file or in a new module. See - [Defining new units](@ref) and [Extending Unitful](@ref) for details. - Additionally, to specify custom rules for promotion, see - [Basic promotion mechanisms](@ref) and [Advanced promotion mechanisms](@ref). +that are defined in Julia. ## Quick start -- This package requires Julia 0.5. Older versions will not be supported. +- This package requires Julia 0.6. Older versions will not be supported. - `Pkg.add("Unitful")` - `using Unitful` diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md new file mode 100644 index 00000000..ef72f9af --- /dev/null +++ b/docs/src/logarithm.md @@ -0,0 +1,367 @@ +```@meta +DocTestSetup = quote + using Unitful +end +``` + +Unitful provides a way to use logarithmically-scaled quantities as of v0.4.0. 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. + +```jldoctest +julia> 3u"dB" +3 dB + +julia> 3u"dBm" +3.0 dBm + +julia> u"dB"*3 === 3u"dB" +true +``` + +Currently implemented are `dB`, `dBm`, `dBV`, `dBu`, `dBμV`, `dBSPL`, `Np`. + +One can also construct logarithmic quantities using the `@dB` or `@Np` macros to use +an arbitrary reference level: + +```jldoctest +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 e*V/V # e = 2.71828... +1.0 Np (1 V) +``` + +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 `3dBm`, ultimately the power in `mW` is being stored, +resulting in a lossy conversion. However, +`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`. + +Note that logarithmic "units" can only multiply or be multiplied by pure numbers, not +other 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. In this package and in the +documentation, we take some pains to avoid using the term "logarithmic units" where possible, +and the usage and design of this package reflects that. + +### Logarithmic quantities with no reference level specified + +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. + +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: + +```@docs + Unitful.Gain +``` + +One might expect that any dimensionless quantity should be convertible to a pure number, +that is, to `x` if you had `10*log10(x)` dB. However, it turns out that in dB, a ratio of +powers is defined as `10*log10(x)`, but a ratio of voltages or other root-power quantities +is defined as `20*log10(x)`. 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.powerratio`](@ref) to interpret a `Gain` as a ratio of power +quantities, or [`Unitful.rootpowerratio`](@ref) (equivalently `fieldratio`) to interpret +as a ratio of 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: + +```@docs + Unitful.Level +``` + +Finally, for completeness we note that both `Level` and `Gain` are subtypes of `LogScaled`: + +```@docs + Unitful.LogScaled +``` + +## Multiplication rules + +Multiplying a dimensionless logarithmic quantity by a pure number acts as like it does for +linear quantities: + +```jldoctest +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: + +```jldoctest +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: + +```jldoctest +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`: + +```jldoctest +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: + +```jldoctest +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 (entries marked by † +indicate prohibited operations): + +| * | 10 | Hz^-1 | dB | dBm | 1/Hz | 1mW | 3dB | 3dBm | +| ------------------- | ---- | ----- | ----- | ------ | ------ | ------- | -------- | --------- | +| **10** | 100 | 10/s | 10dB | 10dBm | 10/s | 10mW | 30dB | 13dBm | +| **Hz^-1** (unit) | | Hz^-2 | † | † | 1/Hz^2 | 1mW/Hz | (3dB)/Hz | (3dBm)/Hz | +| **dB** | | | † | † | † | † | † | † | +| **dBm** | | | | † | † | † | † | † | +| **1/Hz** (quantity) | | | | | 1/Hz^2 | 1mW/Hz | ‡ | ≈ 2mW/Hz | +| **1mW** (quantity) | | | | | | 1mW^2 | ≈2mW | ≈ 2mW^2 | +| **3dB** | | | | | | | † | † | +| **3dBm** | | | | | | | | ≈ 4mW^2 | + +‡: `1/Hz * 3dB` is technically allowed but dumb things can happen when its unclear if a quantity +is a root-power or power quantity: + +```jldoctest +julia> 1/u"Hz" * 20u"dB" +WARNING: result may be incorrect. Define `Unitful.isrootpower(::Type{<:Unitful.LogInfo}, ::typeof(𝐓))` to fix. +100.0 Hz^-1 +``` + +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: + +```jldoctest +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): + +```jldoctest +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: + +```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 +``` + +For logarithmic quantities with the same reference levels, the numbers out in front do not +simply add: + +```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`. + +Rules for addition are summarized in the following table (entries marked by † +indicate prohibited operations): + +| + | 100 | 20dB | 1Np | 10.0dBm | 10.0dBV | 1mW | +| ----------- | ------- | ------- | ------- | -------- | -------- | -------- | +| **100** | 200 | † | † | † | † | † | +| **20dB** | | 40dB | † | 30.0dBm | 30.0dBV | † | +| **1Np** | | | 2Np | ≈18.7dBm | ≈18.7dBV | ≈7.39mW | +| **10.0dBm** | | | | ≈13dBm | † | 11.0mW | +| **10.0dBV** | | | | | ≈16.0dBV | † | +| **1mW** | | | | | | 2mW | + +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`](@ref) 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. + +```jldoctest +julia> linear(@dB 10u"mW"/u"mW") +10 mW + +julia> linear(20u"dBm/Hz") +100.0 Hz^-1 mW + +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.powerratio`](@ref) and [`Unitful.rootpowerratio`](@ref) 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. + +```jldoctest +julia> fieldratio(NoUnits, 20u"dB") # the first argument is optional when it is `NoUnits` +10.0 + +julia> fieldratio(20u"dB") +10.0 + +julia> powerratio(NoUnits, 20u"dB") +100.0 + +julia> powerratio(u"dB", 100) +20.0 dB + +julia> powerratio(u"Np", e^2) +1.0 Np + +julia> fieldratio(u"Np", e) +1//1 Np +``` + +To save typing you can use `fieldratio` instead of `rootpowerratio`, +although according to the infallible source +[Wikipedia](https://en.wikipedia.org/wiki/Decibel#Field_quantities_and_root-power_quantities): + +> The term root-power quantity is introduced by ISO Standard 80000-1:2009 as a substitute +> of field quantity. The term field quantity is deprecated by that standard. + +I would check the primary source but I'm too cheap to pay for the ISO standard. Sorry! + +## 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). + +## API + +```@docs + Unitful.linear + Unitful.reflevel + Unitful.powerratio + Unitful.rootpowerratio +``` diff --git a/docs/src/newunits.md b/docs/src/newunits.md index d27cb8f0..421b0b89 100644 --- a/docs/src/newunits.md +++ b/docs/src/newunits.md @@ -6,6 +6,11 @@ end # Defining new units +!!! note + Logarithmic units cannot be defined by the user and should not be used in the `@refunit` + or `@unit` macros described below. This limitation will likely be lifted eventually, but + not until the interface for logarithmic units settles down. + The package automatically generates a useful set of units and dimensions in the `Unitful` module in `src/pkgdefaults.jl`. diff --git a/docs/src/trouble.md b/docs/src/trouble.md index 7cb91fed..9a4a8d5b 100644 --- a/docs/src/trouble.md +++ b/docs/src/trouble.md @@ -108,7 +108,8 @@ julia> uconvert(m,0x01cm) # the user means cm, not 0x01c*m ``` This behavior is a consequence of -[a Julia issue](https://github.com/JuliaLang/julia/issues/16356). +[a Julia issue](https://github.com/JuliaLang/julia/issues/16356) that has recently +been fixed and will no longer be a problem in future Julia versions. ## I have a different problem diff --git a/src/Unitful.jl b/src/Unitful.jl index cc864171..1c208fe5 100644 --- a/src/Unitful.jl +++ b/src/Unitful.jl @@ -19,12 +19,14 @@ import Base: steprange_last, unsigned import Base.LinAlg: istril, istriu -export unit, dimension, uconvert, ustrip, upreferred +export logunit, unit, dimension, uconvert, ustrip, upreferred export @dimension, @derived_dimension, @refunit, @unit, @u_str export Quantity export DimensionlessQuantity export NoUnits, NoDims +export powerratio, fieldratio, rootpowerratio, reflevel, linear, @dB, @Np + const unitmodules = Vector{Module}() const basefactors = Dict{Symbol,Tuple{Float64,Rational{Int}}}() @@ -32,1017 +34,17 @@ include("types.jl") const promotion = Dict{Symbol,Unit}() include("user.jl") -const NoUnits = FreeUnits{(), Dimensions{()}}() -const NoDims = Dimensions{()}() -isunitless(::Units) = false -isunitless(::Units{()}) = true - -(y::FreeUnits)(x::Number) = uconvert(y,x) -(y::ContextUnits)(x::Number) = uconvert(y,x) - -""" - mutable struct DimensionError{T,S} <: Exception - x::T - y::S - end -Thrown when dimensions don't match in an operation that demands they do. -Display `x` and `y` in error message. -""" -mutable struct DimensionError{T,S} <: Exception - x::T - y::S -end -Base.showerror(io::IO, e::DimensionError) = - print(io,"DimensionError: $(e.x) and $(e.y) are not dimensionally compatible."); - -numtype(::Quantity{T}) where {T} = T -numtype(::Type{Quantity{T,D,U}}) where {T,D,U} = T - -""" - ustrip(x::Number) -Returns the number out in front of any units. This may be different from the value -in the case of dimensionless quantities. See [`uconvert`](@ref) and the example -below. Because the units are removed, information may be lost and this should -be used with some care. - -This function is just calling `x/unit(x)`, which is as fast as directly -accessing the `val` field of `x::Quantity`, but also works for any other kind -of number. - -This function is mainly intended for compatibility with packages that don't know -how to handle quantities. This function may be deprecated in the future. - -```jldoctest -julia> ustrip(2u"μm/m") == 2 -true - -julia> uconvert(NoUnits, 2u"μm/m") == 2//1000000 -true -``` -""" -@inline ustrip(x::Number) = x/unit(x) - -""" - ustrip{Q<:Quantity}(x::Array{Q}) -Strip units from an `Array` by reinterpreting to type `T`. The resulting -`Array` is a "unit free view" into array `x`. Because the units are -removed, information may be lost and this should be used with some care. - -This function is provided primarily for compatibility purposes; you could pass -the result to PyPlot, for example. This function may be deprecated in the future. - -```jldoctest -julia> a = [1u"m", 2u"m"] -2-element Array{Quantity{Int64, Dimensions:{𝐋}, Units:{m}},1}: - 1 m - 2 m - -julia> b = ustrip(a) -2-element Array{Int64,1}: - 1 - 2 - -julia> a[1] = 3u"m"; b -2-element Array{Int64,1}: - 3 - 2 -``` -""" -@inline ustrip(x::Array{Q}) where {Q <: Quantity} = reinterpret(numtype(Q), x) - -""" - ustrip{Q<:Quantity}(A::AbstractArray{Q}) -Strip units from an `AbstractArray` by making a new array without units using -array comprehensions. - -This function is provided primarily for compatibility purposes; you could pass -the result to PyPlot, for example. This function may be deprecated in the future. -""" -ustrip(A::AbstractArray{Q}) where {Q <: Quantity} = (numtype(Q))[ustrip(x) for x in A] - -""" - ustrip{T<:Number}(x::AbstractArray{T}) -Fall-back that returns `x`. -""" -@inline ustrip(A::AbstractArray{T}) where {T <: Number} = A - -ustrip(A::Diagonal{T}) where {T <: Quantity} = Diagonal(ustrip(A.diag)) -ustrip(A::Bidiagonal{T}) where {T <: Quantity} = - Bidiagonal(ustrip(A.dv), ustrip(A.ev), A.isupper) -ustrip(A::Tridiagonal{T}) where {T <: Quantity} = - Tridiagonal(ustrip(A.dl), ustrip(A.d), ustrip(A.du)) -ustrip(A::SymTridiagonal{T}) where {T <: Quantity} = - SymTridiagonal(ustrip(A.dv), ustrip(A.ev)) - -""" - unit{T,D,U}(x::Quantity{T,D,U}) -Returns the units associated with a quantity. - -Examples: - -```jldoctest -julia> unit(1.0u"m") == u"m" -true - -julia> typeof(u"m") -Unitful.FreeUnits{(Unitful.Unit{:Meter,Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}}(0, 1//1),),Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)}} -``` -""" -@inline unit(x::Quantity{T,D,U}) where {T,D,U} = U() - -""" - unit{T,D,U}(x::Type{Quantity{T,D,U}}) -Returns the units associated with a quantity type, `ContextUnits(U(),P())`. - -Examples: - -```jldoctest -julia> unit(typeof(1.0u"m")) == u"m" -true -``` -""" -@inline unit(::Type{Quantity{T,D,U}}) where {T,D,U} = U() - - -""" - unit(x::Number) -Returns a `Unitful.Units{(), Dimensions{()}}` object to indicate that ordinary -numbers have no units. This is a singleton, which we export as `NoUnits`. -The unit is displayed as an empty string. - -Examples: - -```jldoctest -julia> typeof(unit(1.0)) -Unitful.FreeUnits{(),Unitful.Dimensions{()}} -julia> typeof(unit(Float64)) -Unitful.FreeUnits{(),Unitful.Dimensions{()}} -julia> unit(1.0) == NoUnits -true -``` -""" -@inline unit(x::Number) = NoUnits -@inline unit(x::Type{T}) where {T <: Number} = NoUnits - -""" - dimension(x::Number) - dimension{T<:Number}(x::Type{T}) -Returns a `Unitful.Dimensions{()}` object to indicate that ordinary -numbers are dimensionless. This is a singleton, which we export as `NoDims`. -The dimension is displayed as an empty string. - -Examples: - -```jldoctest -julia> typeof(dimension(1.0)) -Unitful.Dimensions{()} -julia> typeof(dimension(Float64)) -Unitful.Dimensions{()} -julia> dimension(1.0) == NoDims -true -``` -""" -@inline dimension(x::Number) = NoDims -@inline dimension(x::Type{T}) where {T <: Number} = NoDims - -""" - dimension{U,D}(u::Units{U,D}) -Returns a [`Unitful.Dimensions`](@ref) object corresponding to the dimensions -of the units, `D()`. For a dimensionless combination of units, a -`Unitful.Dimensions{()}` object is returned. - -Examples: - -```jldoctest -julia> dimension(u"m") -𝐋 - -julia> typeof(dimension(u"m")) -Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)} - -julia> typeof(dimension(u"m/km")) -Unitful.Dimensions{()} -``` -""" -@inline dimension(u::Units{U,D}) where {U,D} = D() - -""" - dimension{T,D}(x::Quantity{T,D}) -Returns a [`Unitful.Dimensions`](@ref) object `D()` corresponding to the -dimensions of quantity `x`. For a dimensionless [`Unitful.Quantity`](@ref), a -`Unitful.Dimensions{()}` object is returned. - -Examples: - -```jldoctest -julia> dimension(1.0u"m") -𝐋 - -julia> typeof(dimension(1.0u"m/μm")) -Unitful.Dimensions{()} -``` -""" -@inline dimension(x::Quantity{T,D}) where {T,D} = D() -@inline dimension(::Type{Quantity{T,D,U}}) where {T,D,U} = D() - -""" - dimension{T<:Number}(x::AbstractArray{T}) -Just calls `map(dimension, x)`. -""" -dimension(x::AbstractArray{T}) where {T <: Number} = map(dimension, x) - -""" - dimension{T<:Units}(x::AbstractArray{T}) -Just calls `map(dimension, x)`. -""" -dimension(x::AbstractArray{T}) where {T <: Units} = map(dimension, x) - -""" - Quantity(x::Number, y::Units) -Outer constructor for `Quantity`s. This is a generated function to avoid -determining the dimensions of a given set of units each time a new quantity is -made. -""" -@generated function Quantity(x::Number, y::Units) - u = y() - d = dimension(u) - :(Quantity{typeof(x), typeof($d), typeof($u)}(x)) -end -Quantity(x::Number, y::Units{()}) = x - -""" - promote_unit(::Units, ::Units...) -Given `Units` objects as arguments, this function returns a `Units` object appropriate -for the result of promoting quantities which have these units. This function is kind -of like `promote_rule`, except that it doesn't take types. It also does not return a tuple, -but rather just a [`Unitful.Units`](@ref) object (or it throws an error). - -Although we had used `promote_rule` for `Units` objects in prior versions of Unitful, -this was always kind of a hack; it doesn't make sense to promote units directly for -a variety of reasons. -""" -function promote_unit end - -# Generic methods -@inline promote_unit(x) = _promote_unit(x) -@inline _promote_unit(x::Units) = x - -@inline promote_unit(x,y) = _promote_unit(x,y) - -promote_unit(x::Units, y::Units, z::Units, t::Units...) = - promote_unit(_promote_unit(x,y), z, t...) - -# Use configurable fall-back mechanism for FreeUnits -@inline _promote_unit(x::T, y::T) where {T <: FreeUnits} = T() -@inline _promote_unit(x::FreeUnits{N1,D}, y::FreeUnits{N2,D}) where {N1,N2,D} = - upreferred(dimension(x)) - -# same units, but promotion context disagrees -@inline _promote_unit(x::T, y::T) where {T <: ContextUnits} = T() #ambiguity reasons -@inline _promote_unit(x::ContextUnits{N,D,P1}, y::ContextUnits{N,D,P2}) where {N,D,P1,P2} = - ContextUnits{N,D,promote_unit(P1(), P2())}() -# different units, but promotion context agrees -@inline _promote_unit(x::ContextUnits{N1,D,P}, y::ContextUnits{N2,D,P}) where {N1,N2,D,P} = - ContextUnits(P(), P()) -# different units, promotion context disagrees, fall back to FreeUnits -@inline _promote_unit(x::ContextUnits{N1,D}, y::ContextUnits{N2,D}) where {N1,N2,D} = - promote_unit(FreeUnits(x), FreeUnits(y)) - -# ContextUnits beat FreeUnits -@inline _promote_unit(x::ContextUnits{N,D}, y::FreeUnits{N,D}) where {N,D} = x -@inline _promote_unit(x::ContextUnits{N1,D,P}, y::FreeUnits{N2,D}) where {N1,N2,D,P} = - ContextUnits(P(), P()) -@inline _promote_unit(x::FreeUnits, y::ContextUnits) = promote_unit(y,x) - -# FixedUnits beat everything -@inline _promote_unit(x::T, y::T) where {T <: FixedUnits} = T() -@inline _promote_unit(x::FixedUnits{M,D}, y::Units{N,D}) where {M,N,D} = x -@inline _promote_unit(x::Units, y::FixedUnits) = promote_unit(y,x) - -# Different units but same dimension are not fungible for FixedUnits -@inline _promote_unit(x::FixedUnits{M,D}, y::FixedUnits{N,D}) where {M,N,D} = - error("automatic conversion prohibited.") - -# If we didn't handle it above, the dimensions mismatched. -@inline _promote_unit(x::Units, y::Units) = throw(DimensionError(x,y)) - -@inline name(x::Unit{S,D}) where {S,D} = S -@inline name(x::Dimension{S}) where {S} = S -@inline tens(x::Unit) = x.tens -@inline power(x::Unit) = x.power -@inline power(x::Dimension) = x.power - -# This is type unstable but -# a) this method is not called by the user -# b) ultimately the instability will only be present at compile time as it is -# hidden behind a "generated function barrier" -function basefactor(inex, ex, eq, tens, p) - # Sometimes (x::Rational)^1 can fail for large rationals because the result - # is of type x*x so we do a hack here - function dpow(x,p) - if p == 0 - 1 - elseif p == 1 - x - elseif p == -1 - 1//x - else - x^p - end - end - - if isinteger(p) - p = Integer(p) - end - - eq_is_exact = false - output_ex_float = (10.0^tens * float(ex))^p - eq_raised = float(eq)^p - if isa(eq, Integer) || isa(eq, Rational) - output_ex_float *= eq_raised - eq_is_exact = true - end - - can_exact = (output_ex_float < typemax(Int)) - can_exact &= (1/output_ex_float < typemax(Int)) - can_exact &= isinteger(p) - - can_exact2 = (eq_raised < typemax(Int)) - can_exact2 &= (1/eq_raised < typemax(Int)) - can_exact2 &= isinteger(p) - - if can_exact - if eq_is_exact - # If we got here then p is an integer. - # Note that sometimes x^1 can cause an overflow error if x is large because - # of how power_by_squaring is implemented for Rationals, so we use dpow. - x = dpow(eq*ex*(10//1)^tens, p) - return (inex^p, isinteger(x) ? Int(x) : x) - else - x = dpow(ex*(10//1)^tens, p) - return ((inex*eq)^p, isinteger(x) ? Int(x) : x) - end - else - if eq_is_exact && can_exact2 - x = dpow(eq,p) - return ((inex * ex * 10.0^tens)^p, isinteger(x) ? Int(x) : x) - else - return ((inex * ex * 10.0^tens * eq)^p, 1) - end - end -end - -@inline basefactor(x::Unit{U}) where {U} = basefactor(basefactors[U]..., 1, 0, power(x)) - -function basefactor(x::Units{U}) where {U} - fact1 = map(basefactor, U) - inex1 = mapreduce(x->getfield(x,1), *, 1.0, fact1) - float_ex1 = mapreduce(x->float(getfield(x,2)), *, 1, fact1) - can_exact = (float_ex1 < typemax(Int)) - can_exact &= (1/float_ex1 < typemax(Int)) - if can_exact - inex1, mapreduce(x->getfield(x,2), *, 1, fact1) - else - inex1*float_ex1, 1 - end -end - -# Addition / subtraction -for op in [:+, :-] - @eval ($op)(x::Quantity{S,D,U}, y::Quantity{T,D,U}) where {S,T,D,U} = - Quantity(($op)(x.val,y.val), U()) - - # If not generated, there are run-time allocations - @eval function ($op)(x::Quantity{S,D,SU}, y::Quantity{T,D,TU}) where {S,T,D,SU,TU} - ($op)(promote(x,y)...) - end - - @eval ($op)(x::Quantity, y::Quantity) = throw(DimensionError(x,y)) - @eval function ($op)(x::Quantity, y::Number) - if isa(x, DimensionlessQuantity) - ($op)(promote(x,y)...) - else - throw(DimensionError(x,y)) - end - end - @eval function ($op)(x::Number, y::Quantity) - if isa(y, DimensionlessQuantity) - ($op)(promote(x,y)...) - else - throw(DimensionError(x,y)) - end - end - - @eval ($op)(x::Quantity) = Quantity(($op)(x.val),unit(x)) -end - -*(x::Number, y::Units, z::Units...) = Quantity(x,*(y,z...)) - -# Kind of weird, but okay, no need to make things noncommutative. -*(x::Units, y::Number) = *(y,x) - -function tensfactor(x::Unit) - p = power(x) - if isinteger(p) - p = Integer(p) - end - tens(x)*p -end - -@generated function tensfactor(x::Units) - tunits = x.parameters[1] - a = mapreduce(tensfactor, +, 0, tunits) - :($a) -end - -""" -``` -*(a0::Dimensions, a::Dimensions...) -``` - -Given however many dimensions, multiply them together. - -Collect [`Unitful.Dimension`](@ref) objects from the type parameter of the -[`Unitful.Dimensions`](@ref) objects. For identical dimensions, collect powers -and sort uniquely by the name of the `Dimension`. - -Examples: - -```jldoctest -julia> u"𝐌*𝐋/𝐓^2" -𝐋 𝐌 𝐓^-2 - -julia> u"𝐋*𝐌/𝐓^2" -𝐋 𝐌 𝐓^-2 - -julia> typeof(u"𝐋*𝐌/𝐓^2") == typeof(u"𝐌*𝐋/𝐓^2") -true -``` -""" -@generated function *(a0::Dimensions, a::Dimensions...) - # Implementation is very similar to *(::Units, ::Units...) - b = Vector{Dimension}() - a0p = a0.parameters[1] - length(a0p) > 0 && append!(b, a0p) - for x in a - xp = x.parameters[1] - length(xp) > 0 && append!(b, xp) - end - - sort!(b, by=x->power(x)) - sort!(b, by=x->name(x)) - - c = Vector{Dimension}() - if !isempty(b) - i = start(b) - oldstate = b[i] - p=0//1 - while !done(b, i) - (state, i) = next(b, i) - if name(state) == name(oldstate) - p += power(state) - else - if p != 0 - push!(c, Dimension{name(oldstate)}(p)) - end - p = power(state) - end - oldstate = state - end - if p != 0 - push!(c, Dimension{name(oldstate)}(p)) - end - end - - d = (c...) - :(Dimensions{$d}()) -end - -# Both methods needed for ambiguity resolution -^(x::Dimension{T}, y::Integer) where {T} = Dimension{T}(power(x)*y) -^(x::Dimension{T}, y::Number) where {T} = Dimension{T}(power(x)*y) - -# A word of caution: -# Exponentiation is not type-stable for `Dimensions` objects in many cases -^(x::Dimensions{T}, y::Integer) where {T} = *(Dimensions{map(a->a^y, T)}()) -^(x::Dimensions{T}, y::Number) where {T} = *(Dimensions{map(a->a^y, T)}()) -@generated function Base.literal_pow(::typeof(^), x::Dimensions{T}, ::Type{Val{p}}) where {T,p} - z = *(Dimensions{map(a->a^p, T)}()) - :($z) -end - -@inline dimension(u::Unit{U,D}) where {U,D} = D()^u.power - -*(x::Quantity, y::Units, z::Units...) = Quantity(x.val, *(unit(x),y,z...)) -*(x::Quantity, y::Quantity) = Quantity(x.val*y.val, unit(x)*unit(y)) - -# Next two lines resolves some method ambiguity: -*(x::Bool, y::T) where {T <: Quantity} = - ifelse(x, y, ifelse(signbit(y), -zero(y), zero(y))) -*(x::Quantity, y::Bool) = Quantity(x.val*y, unit(x)) - -*(y::Number, x::Quantity) = *(x,y) -*(x::Quantity, y::Number) = Quantity(x.val*y, unit(x)) - -# looked in arraymath.jl for similar code -for f in (:*,) - @eval begin - function ($f)(A::Units, B::AbstractArray{T}) where {T} - F = similar(B, Base.promote_op($f,typeof(A),T)) - for (iF, iB) in zip(eachindex(F), eachindex(B)) - @inbounds F[iF] = ($f)(A, B[iB]) - end - return F - end - function ($f)(A::AbstractArray{T}, B::Units) where {T} - F = similar(A, Base.promote_op($f,T,typeof(B))) - for (iF, iA) in zip(eachindex(F), eachindex(A)) - @inbounds F[iF] = ($f)(A[iA], B) - end - return F - end - end -end - -# Division (units) - -/(x::Units, y::Units) = *(x,inv(y)) -/(x::Dimensions, y::Dimensions) = *(x,inv(y)) -/(x::Quantity, y::Units) = Quantity(x.val, unit(x) / y) -/(x::Units, y::Quantity) = Quantity(1/y.val, x / unit(y)) -/(x::Number, y::Units) = Quantity(x,inv(y)) -/(x::Units, y::Number) = (1/y) * x - -//(x::Units, y::Units) = x/y -//(x::Dimensions, y::Dimensions) = x/y -//(x::Quantity, y::Units) = Quantity(x.val, unit(x) / y) -//(x::Units, y::Quantity) = Quantity(1//y.val, x / unit(y)) -//(x::Number, y::Units) = Rational(x)/y -//(x::Units, y::Number) = (1//y) * x - -# Division (quantities) - -for op in (:/, ://) - @eval begin - ($op)(x::Quantity, y::Quantity) = Quantity(($op)(x.val, y.val), unit(x) / unit(y)) - ($op)(x::Quantity, y::Number) = Quantity(($op)(x.val, y), unit(x)) - ($op)(x::Number, y::Quantity) = Quantity(($op)(x, y.val), inv(unit(y))) - end -end - -# ambiguity resolution -//(x::Quantity, y::Complex) = Quantity(//(x.val, y), unit(x)) - -# Division (other functions) - -for f in (:div, :fld, :cld) - @eval function ($f)(x::Quantity, y::Quantity) - z = uconvert(unit(y), x) # TODO: use promote? - ($f)(z.val,y.val) - end -end - -for f in (:mod, :rem) - @eval function ($f)(x::Quantity, y::Quantity) - z = uconvert(unit(y), x) # TODO: use promote? - Quantity(($f)(z.val,y.val), unit(y)) - end -end - -# Needed until LU factorization is made to work with unitful numbers -function inv(x::StridedMatrix{T}) where {T <: Quantity} - m = inv(ustrip(x)) - iq = eltype(m) - reinterpret(Quantity{iq, typeof(inv(dimension(T))), typeof(inv(unit(T)))}, m) -end - -for x in (:istriu, :istril) - @eval ($x)(A::AbstractMatrix{T}) where {T <: Quantity} = ($x)(ustrip(A)) -end - -# Other mathematical functions - -# `fma` and `muladd` -# The idea here is that if the numeric backing types are not the same, they -# will be promoted to be the same by the generic `fma(::Number, ::Number, ::Number)` -# method. We then catch the possible results and handle the units logic with one -# performant method. - -for (_x,_y) in [(:fma, :_fma), (:muladd, :_muladd)] - @static if VERSION >= v"0.6.0-" # work-around Julia issue 20103 - # Catch some signatures pre-promotion - @eval @inline ($_x)(x::Number, y::Quantity, z::Quantity) = ($_y)(x,y,z) - @eval @inline ($_x)(x::Quantity, y::Number, z::Quantity) = ($_y)(x,y,z) - - # Post-promotion - @eval @inline ($_x)(x::Quantity{T}, y::Quantity{T}, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) - else - @eval @inline ($_x)(x::Quantity{T}, y::T, z::T) where {T <: Number} = ($_y)(x,y,z) - @eval @inline ($_x)(x::T, y::Quantity{T}, z::T) where {T <: Number} = ($_y)(x,y,z) - @eval @inline ($_x)(x::T, y::T, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) - @eval @inline ($_x)(x::Quantity{T}, y::Quantity{T}, z::T) where {T <: Number} = ($_y)(x,y,z) - @eval @inline ($_x)(x::T, y::Quantity{T}, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) - @eval @inline ($_x)(x::Quantity{T}, y::T, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) - @eval @inline ($_x)(x::Quantity{T}, y::Quantity{T}, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) - end - - # It seems like most of this is optimized out by the compiler, including the - # apparent runtime check of dimensions, which does not appear in @code_llvm. - @eval @inline function ($_y)(x,y,z) - dimension(x) * dimension(y) != dimension(z) && throw(DimensionError(x*y,z)) - uI = unit(x)*unit(y) - uF = promote_unit(uI, unit(z)) - c = ($_x)(ustrip(x), ustrip(y), ustrip(uconvert(uI, z))) - uconvert(uF, Quantity(c, uI)) - end -end - -sqrt(x::Quantity) = Quantity(sqrt(x.val), sqrt(unit(x))) -cbrt(x::Quantity) = Quantity(cbrt(x.val), cbrt(unit(x))) - -for _y in (:sin, :cos, :tan, :cot, :sec, :csc, :cis) - @eval ($_y)(x::DimensionlessQuantity) = ($_y)(uconvert(NoUnits, x)) -end - -atan2(y::Quantity, x::Quantity) = atan2(promote(y,x)...) -atan2(y::Quantity{T,D,U}, x::Quantity{T,D,U}) where {T,D,U} = atan2(y.val,x.val) -atan2(y::Quantity{T,D1,U1}, x::Quantity{T,D2,U2}) where {T,D1,U1,D2,U2} = - throw(DimensionError(x,y)) - -for (f, F) in [(:min, :<), (:max, :>)] - @eval @generated function ($f)(x::Quantity, y::Quantity) #TODO - xdim = x.parameters[2]() - ydim = y.parameters[2]() - if xdim != ydim - return :(throw(DimensionError(x,y))) - end - - isa(x.parameters[3](), FixedUnits) && - isa(y.parameters[3](), FixedUnits) && - x.parameters[3] !== y.parameters[3] && - error("automatic conversion prohibited.") - - xunits = x.parameters[3].parameters[1] - yunits = y.parameters[3].parameters[1] - - factx = mapreduce((x,y)->broadcast(*,x,y), xunits) do x - vcat(basefactor(x)...) - end - facty = mapreduce((x,y)->broadcast(*,x,y), yunits) do x - vcat(basefactor(x)...) - end - - tensx = mapreduce(tensfactor, +, xunits) - tensy = mapreduce(tensfactor, +, yunits) - - convx = *(factx..., (10.0)^tensx) - convy = *(facty..., (10.0)^tensy) - - :($($F)(x.val*$convx, y.val*$convy) ? x : y) - end -end - -abs(x::Quantity) = Quantity(abs(x.val), unit(x)) -abs2(x::Quantity) = Quantity(abs2(x.val), unit(x)*unit(x)) - -copysign(x::Quantity, y::Number) = Quantity(copysign(x.val,y/unit(y)), unit(x)) -flipsign(x::Quantity, y::Number) = Quantity(flipsign(x.val,y/unit(y)), unit(x)) - -@inline isless(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = _isless(x,y) -@inline _isless(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = isless(x.val, y.val) -@inline _isless(x::Quantity{T,D1,U1}, y::Quantity{T,D2,U2}) where {T,D1,D2,U1,U2} = throw(DimensionError(x,y)) -@inline _isless(x,y) = isless(x,y) - -isless(x::Quantity, y::Quantity) = _isless(promote(x,y)...) -isless(x::Quantity, y::Number) = _isless(promote(x,y)...) -isless(x::Number, y::Quantity) = _isless(promote(x,y)...) - -@inline <(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = _lt(x,y) -@inline _lt(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = <(x.val,y.val) -@inline _lt(x::Quantity{T,D1,U1}, y::Quantity{T,D2,U2}) where {T,D1,D2,U1,U2} = throw(DimensionError(x,y)) -@inline _lt(x,y) = <(x,y) - -<(x::Quantity, y::Quantity) = _lt(promote(x,y)...) -<(x::Quantity, y::Number) = _lt(promote(x,y)...) -<(x::Number, y::Quantity) = _lt(promote(x,y)...) - -Base.rtoldefault(::Type{Quantity{T,D,U}}) where {T,D,U} = Base.rtoldefault(T) -isapprox(x::Quantity{T,D,U}, y::Quantity{T,D,U}; atol=zero(Quantity{real(T),D,U}), kwargs...) where {T,D,U} = - isapprox(x.val, y.val; atol=uconvert(unit(y), atol).val, kwargs...) -function isapprox(x::Quantity, y::Quantity; kwargs...) - dimension(x) != dimension(y) && return false - return isapprox(promote(x,y)...; kwargs...) -end -isapprox(x::Quantity, y::Number; kwargs...) = isapprox(uconvert(NoUnits, x), y; kwargs...) -isapprox(x::Number, y::Quantity; kwargs...) = isapprox(y, x; kwargs...) - -function isapprox(x::AbstractArray{Quantity{T1,D,U1}}, - y::AbstractArray{Quantity{T2,D,U2}}; rtol::Real=Base.rtoldefault(T1,T2), - atol=zero(Quantity{T1,D,U1}), norm::Function=vecnorm) where {T1,D,U1,T2,U2} - - d = norm(x - y) - if isfinite(d) - return d <= atol + rtol*max(norm(x), norm(y)) - else - # Fall back to a component-wise approximate comparison - return all(ab -> isapprox(ab[1], ab[2]; rtol=rtol, atol=atol), zip(x, y)) - end -end -isapprox(x::AbstractArray{S}, y::AbstractArray{T}; - kwargs...) where {S <: Quantity,T <: Quantity} = false -function isapprox(x::AbstractArray{S}, y::AbstractArray{N}; - kwargs...) where {S <: Quantity,N <: Number} - if dimension(N) == dimension(S) - isapprox(map(x->uconvert(NoUnits,x),x),y; kwargs...) - else - false - end -end -isapprox(y::AbstractArray{N}, x::AbstractArray{S}; - kwargs...) where {S <: Quantity,N <: Number} = isapprox(x,y; kwargs...) - -==(x::Quantity{S,D,U}, y::Quantity{T,D,U}) where {S,T,D,U} = (x.val == y.val) -function ==(x::Quantity, y::Quantity) - dimension(x) != dimension(y) && return false - ==(promote(x,y)...) -end - -function ==(x::Quantity, y::Number) - if dimension(x) == NoDims - uconvert(NoUnits, x) == y - else - false - end -end -==(x::Number, y::Quantity) = ==(y,x) -<=(x::Quantity, y::Quantity) = <(x,y) || x==y - -_dimerr(f) = error("$f can only be well-defined for dimensionless ", - "numbers. For dimensionful numbers, different input units yield physically ", - "different results.") -isinteger(x::Quantity) = _dimerr(isinteger) -isinteger(x::DimensionlessQuantity) = isinteger(uconvert(NoUnits, x)) -for f in (:floor, :ceil, :trunc, :round) - @eval ($f)(x::Quantity) = _dimerr($f) - @eval ($f)(x::DimensionlessQuantity) = ($f)(uconvert(NoUnits, x)) - @eval ($f)(::Type{T}, x::Quantity) where {T <: Integer} = _dimerr($f) - @eval ($f)(::Type{T}, x::DimensionlessQuantity) where {T <: Integer} = ($f)(T, uconvert(NoUnits, x)) -end - -zero(x::Quantity) = Quantity(zero(x.val), unit(x)) -zero(x::Type{Quantity{T,D,U}}) where {T,D,U} = zero(T)*U() - -one(x::Quantity) = one(x.val) -one(x::Type{Quantity{T,D,U}}) where {T,D,U} = one(T) - -isreal(x::Quantity) = isreal(x.val) -isfinite(x::Quantity) = isfinite(x.val) -isinf(x::Quantity) = isinf(x.val) -isnan(x::Quantity) = isnan(x.val) - -unsigned(x::Quantity) = Quantity(unsigned(x.val), unit(x)) - -for f in (:exp, :exp10, :exp2, :expm1, :log, :log10, :log1p, :log2) - @eval ($f)(x::DimensionlessQuantity) = ($f)(uconvert(NoUnits, x)) -end - -real(x::Quantity) = Quantity(real(x.val), unit(x)) -imag(x::Quantity) = Quantity(imag(x.val), unit(x)) -conj(x::Quantity) = Quantity(conj(x.val), unit(x)) - -@inline vecnorm(x::Quantity, p::Real=2) = - p == 0 ? (x==zero(x) ? typeof(abs(x))(0) : typeof(abs(x))(1)) : abs(x) - -""" - sign(x::Quantity) -Returns the sign of `x`. -""" -sign(x::Quantity) = sign(x.val) - -""" - signbit(x::Quantity) -Returns the sign bit of the underlying numeric value of `x`. -""" -signbit(x::Quantity) = signbit(x.val) - -prevfloat(x::Quantity{T}) where {T <: AbstractFloat} = Quantity(prevfloat(x.val), unit(x)) -nextfloat(x::Quantity{T}) where {T <: AbstractFloat} = Quantity(nextfloat(x.val), unit(x)) - -function frexp(x::Quantity{T}) where {T <: AbstractFloat} - a,b = frexp(x.val) - a*unit(x), b -end - -""" - float(x::Quantity) -Convert the numeric backing type of `x` to a floating-point representation. -Returns a `Quantity` with the same units. -""" -float(x::Quantity) = Quantity(float(x.val), unit(x)) - -""" - Integer(x::Quantity) -Convert the numeric backing type of `x` to an integer representation. -Returns a `Quantity` with the same units. -""" -Integer(x::Quantity) = Quantity(Integer(x.val), unit(x)) - -""" - Rational(x::Quantity) -Convert the numeric backing type of `x` to a rational number representation. -Returns a `Quantity` with the same units. -""" -Rational(x::Quantity) = Quantity(Rational(x.val), unit(x)) - -*(y::Units, r::Range) = *(r,y) -*(r::Range, y::Units) = range(first(r)*y, step(r)*y, length(r)) -*(r::Range, y::Units, z::Units...) = *(x, *(y,z...)) - -include("range.jl") - -typemin(::Type{Quantity{T,D,U}}) where {T,D,U} = typemin(T)*U() -typemin(x::Quantity{T}) where {T} = typemin(T)*unit(x) - -typemax(::Type{Quantity{T,D,U}}) where {T,D,U} = typemax(T)*U() -typemax(x::Quantity{T}) where {T} = typemax(T)*unit(x) - -""" - offsettemp(::Unit) -For temperature units, this function is used to set the scale offset. -""" -offsettemp(::Unit) = 0 - -@inline dimtype(u::Unit{U,D}) where {U,D} = D - -@generated function *(a0::FreeUnits, a::FreeUnits...) - - # Sort the units uniquely. This is a generated function so that we - # don't have to figure out the units each time. - b = Vector{Unit}() - a0p = a0.parameters[1] - length(a0p) > 0 && append!(b, a0p) - for x in a - xp = x.parameters[1] - length(xp) > 0 && append!(b, xp) - end - - # b is an Array containing all of the Unit objects that were - # found in the type parameters of the Units objects (a0, a...) - sort!(b, by=x->power(x)) - sort!(b, by=x->tens(x)) - sort!(b, by=x->name(x)) - - # Units[m,m,cm,cm^2,cm^3,nm,m^4,µs,µs^2,s] - # reordered as: - # Units[nm,cm,cm^2,cm^3,m,m,m^4,µs,µs^2,s] - - # Collect powers of a given unit - c = Vector{Unit}() - if !isempty(b) - i = start(b) - oldstate = b[i] - p=0//1 - while !done(b, i) - (state, i) = next(b, i) - if tens(state) == tens(oldstate) && name(state) == name(oldstate) - p += power(state) - else - if p != 0 - push!(c, Unit{name(oldstate),dimtype(oldstate)}(tens(oldstate), p)) - end - p = power(state) - end - oldstate = state - end - if p != 0 - push!(c, Unit{name(oldstate),dimtype(oldstate)}(tens(oldstate), p)) - end - end - # results in: - # Units[nm,cm^6,m^6,µs^3,s] - - d = (c...) - f = typeof(mapreduce(dimension, *, NoDims, c)) - :(FreeUnits{$d,$f}()) -end -*(a0::ContextUnits, a::ContextUnits...) = - ContextUnits(*(FreeUnits(a0), FreeUnits.(a)...), - *(FreeUnits(upreferred(a0)), FreeUnits.((upreferred).(a))...)) -FreeOrContextUnits = Union{FreeUnits, ContextUnits} -*(a0::FreeOrContextUnits, a::FreeOrContextUnits...) = - *(ContextUnits(a0), ContextUnits.(a)...) -*(a0::FixedUnits, a::FixedUnits...) = - FixedUnits(*(FreeUnits(a0), FreeUnits.(a)...)) - -""" -``` -*(a0::Units, a::Units...) -``` - -Given however many units, multiply them together. This is actually handled by -a few different methods, since we have `FreeUnits`, `ContextUnits`, and `FixedUnits`. - -Collect [`Unitful.Unit`](@ref) objects from the type parameter of the -[`Unitful.Units`](@ref) objects. For identical units including SI prefixes -(i.e. cm ≠ m), collect powers and sort uniquely by the name of the `Unit`. -The unique sorting permits easy unit comparisons. - -Examples: - -```jldoctest -julia> u"kg*m/s^2" -kg m s^-2 - -julia> u"m/s*kg/s" -kg m s^-2 - -julia> typeof(u"m/s*kg/s") == typeof(u"kg*m/s^2") -true -``` -""" -*(a0::Units, a::Units...) = FixedUnits(*(FreeUnits(a0), FreeUnits.(a)...)) -# Logic above is that if we're not using FreeOrContextUnits, at least one is FixedUnits. - -# Both methods needed for ambiguity resolution -^(x::Unit{U,D}, y::Integer) where {U,D} = Unit{U,D}(tens(x), power(x)*y) -^(x::Unit{U,D}, y::Number) where {U,D} = Unit{U,D}(tens(x), power(x)*y) - -# A word of caution: -# Exponentiation is not type-stable for `Units` objects. -# Dimensions get reconstructed anyway so we pass () for the D type parameter... -^(x::FreeUnits{N}, y::Integer) where {N} = *(FreeUnits{map(a->a^y, N), ()}()) -^(x::FreeUnits{N}, y::Number) where {N} = *(FreeUnits{map(a->a^y, N), ()}()) - -^(x::ContextUnits{N,D,P}, y::Integer) where {N,D,P} = - *(ContextUnits{map(a->a^y, N), (), typeof(P()^y)}()) -^(x::ContextUnits{N,D,P}, y::Number) where {N,D,P} = - *(ContextUnits{map(a->a^y, N), (), typeof(P()^y)}()) - -^(x::FixedUnits{N}, y::Integer) where {N} = *(FixedUnits{map(a->a^y, N), ()}()) -^(x::FixedUnits{N}, y::Number) where {N} = *(FixedUnits{map(a->a^y, N), ()}()) - -@generated function Base.literal_pow(::typeof(^), x::FreeUnits{N}, ::Type{Val{p}}) where {N,p} - y = *(FreeUnits{map(a->a^p, N), ()}()) - :($y) -end -@generated function Base.literal_pow(::typeof(^), x::ContextUnits{N,D,P}, ::Type{Val{p}}) where {N,D,P,p} - y = *(ContextUnits{map(a->a^p, N), (), typeof(P()^p)}()) - :($y) -end -@generated function Base.literal_pow(::typeof(^), x::FixedUnits{N}, ::Type{Val{p}}) where {N,p} - y = *(FixedUnits{map(a->a^p, N), ()}()) - :($y) -end -Base.literal_pow(::typeof(^), x::Quantity, ::Type{Val{v}}) where {v} = - Quantity(Base.literal_pow(^, x.val, Val{v}), - Base.literal_pow(^, unit(x), Val{v})) - -# All of these are needed for ambiguity resolution -^(x::Quantity, y::Integer) = Quantity((x.val)^y, unit(x)^y) -^(x::Quantity, y::Rational) = Quantity((x.val)^y, unit(x)^y) -^(x::Quantity, y::Real) = Quantity((x.val)^y, unit(x)^y) - -# Since exponentiation is not type stable, we define a special `inv` method to -# enable fast division. For julia 0.6.0-dev.1711, the appropriate methods for ^ -# and * need to be defined before this one! -for (fun,pow) in ((:inv, -1//1), (:sqrt, 1//2), (:cbrt, 1//3)) - # The following are generated functions to ensure type stability. - @eval @generated function ($fun)(x::Dimensions) - dimtuple = map(x->x^($pow), x.parameters[1]) - y = *(Dimensions{dimtuple}()) # sort appropriately - :($y) - end - - @eval @generated function ($fun)(x::FreeUnits) - unittuple = map(x->x^($pow), x.parameters[1]) - y = *(FreeUnits{unittuple,()}()) # sort appropriately - :($y) - end - - @eval @generated function ($fun)(x::ContextUnits) - unittuple = map(x->x^($pow), x.parameters[1]) - promounit = ($fun)(x.parameters[3]()) - y = *(ContextUnits{unittuple,(),typeof(promounit)}()) # sort appropriately - :($y) - end - - @eval @generated function ($fun)(x::FixedUnits) - unittuple = map(x->x^($pow), x.parameters[1]) - y = *(FixedUnits{unittuple,()}()) # sort appropriately - :($y) - end -end - -Base.rand(r::AbstractRNG, ::Type{Quantity{T,D,U}}) where {T,D,U} = rand(r,T)*U() -Base.ones(Q::Type{<:Quantity}, dims::Tuple) = fill!(Array{Q}(dims), oneunit(Q)) -Base.ones(a::AbstractArray, Q::Type{<:Quantity}) = fill!(similar(a,Q), oneunit(Q)) - +include("utils.jl") +include("dimensions.jl") +include("units.jl") +include("quantities.jl") include("display.jl") include("promotion.jl") include("conversion.jl") +include("range.jl") include("fastmath.jl") include("pkgdefaults.jl") -include("temperature.jl") +include("logarithm.jl") function __init__() # @u_str should be aware of units defined in module Unitful diff --git a/src/conversion.jl b/src/conversion.jl index 15bb00a6..bbde5040 100644 --- a/src/conversion.jl +++ b/src/conversion.jl @@ -107,7 +107,7 @@ function convert(::Type{Quantity{T}}, x::Number) where {T} Quantity{T,typeof(NoDims),typeof(NoUnits)}(x) end function convert(::Type{Quantity{T}}, x::Quantity) where {T} - Quantity{T,typeof(dimension(x)),typeof(unit(x))}(T(ustrip(x))) + Quantity{T,typeof(dimension(x)),typeof(unit(x))}(convert(T, x.val)) end """ diff --git a/src/dimensions.jl b/src/dimensions.jl new file mode 100644 index 00000000..a81eda3b --- /dev/null +++ b/src/dimensions.jl @@ -0,0 +1,87 @@ +""" + \*(a0::Dimensions, a::Dimensions...) +Given however many dimensions, multiply them together. + +Collect [`Unitful.Dimension`](@ref) objects from the type parameter of the +[`Unitful.Dimensions`](@ref) objects. For identical dimensions, collect powers +and sort uniquely by the name of the `Dimension`. + +Examples: + +```jldoctest +julia> u"𝐌*𝐋/𝐓^2" +𝐋 𝐌 𝐓^-2 + +julia> u"𝐋*𝐌/𝐓^2" +𝐋 𝐌 𝐓^-2 + +julia> typeof(u"𝐋*𝐌/𝐓^2") == typeof(u"𝐌*𝐋/𝐓^2") +true +``` +""" +@generated function *(a0::Dimensions, a::Dimensions...) + # Implementation is very similar to *(::Units, ::Units...) + b = Vector{Dimension}() + a0p = a0.parameters[1] + length(a0p) > 0 && append!(b, a0p) + for x in a + xp = x.parameters[1] + length(xp) > 0 && append!(b, xp) + end + + sort!(b, by=x->power(x)) + sort!(b, by=x->name(x)) + + c = Vector{Dimension}() + if !isempty(b) + i = start(b) + oldstate = b[i] + p=0//1 + while !done(b, i) + (state, i) = next(b, i) + if name(state) == name(oldstate) + p += power(state) + else + if p != 0 + push!(c, Dimension{name(oldstate)}(p)) + end + p = power(state) + end + oldstate = state + end + if p != 0 + push!(c, Dimension{name(oldstate)}(p)) + end + end + + d = (c...) + :(Dimensions{$d}()) +end + +/(x::Dimensions, y::Dimensions) = *(x,inv(y)) +//(x::Dimensions, y::Dimensions) = x/y + +# Both methods needed for ambiguity resolution +^(x::Dimension{T}, y::Integer) where {T} = Dimension{T}(power(x)*y) +^(x::Dimension{T}, y::Number) where {T} = Dimension{T}(power(x)*y) + +# A word of caution: +# Exponentiation is not type-stable for `Dimensions` objects in many cases +^(x::Dimensions{T}, y::Integer) where {T} = *(Dimensions{map(a->a^y, T)}()) +^(x::Dimensions{T}, y::Number) where {T} = *(Dimensions{map(a->a^y, T)}()) +@generated function Base.literal_pow(::typeof(^), x::Dimensions{T}, ::Type{Val{p}}) where {T,p} + z = *(Dimensions{map(a->a^p, T)}()) + :($z) +end + +# Since exponentiation is not type stable, we define a special `inv` method to enable fast +# division. For julia 0.6.0, the appropriate methods for ^ and * need to be defined before +# this one! +for (fun,pow) in ((:inv, -1//1), (:sqrt, 1//2), (:cbrt, 1//3)) + # The following are generated functions to ensure type stability. + @eval @generated function ($fun)(x::Dimensions) + dimtuple = map(x->x^($pow), x.parameters[1]) + y = *(Dimensions{dimtuple}()) # sort appropriately + :($y) + end +end diff --git a/src/logarithm.jl b/src/logarithm.jl new file mode 100644 index 00000000..0c8ab061 --- /dev/null +++ b/src/logarithm.jl @@ -0,0 +1,397 @@ + +abbr(::LogInfo{:Decibel}) = "dB" +abbr(::LogInfo{:Neper}) = "Np" +base(::LogInfo{N,B}) where {N,B} = B +prefactor(::LogInfo{N,B,P}) where {N,B,P} = P + +dimension(x::Level) = dimension(reflevel(x)) +dimension(x::Type{T}) where {L,S,T<:Level{L,S}} = dimension(S) + +logunit(x::Level{L,S}) where {L,S} = MixedUnits{Level{L,S}}() +logunit(x::Type{T}) where {L,S,T<:Level{L,S}} = MixedUnits{Level{L,S}}() + +function abbr(x::Level{L,S}) where {L,S} + if dimension(S) == NoDims + return abbr(L()) + else + return join([abbr(L()), " (", reflevel(x), ")"]) + end +end + +function uconvert(a::Units, x::Level) + dimension(a) != dimension(x) && throw(DimensionError(a,x)) + return uconvert(a, x.val) +end +uconvert(a::Units, x::Quantity{<:Level}) = uconvert(a, linear(x)) +Base.convert(::Type{LogScaled{L1}}, x::Level{L2,S}) where {L1,L2,S} = Level{L1,S}(x.val) +Base.convert(T::Type{<:Level}, x::Level) = T(x.val) + +""" + reflevel(x::Level{L,S}) where {L,S} = S + reflevel(::Type{Level{L,S}}) where {L,S} = S + reflevel(::Type{Level{L,S,T}}) where {L,S,T} = S +Returns the reference level, e.g. + +```jldoctest +julia> reflevel(3u"dBm") +1 mW +``` +""" +reflevel(x::Level{L,S}) where {L,S} = S +reflevel(::Type{Level{L,S}}) where {L,S} = S +reflevel(::Type{Level{L,S,T}}) where {L,S,T} = S + +dimension(x::Gain) = NoDims +dimension(x::Type{<:Gain}) = NoDims + +logunit(x::Gain{L}) where {L} = MixedUnits{Gain{L}}() +logunit(x::Type{T}) where {L, T<:Gain{L}} = MixedUnits{Gain{L}}() + +abbr(x::Gain{L}) where {L} = abbr(L()) +function Gain{L}(val::Real) where {L <: LogInfo} + dimension(val) != NoDims && throw(DimensionError(val,1)) + return Gain{L, typeof(val)}(val) +end + +Base.convert(::Type{Gain{L}}, x::Gain{L}) where {L} = Gain{L}(x.val) +Base.convert(::Type{Gain{L1}}, x::Gain{L2}) where {L1,L2} = Gain{L1}(_gconv(L1,L2,x)) +Base.convert(::Type{Gain{L,T1}}, x::Gain{L,T2}) where {L,T1,T2} = Gain{L,T1}(x.val) +Base.convert(T::Type{Gain{L1,T1}}, x::Gain{L2,T2}) where {L1,L2,T1,T2} = T(_gconv(L1,L2,x)) +Base.convert(::Type{LogScaled{L1}}, x::Gain{L2}) where {L1,L2} = Gain{L1}(_gconv(L1,L2,x)) +function _gconv(L1,L2,x) + if isrootpower(L1) == isrootpower(L2) + gain = tolog(L1,1,fromlog(L2,1,x.val)) + elseif isrootpower(L1) && !isrootpower(L2) + gain = tolog(L1,1,fromlog(L2,1,0.5*x.val)) + else + gain = tolog(L1,1,fromlog(L2,1,2*x.val)) + end + return gain +end + +tolog(L,S,x) = (1+isrootpower(L,dimension(S))) * prefactor(L()) * (logfn(L()))(x) +fromlog(L,S,x) = S * expfn(L())( x / ((1+isrootpower(L,dimension(S)))*prefactor(L())) ) + +function Base.show(io::IO, x::MixedUnits{T,U}) where {T,U} + print(io, abbr(x)) + if x.units != NoUnits + print(io, " ") + show(io, x.units) + end +end + +abbr(::MixedUnits{L}) where {L <: Level} = abbr(L(reflevel(L))) +abbr(::MixedUnits{L}) where {L <: Gain} = abbr(L(1)) + +dimension(a::MixedUnits{L}) where {L} = dimension(L) * dimension(a.units) +unit(a::MixedUnits{L,U}) where {L,U} = U() +logunit(a::MixedUnits{L}) where {L} = MixedUnits{L}() +isunitless(::MixedUnits) = false + +Base. *(::MixedUnits, ::MixedUnits) = error("cannot have more than one logarithmic unit.") +Base. /(::MixedUnits{T}, ::MixedUnits{S}) where {T,S} = + error("cannot divide logarithmic units except to cancel.") +Base. /(x::MixedUnits{T}, y::MixedUnits{T}) where {T} = x.units / y.units + +Base. *(x::MixedUnits{T}, y::Units) where {T} = MixedUnits{T}(x.units * y) +Base. *(x::Units, y::MixedUnits) = y * x +Base. /(x::MixedUnits{T}, y::Units) where {T} = MixedUnits{T}(x.units / y) +Base. /(x::Units, y::MixedUnits) = error("cannot divide logarithmic units except to cancel.") + +Base. *(x::Real, y::MixedUnits{Level{L,S}}) where {L,S} = (Level{L,S}(fromlog(L,S,x)))*y.units +Base. *(x::Real, y::MixedUnits{Gain{L}}) where {L} = (Gain{L}(x))*y.units +Base. *(x::MixedUnits, y::Number) = y * x +Base. /(x::Number, y::MixedUnits) = error("cannot divide out logarithmic units; try `linear`.") +Base. /(x::MixedUnits, y::Number) = inv(y) * x + +function uconvert(a::MixedUnits{Level{L,S}}, x::Number) where {L,S} + dimension(a) != dimension(x) && throw(DimensionError(a,x)) + q1 = uconvert(unit(S)*a.units, linear(x)) / a.units + return Level{L,S}(q1) * a.units +end +function uconvert(a::MixedUnits{Gain{L}}, x::Gain) where {L} + dimension(a) != dimension(x) && throw(DimensionError(a,x)) + return convert(Gain{L}, x) +end +function uconvert(a::MixedUnits{<:Gain}, x::Number) + dimension(a) != dimension(x) && throw(DimensionError(a,x)) + ustr = replace(string(a), " ", "*") + error("perhaps you meant `($x)*($ustr)`?") +end + +for (_short,_long,_base,_pre) in ((:dB, :Decibel, 10, 10), + (:Np, :Neper, e, 1//2)) + li = Symbol("li_",_short) + @eval begin + const $li = LogInfo{$(QuoteNode(_long)),$_base,$_pre} + const $_short = MixedUnits{Gain{$li}}() + end +end + +abbr(::Level{li_dB, 1mW}) = "dBm" +abbr(::Level{li_dB, 1V}) = "dBV" +abbr(::Level{li_dB, sqrt(0.6)V}) = "dBu" +abbr(::Level{li_dB, 1μV}) = "dBμV" +abbr(::Level{li_dB, 20μPa}) = "dBSPL" + +const dBV = MixedUnits{Level{li_dB, 1V}}() +const dBu = MixedUnits{Level{li_dB, sqrt(0.6)V}}() +const dBμV = MixedUnits{Level{li_dB, 1μV}}() +const dBµV = dBμV # different character encoding of μ +const dBm = MixedUnits{Level{li_dB, 1mW}}() +const dBSPL = MixedUnits{Level{li_dB, 20μPa}}() + +ustrip(x::Level{L,S}) where {L<:LogInfo, S} = tolog(L,S,x.val/reflevel(x)) +ustrip(x::Gain) = x.val + +# TODO: some more dimensions? +isrootpower(x,y) = isrootpower_warn(x,y) +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(W))) = false +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(V))) = true +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(A))) = true +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(Pa))) = true + +# Default to power or root-power as appropriate for the given logarithmic unit +function isrootpower_warn(x,y) + irp = isrootpower(x) + str = ifelse(irp, "root-power", "power") + warn("result may be incorrect. Define ", + "`Unitful.isrootpower(::Type{<:Unitful.LogInfo}, ::typeof($y))` to fix.") + return irp +end + +isrootpower(t::Type{<:LogInfo}, ::typeof(NoDims)) = isrootpower(t) +isrootpower(::Type{li_dB}) = false +isrootpower(::Type{li_Np}) = true + +==(x::Gain, y::Level) = ==(y,x) +==(x::Level, y::Gain) = false + +Base. +(x::Level{L,S}, y::Level{L,S}) where {L,S} = Level{L,S}(x.val + y.val) +Base. +(x::Gain{L}, y::Gain{L}) where {L} = Gain{L}(x.val + y.val) +Base. +(x::Level{L,S}, y::Gain{L}) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(x)+y.val)) +Base. +(x::Gain, y::Level) = +(y,x) + +Base. -(x::Level{L,S}, y::Level{L,S}) where {L,S} = Level{L,S}(x.val - y.val) +Base. -(x::Gain{L}, y::Gain{L}) where {L} = Gain{L}(x.val - y.val) +Base. -(x::Level{L,S}, y::Gain{L}) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(x) - y.val)) +Base. -(x::Gain, y::Level) = error("cannot subtract a level from a gain.") + +# Multiplication +Base. *(x::Number, y::Level) = *(y,x) +Base. *(x::Bool, y::Level) = *(y,x) # for method ambiguity +Base. *(x::Quantity, y::Level) = *(y,x) # for method ambiguity +Base. *(x::Level{L,S}, y::Number) where {L,S} = Level{L,S}(x.val * y) +Base. *(x::Level{L,S}, y::Bool) where {L,S} = Level{L,S}(x.val * y) # for method ambiguity +Base. *(x::Level{L,S}, y::Quantity) where {L,S} = *(x.val, y) +Base. *(x::Level{L,S}, y::Level) where {L,S} = *(x.val, y.val) +Base. *(x::Level{L,S}, y::Gain) where {L,S} = error("logarithmic gains add, not multiply.") + +Base. *(x::Number, y::Gain) = *(y,x) +Base. *(x::Bool, y::Gain) = *(y,x) # for method ambiguity +Base. *(x::Gain{L}, y::Number) where {L} = Gain{L}(x.val * y) +Base. *(x::Gain{L}, y::Bool) where {L} = Gain{L}(x.val * y) # for method ambiguity +Base. *(x::Gain{L}, y::Level) where {L} = error("logarithmic gains add, not multiply.") +Base. *(x::Gain{L}, y::Gain) where {L} = error("logarithmic gains add, not multiply.") + +Base. *(x::Quantity, y::Gain{L}) where {L} = + isrootpower(L, dimension(x)) ? rootpowerratio(y) * x : powerratio(y) * x +Base. *(x::Gain, y::Quantity) = *(y,x) + +# Division +Base. /(x::Number, y::Level) = x / y.val +Base. /(x::Level{L,S}, y::Number) where {L,S} = Level{L,S}(x.val / y) +Base. /(x::Level{L,S}, y::Quantity) where {L,S} = x.val / y +Base. /(x::Level{L,S}, y::Level) where {L,S} = x.val / y.val +Base. /(x::Level{L,S}, y::Gain) where {L,S} = error("logarithmic gains subtract, not divide.") +Base. /(x::Quantity, y::Gain) = error("logarithmic gains subtract, not divide.") +Base. /(x::Quantity, y::Level) = x / y.val + +function (Base.promote_rule(::Type{Level{L1,S1,T1}}, ::Type{Level{L2,S2,T2}}) + where {L1,L2,S1,S2,T1<:Number,T2<:Number}) + if L1 == L2 + if S1 == S2 + # Use convert(promote_type(typeof(S1), typeof(S2)), S1) instead of S1? + return Level{L1, S1, promote_type(T1,T2)} + else + return promote_type(T1,T2) + end + else + return promote_type(T1,T2) + end +end + +function Base.promote_rule(::Type{Level{L,R,S}}, ::Type{Quantity{T,D,U}}) where {L,R,S,T,D,U} + return promote_type(S, Quantity{T,D,U}) +end +function Base.promote_rule(::Type{Quantity{T,D,U}}, ::Type{Level{L,R,S}}) where {L,R,S,T,D,U} + return promote_type(S, Quantity{T,D,U}) +end + +Base.promote_rule(::Type{G1}, ::Type{G2}) where {L,T1,T2, G1<:Gain{L,T1}, G2<:Gain{L,T2}} = + Gain{L,promote_type(T1,T2)} +Base.promote_rule(A::Type{G}, B::Type{N}) where {L,T1, G<:Gain{L,T1}, N<:Number} = + error("no automatic promotion of $A and $B.") +Base.promote_rule(A::Type{G}, B::Type{L}) where {G<:Gain, L2, L<:Level{L2}} = LogScaled{L2} + +Base.convert(::Type{Quantity{T,D,U}}, x::Level) where {T,D,U} = + convert(Quantity{T,D,U}, x.val) +Base.convert(::Type{Quantity{T}}, x::Level) where {T<:Number} = convert(Quantity{T}, x.val) +Base.convert(::Type{T}, x::Quantity) where {L,S, T<:Level{L,S}} = T(x) + +function Base.show(io::IO, x::Gain) + print(io, x.val, " ", abbr(x)) + nothing +end +function Base.show(io::IO, x::Level) + print(io, ustrip(x), " ", abbr(x)) + nothing +end + +function Base.show(io::IO, x::Quantity{<:Union{Level,Gain},D,U}) where {D,U} + print(io, "[") + show(io, x.val) + print(io, "]") + if !isunitless(U()) + print(io," ") + show(io, U()) + end + nothing +end + +for li in (:B, :dB, :Np) + @eval begin + $(Expr(:export, Symbol("@",li))) + + macro ($li)(r::Union{Real,Symbol}) + throw(ArgumentError(join(["usage: `@", $(String(li)), " (a)/(b)`"]))) + end + + macro ($li)(expr::Expr) + s = $(Symbol("_", li)) + expr.args[1] != :/ && + throw(ArgumentError(join(["usage: `@", $(String(li)), " (a)/(b)`"]))) + length(expr.args) != 3 && + throw(ArgumentError(join(["usage: `@", $(String(li)), " (a)/(b)`"]))) + esc(quote + ($s)($(expr.args[2]), $(expr.args[3])) + end) + end + + function $(Symbol("_", li))(num::Number, den::Number) + dimension(num) != dimension(den) && throw(DimensionError(num,den)) + dimension(num) == NoDims && + throw(ArgumentError("cannot use this macro with dimensionless numbers.")) + return Level{$(Symbol("li_", li)), den}(num) + end + + function $(Symbol("_", li))(num::Number, den::Units) + $(Symbol("_", li))(num, 1*den) + end + end +end + +""" + powerratio(::Type{T}, x::Real) where {T<:Number} = convert(T, x) +Returns the gain as a ratio of power quantities. + +It is important to note that this function is undefined for `Quantity{<:Gain}` types. It is +tempting to make this function transform `-20dB/m` into `0.01/m`, however this means +something fundamentally different than `-20dB/m`, and cannot be used to calculate +exponential attenuation. +""" +function powerratio end +powerratio(x) = powerratio(NoUnits, x) +powerratio(::Units{()}, x::Gain{L}) where {L} = + fromlog(L, 1, ifelse(isrootpower(L), 2, 1)*x.val) +powerratio(::Units{()}, x::Real) = x +powerratio(u::MixedUnits{<:Gain}, x::Gain) = uconvert(u, x) +powerratio(u::T, x::Real) where {L, T <: MixedUnits{Gain{L}, <:Units{()}}} = + ifelse(isrootpower(L), 0.5, 1) * tolog(L, 1, x) * u + +""" + rootpowerratio(x::Gain) +Returns the gain as a ratio of root-power quantities (field quantities), a `Real` number. + + rootpowerratio(::Type{T}, x::Gain) where {T} +Returns the gain as a ratio of root-power quantities (field quantities), a `Real` number, +and converts to type `T`. + + rootpowerratio(x::Real) = x + rootpowerratio(::Type{T}, x::Real) where {T} = convert(T, x) +Fall-back methods so that `rootpowerratio` may be used generically. + +It is important to note that this function is undefined for `Quantity{<:Gain}` types. It is +tempting to make this function transform `-20dB/m` into `0.1/m`, however this means +something fundamentally different than `-20dB/m`, and cannot be used to calculate +exponential attenuation. + +`fieldratio` and `rootpowerratio` are synonymous, so you can save some typing if you like. +""" +function rootpowerratio end +rootpowerratio(x) = rootpowerratio(NoUnits, x) +rootpowerratio(::Units{()}, x::Gain{L}) where {L} = + fromlog(L, 1, ifelse(isrootpower(L), 1.0, 0.5)*x.val) +rootpowerratio(::Units{()}, x::Real) = x +rootpowerratio(u::MixedUnits{<:Gain}, x::Gain) = uconvert(u, x) +rootpowerratio(u::T, x::Real) where {L, T <: MixedUnits{Gain{L}, <:Units{()}}} = + ifelse(isrootpower(L), 1, 2) * tolog(L, 1, x) * u + +fieldratio = rootpowerratio + +""" + 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. +""" +linear(x::Quantity{<:Level,D,U}) where {D,U} = (x.val.val)*U() +linear(x::Quantity{<:Gain}) = error("use powerratio or rootpowerratio instead.") +linear(x::Level) = x.val +linear(x::Number) = x + +""" + logfn(x::LogInfo) +Returns the appropriate logarithm function to use in calculations involving the +logarithmic unit / quantity. For example, decibel-based units yield `log10`, +Neper-based yield `ln`, and so on. Returns `x->log(base, x)` as a fallback. +""" +function logfn end +logfn(x::LogInfo{N,10}) where {N} = log10 +logfn(x::LogInfo{N,2}) where {N} = log2 +logfn(x::LogInfo{N,e}) where {N} = log +logfn(x::LogInfo{N,B}) where {N,B} = x->log(B,x) + +""" + expfn(x::LogInfo) +Returns the appropriate exponential function to use in calculations involving the +logarithmic unit / quantity. For example, decibel-based units yield `exp10`, +Neper-based yield `exp`, and so on. Returns `x->(base)^x` as a fallback. +""" +function expfn end +expfn(x::LogInfo{N,10}) where {N} = exp10 +expfn(x::LogInfo{N,2}) where {N} = exp2 +expfn(x::LogInfo{N,e}) where {N} = exp +expfn(x::LogInfo{N,B}) where {N,B} = x->B^x + +Base.rtoldefault(::Type{Level{L,S,T}}) where {L,S,T} = + Base.rtoldefault(typeof(tolog(L,S,oneunit(T)/S))) +Base.rtoldefault(::Type{Gain{L,T}}) where {L,T} = Base.rtoldefault(T) + +Base.isapprox(x::Level, y::Level; kwargs...) = isapprox(promote(x,y)...; kwargs...) +Base.isapprox(x::T, y::T; kwargs...) where {T <: Level} = _isapprox(x, y; kwargs...) +_isapprox(x::Level{L,S,T}, y::Level{L,S,T}; atol = Level{L,S}(S), kwargs...) where {L,S,T} = + isapprox(ustrip(x), ustrip(y); atol = ustrip(convert(Level{L,S}, atol)), + kwargs...) + +Base.isapprox(x::Gain, y::Gain; kwargs...) = isapprox(promote(x,y)...; kwargs...) +Base.isapprox(x::T, y::T; kwargs...) where {T <: Gain} = _isapprox(x, y; kwargs...) +_isapprox(x::Gain{L,T}, y::Gain{L,T}; atol = Gain{L}(oneunit(T)), kwargs...) where {L,T} = + isapprox(ustrip(x), ustrip(y); atol = ustrip(convert(Gain{L,T}, atol)), kwargs...) diff --git a/src/pkgdefaults.jl b/src/pkgdefaults.jl index cf30d338..165b7e6b 100644 --- a/src/pkgdefaults.jl +++ b/src/pkgdefaults.jl @@ -10,6 +10,8 @@ @dimension 𝐉 "𝐉" Luminosity @dimension 𝐍 "𝐍" Amount +include("temperature.jl") + # Define derived dimensions. @derived_dimension Area 𝐋^2 @derived_dimension Volume 𝐋^3 diff --git a/src/promotion.jl b/src/promotion.jl index 6fc611cd..54cdcd47 100644 --- a/src/promotion.jl +++ b/src/promotion.jl @@ -1,3 +1,62 @@ +""" + promote_unit(::Units, ::Units...) +Given `Units` objects as arguments, this function returns a `Units` object appropriate +for the result of promoting quantities which have these units. This function is kind +of like `promote_rule`, except that it doesn't take types. It also does not return a tuple, +but rather just a [`Unitful.Units`](@ref) object (or it throws an error). + +Although we had used `promote_rule` for `Units` objects in prior versions of Unitful, +this was always kind of a hack; it doesn't make sense to promote units directly for +a variety of reasons. +""" +function promote_unit end + +# Generic methods +@inline promote_unit(x) = _promote_unit(x) +@inline _promote_unit(x::Units) = x + +@inline promote_unit(x,y) = _promote_unit(x,y) + +promote_unit(x::Units, y::Units, z::Units, t::Units...) = + promote_unit(_promote_unit(x,y), z, t...) + +# Use configurable fall-back mechanism for FreeUnits +@inline _promote_unit(x::T, y::T) where {T <: FreeUnits} = T() +@inline _promote_unit(x::FreeUnits{N1,D}, y::FreeUnits{N2,D}) where {N1,N2,D} = + upreferred(dimension(x)) + +# same units, but promotion context disagrees +@inline _promote_unit(x::T, y::T) where {T <: ContextUnits} = T() #ambiguity reasons +@inline _promote_unit(x::ContextUnits{N,D,P1}, y::ContextUnits{N,D,P2}) where {N,D,P1,P2} = + ContextUnits{N,D,promote_unit(P1(), P2())}() +# different units, but promotion context agrees +@inline _promote_unit(x::ContextUnits{N1,D,P}, y::ContextUnits{N2,D,P}) where {N1,N2,D,P} = + ContextUnits(P(), P()) +# different units, promotion context disagrees, fall back to FreeUnits +@inline _promote_unit(x::ContextUnits{N1,D}, y::ContextUnits{N2,D}) where {N1,N2,D} = + promote_unit(FreeUnits(x), FreeUnits(y)) + +# ContextUnits beat FreeUnits +@inline _promote_unit(x::ContextUnits{N,D}, y::FreeUnits{N,D}) where {N,D} = x +@inline _promote_unit(x::ContextUnits{N1,D,P}, y::FreeUnits{N2,D}) where {N1,N2,D,P} = + ContextUnits(P(), P()) +@inline _promote_unit(x::FreeUnits, y::ContextUnits) = promote_unit(y,x) + +# FixedUnits beat everything +@inline _promote_unit(x::T, y::T) where {T <: FixedUnits} = T() +@inline _promote_unit(x::FixedUnits{M,D}, y::Units{N,D}) where {M,N,D} = x +@inline _promote_unit(x::Units, y::FixedUnits) = promote_unit(y,x) + +# Different units but same dimension are not fungible for FixedUnits +@inline _promote_unit(x::FixedUnits{M,D}, y::FixedUnits{N,D}) where {M,N,D} = + error("automatic conversion prohibited.") + +# If we didn't handle it above, the dimensions mismatched. +@inline _promote_unit(x::Units, y::Units) = throw(DimensionError(x,y)) + +#### +# Base.promote_rule + # quantity, quantity (different dims) Base.promote_rule(::Type{Quantity{S1,D1,U1}}, ::Type{Quantity{S2,D2,U2}}) where {S1,D1,U1,S2,D2,U2} = diff --git a/src/quantities.jl b/src/quantities.jl new file mode 100644 index 00000000..2de75c8c --- /dev/null +++ b/src/quantities.jl @@ -0,0 +1,357 @@ + +""" + Quantity(x::Number, y::Units) +Outer constructor for `Quantity`s. This is a generated function to avoid +determining the dimensions of a given set of units each time a new quantity is +made. +""" +@generated function Quantity(x::Number, y::Units) + u = y() + du = dimension(u) + dx = dimension(x) + d = du*dx + :(Quantity{typeof(x), typeof($d), typeof($u)}(x)) +end +Quantity(x::Number, y::Units{()}) = x + +*(x::Number, y::Units, z::Units...) = Quantity(x,*(y,z...)) +*(x::Units, y::Number) = *(y,x) + +*(x::Quantity, y::Units, z::Units...) = Quantity(x.val, *(unit(x),y,z...)) +*(x::Quantity, y::Quantity) = Quantity(x.val*y.val, unit(x)*unit(y)) + +# Next two lines resolves some method ambiguity: +*(x::Bool, y::T) where {T <: Quantity} = + ifelse(x, y, ifelse(signbit(y), -zero(y), zero(y))) +*(x::Quantity, y::Bool) = Quantity(x.val*y, unit(x)) + +*(y::Number, x::Quantity) = *(x,y) +*(x::Quantity, y::Number) = Quantity(x.val*y, unit(x)) + +# looked in arraymath.jl for similar code +function *(A::Units, B::AbstractArray{T}) where {T} + F = similar(B, Base.promote_op(*, typeof(A), T)) + for (iF, iB) in zip(eachindex(F), eachindex(B)) + @inbounds F[iF] = *(A, B[iB]) + end + return F +end + +function *(A::AbstractArray{T}, B::Units) where {T} + F = similar(A, Base.promote_op(*, T, typeof(B))) + for (iF, iA) in zip(eachindex(F), eachindex(A)) + @inbounds F[iF] = *(A[iA], B) + end + return F +end + +# Division (units) +/(x::Quantity, y::Units) = Quantity(x.val, unit(x) / y) +/(x::Units, y::Quantity) = Quantity(1/y.val, x / unit(y)) +/(x::Number, y::Units) = Quantity(x,inv(y)) +/(x::Units, y::Number) = (1/y) * x + +//(x::Quantity, y::Units) = Quantity(x.val, unit(x) / y) +//(x::Units, y::Quantity) = Quantity(1//y.val, x / unit(y)) +//(x::Number, y::Units) = Rational(x)/y +//(x::Units, y::Number) = (1//y) * x + +# Division (quantities) +for op in (:/, ://) + @eval begin + ($op)(x::Quantity, y::Quantity) = Quantity(($op)(x.val, y.val), unit(x) / unit(y)) + ($op)(x::Quantity, y::Number) = Quantity(($op)(x.val, y), unit(x)) + ($op)(x::Number, y::Quantity) = Quantity(($op)(x, y.val), inv(unit(y))) + end +end + +# ambiguity resolution +//(x::Quantity, y::Complex) = Quantity(//(x.val, y), unit(x)) + +for f in (:div, :fld, :cld) + @eval function ($f)(x::Quantity, y::Quantity) + z = uconvert(unit(y), x) # TODO: use promote? + ($f)(z.val,y.val) + end +end + +for f in (:mod, :rem) + @eval function ($f)(x::Quantity, y::Quantity) + z = uconvert(unit(y), x) # TODO: use promote? + Quantity(($f)(z.val,y.val), unit(y)) + end +end + +# Addition / subtraction +for op in [:+, :-] + @eval ($op)(x::Quantity{S,D,U}, y::Quantity{T,D,U}) where {S,T,D,U} = + Quantity(($op)(x.val,y.val), U()) + + # If not generated, there are run-time allocations + @eval function ($op)(x::Quantity{S,D,SU}, y::Quantity{T,D,TU}) where {S,T,D,SU,TU} + ($op)(promote(x,y)...) + end + + @eval ($op)(x::Quantity, y::Quantity) = throw(DimensionError(x,y)) + @eval ($op)(x::Quantity) = Quantity(($op)(x.val),unit(x)) +end + +# Needed until LU factorization is made to work with unitful numbers +function inv(x::StridedMatrix{T}) where {T <: Quantity} + m = inv(ustrip(x)) + iq = eltype(m) + reinterpret(Quantity{iq, typeof(inv(dimension(T))), typeof(inv(unit(T)))}, m) +end + +for x in (:istriu, :istril) + @eval ($x)(A::AbstractMatrix{T}) where {T <: Quantity} = ($x)(ustrip(A)) +end + +# Other mathematical functions + +# `fma` and `muladd` +# The idea here is that if the numeric backing types are not the same, they +# will be promoted to be the same by the generic `fma(::Number, ::Number, ::Number)` +# method. We then catch the possible results and handle the units logic with one +# performant method. + +for (_x,_y) in [(:fma, :_fma), (:muladd, :_muladd)] + @static if VERSION >= v"0.6.0-" # work-around Julia issue 20103 + # Catch some signatures pre-promotion + @eval @inline ($_x)(x::Number, y::Quantity, z::Quantity) = ($_y)(x,y,z) + @eval @inline ($_x)(x::Quantity, y::Number, z::Quantity) = ($_y)(x,y,z) + + # Post-promotion + @eval @inline ($_x)(x::Quantity{T}, y::Quantity{T}, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) + else + @eval @inline ($_x)(x::Quantity{T}, y::T, z::T) where {T <: Number} = ($_y)(x,y,z) + @eval @inline ($_x)(x::T, y::Quantity{T}, z::T) where {T <: Number} = ($_y)(x,y,z) + @eval @inline ($_x)(x::T, y::T, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) + @eval @inline ($_x)(x::Quantity{T}, y::Quantity{T}, z::T) where {T <: Number} = ($_y)(x,y,z) + @eval @inline ($_x)(x::T, y::Quantity{T}, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) + @eval @inline ($_x)(x::Quantity{T}, y::T, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) + @eval @inline ($_x)(x::Quantity{T}, y::Quantity{T}, z::Quantity{T}) where {T <: Number} = ($_y)(x,y,z) + end + + # It seems like most of this is optimized out by the compiler, including the + # apparent runtime check of dimensions, which does not appear in @code_llvm. + @eval @inline function ($_y)(x,y,z) + dimension(x) * dimension(y) != dimension(z) && throw(DimensionError(x*y,z)) + uI = unit(x)*unit(y) + uF = promote_unit(uI, unit(z)) + c = ($_x)(ustrip(x), ustrip(y), ustrip(uconvert(uI, z))) + uconvert(uF, Quantity(c, uI)) + end +end + +sqrt(x::Quantity) = Quantity(sqrt(x.val), sqrt(unit(x))) +cbrt(x::Quantity) = Quantity(cbrt(x.val), cbrt(unit(x))) + +for _y in (:sin, :cos, :tan, :cot, :sec, :csc, :cis) + @eval ($_y)(x::DimensionlessQuantity) = ($_y)(uconvert(NoUnits, x)) +end + +atan2(y::Quantity, x::Quantity) = atan2(promote(y,x)...) +atan2(y::Quantity{T,D,U}, x::Quantity{T,D,U}) where {T,D,U} = atan2(y.val,x.val) +atan2(y::Quantity{T,D1,U1}, x::Quantity{T,D2,U2}) where {T,D1,U1,D2,U2} = + throw(DimensionError(x,y)) + +for (f, F) in [(:min, :<), (:max, :>)] + @eval @generated function ($f)(x::Quantity, y::Quantity) #TODO + xdim = x.parameters[2]() + ydim = y.parameters[2]() + if xdim != ydim + return :(throw(DimensionError(x,y))) + end + + isa(x.parameters[3](), FixedUnits) && + isa(y.parameters[3](), FixedUnits) && + x.parameters[3] !== y.parameters[3] && + error("automatic conversion prohibited.") + + xunits = x.parameters[3].parameters[1] + yunits = y.parameters[3].parameters[1] + + factx = mapreduce((x,y)->broadcast(*,x,y), xunits) do x + vcat(basefactor(x)...) + end + facty = mapreduce((x,y)->broadcast(*,x,y), yunits) do x + vcat(basefactor(x)...) + end + + tensx = mapreduce(tensfactor, +, xunits) + tensy = mapreduce(tensfactor, +, yunits) + + convx = *(factx..., (10.0)^tensx) + convy = *(facty..., (10.0)^tensy) + + :($($F)(x.val*$convx, y.val*$convy) ? x : y) + end +end + +abs(x::Quantity) = Quantity(abs(x.val), unit(x)) +abs2(x::Quantity) = Quantity(abs2(x.val), unit(x)*unit(x)) + +copysign(x::Quantity, y::Number) = Quantity(copysign(x.val,y/unit(y)), unit(x)) +flipsign(x::Quantity, y::Number) = Quantity(flipsign(x.val,y/unit(y)), unit(x)) + +@inline isless(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = _isless(x,y) +@inline _isless(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = isless(x.val, y.val) +@inline _isless(x::Quantity{T,D1,U1}, y::Quantity{T,D2,U2}) where {T,D1,D2,U1,U2} = throw(DimensionError(x,y)) +@inline _isless(x,y) = isless(x,y) + +isless(x::Quantity, y::Quantity) = _isless(promote(x,y)...) +isless(x::Quantity, y::Number) = _isless(promote(x,y)...) +isless(x::Number, y::Quantity) = _isless(promote(x,y)...) + +@inline <(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = _lt(x,y) +@inline _lt(x::Quantity{T,D,U}, y::Quantity{T,D,U}) where {T,D,U} = <(x.val,y.val) +@inline _lt(x::Quantity{T,D1,U1}, y::Quantity{T,D2,U2}) where {T,D1,D2,U1,U2} = throw(DimensionError(x,y)) +@inline _lt(x,y) = <(x,y) + +<(x::Quantity, y::Quantity) = _lt(promote(x,y)...) +<(x::Quantity, y::Number) = _lt(promote(x,y)...) +<(x::Number, y::Quantity) = _lt(promote(x,y)...) + +Base.rtoldefault(::Type{Quantity{T,D,U}}) where {T,D,U} = Base.rtoldefault(T) +isapprox(x::Quantity{T,D,U}, y::Quantity{T,D,U}; atol=zero(Quantity{real(T),D,U}), kwargs...) where {T,D,U} = + isapprox(x.val, y.val; atol=uconvert(unit(y), atol).val, kwargs...) +function isapprox(x::Quantity, y::Quantity; kwargs...) + dimension(x) != dimension(y) && return false + return isapprox(promote(x,y)...; kwargs...) +end +isapprox(x::Quantity, y::Number; kwargs...) = isapprox(promote(x,y)...; kwargs...) +isapprox(x::Number, y::Quantity; kwargs...) = isapprox(y, x; kwargs...) + +function isapprox(x::AbstractArray{Quantity{T1,D,U1}}, + y::AbstractArray{Quantity{T2,D,U2}}; rtol::Real=Base.rtoldefault(T1,T2), + atol=zero(Quantity{T1,D,U1}), norm::Function=vecnorm) where {T1,D,U1,T2,U2} + + d = norm(x - y) + if isfinite(d) + return d <= atol + rtol*max(norm(x), norm(y)) + else + # Fall back to a component-wise approximate comparison + return all(ab -> isapprox(ab[1], ab[2]; rtol=rtol, atol=atol), zip(x, y)) + end +end +isapprox(x::AbstractArray{S}, y::AbstractArray{T}; + kwargs...) where {S <: Quantity,T <: Quantity} = false +function isapprox(x::AbstractArray{S}, y::AbstractArray{N}; + kwargs...) where {S <: Quantity,N <: Number} + if dimension(N) == dimension(S) + isapprox(map(x->uconvert(NoUnits,x),x),y; kwargs...) + else + false + end +end +isapprox(y::AbstractArray{N}, x::AbstractArray{S}; + kwargs...) where {S <: Quantity,N <: Number} = isapprox(x,y; kwargs...) + +==(x::Quantity{S,D,U}, y::Quantity{T,D,U}) where {S,T,D,U} = (x.val == y.val) +function ==(x::Quantity, y::Quantity) + dimension(x) != dimension(y) && return false + ==(promote(x,y)...) +end + +function ==(x::Quantity, y::Number) + ==(promote(x,y)...) +end +==(x::Number, y::Quantity) = ==(y,x) +<=(x::Quantity, y::Quantity) = <(x,y) || x==y + +_dimerr(f) = error("$f can only be well-defined for dimensionless ", + "numbers. For dimensionful numbers, different input units yield physically ", + "different results.") +isinteger(x::Quantity) = _dimerr(isinteger) +isinteger(x::DimensionlessQuantity) = isinteger(uconvert(NoUnits, x)) +for f in (:floor, :ceil, :trunc, :round) + @eval ($f)(x::Quantity) = _dimerr($f) + @eval ($f)(x::DimensionlessQuantity) = ($f)(uconvert(NoUnits, x)) + @eval ($f)(::Type{T}, x::Quantity) where {T <: Integer} = _dimerr($f) + @eval ($f)(::Type{T}, x::DimensionlessQuantity) where {T <: Integer} = ($f)(T, uconvert(NoUnits, x)) +end + +zero(x::Quantity) = Quantity(zero(x.val), unit(x)) +zero(x::Type{Quantity{T,D,U}}) where {T,D,U} = zero(T)*U() + +one(x::Quantity) = one(x.val) +one(x::Type{Quantity{T,D,U}}) where {T,D,U} = one(T) + +isreal(x::Quantity) = isreal(x.val) +isfinite(x::Quantity) = isfinite(x.val) +isinf(x::Quantity) = isinf(x.val) +isnan(x::Quantity) = isnan(x.val) + +unsigned(x::Quantity) = Quantity(unsigned(x.val), unit(x)) + +for f in (:exp, :exp10, :exp2, :expm1, :log, :log10, :log1p, :log2) + @eval ($f)(x::DimensionlessQuantity) = ($f)(uconvert(NoUnits, x)) +end + +real(x::Quantity) = Quantity(real(x.val), unit(x)) +imag(x::Quantity) = Quantity(imag(x.val), unit(x)) +conj(x::Quantity) = Quantity(conj(x.val), unit(x)) + +@inline vecnorm(x::Quantity, p::Real=2) = + p == 0 ? (x==zero(x) ? typeof(abs(x))(0) : typeof(abs(x))(1)) : abs(x) + +""" + sign(x::Quantity) +Returns the sign of `x`. +""" +sign(x::Quantity) = sign(x.val) + +""" + signbit(x::Quantity) +Returns the sign bit of the underlying numeric value of `x`. +""" +signbit(x::Quantity) = signbit(x.val) + +prevfloat(x::Quantity{T}) where {T <: AbstractFloat} = Quantity(prevfloat(x.val), unit(x)) +nextfloat(x::Quantity{T}) where {T <: AbstractFloat} = Quantity(nextfloat(x.val), unit(x)) + +function frexp(x::Quantity{T}) where {T <: AbstractFloat} + a,b = frexp(x.val) + a*unit(x), b +end + +""" + float(x::Quantity) +Convert the numeric backing type of `x` to a floating-point representation. +Returns a `Quantity` with the same units. +""" +float(x::Quantity) = Quantity(float(x.val), unit(x)) + +""" + Integer(x::Quantity) +Convert the numeric backing type of `x` to an integer representation. +Returns a `Quantity` with the same units. +""" +Integer(x::Quantity) = Quantity(Integer(x.val), unit(x)) + +""" + Rational(x::Quantity) +Convert the numeric backing type of `x` to a rational number representation. +Returns a `Quantity` with the same units. +""" +Rational(x::Quantity) = Quantity(Rational(x.val), unit(x)) + +typemin(::Type{Quantity{T,D,U}}) where {T,D,U} = typemin(T)*U() +typemin(x::Quantity{T}) where {T} = typemin(T)*unit(x) + +typemax(::Type{Quantity{T,D,U}}) where {T,D,U} = typemax(T)*U() +typemax(x::Quantity{T}) where {T} = typemax(T)*unit(x) + +Base.literal_pow(::typeof(^), x::Quantity, ::Type{Val{v}}) where {v} = + Quantity(Base.literal_pow(^, x.val, Val{v}), + Base.literal_pow(^, unit(x), Val{v})) + +# All of these are needed for ambiguity resolution +^(x::Quantity, y::Integer) = Quantity((x.val)^y, unit(x)^y) +^(x::Quantity, y::Rational) = Quantity((x.val)^y, unit(x)^y) +^(x::Quantity, y::Real) = Quantity((x.val)^y, unit(x)^y) + +Base.rand(r::AbstractRNG, ::Type{Quantity{T,D,U}}) where {T,D,U} = rand(r,T)*U() +Base.ones(Q::Type{<:Quantity}, dims::Tuple) = fill!(Array{Q}(dims), oneunit(Q)) +Base.ones(a::AbstractArray, Q::Type{<:Quantity}) = fill!(similar(a,Q), oneunit(Q)) diff --git a/src/range.jl b/src/range.jl index 99889e93..d1c05589 100644 --- a/src/range.jl +++ b/src/range.jl @@ -1,3 +1,7 @@ +*(y::Units, r::Range) = *(r,y) +*(r::Range, y::Units) = range(first(r)*y, step(r)*y, length(r)) +*(r::Range, y::Units, z::Units...) = *(x, *(y,z...)) + Base.linspace(start::Quantity{<:Real}, stop, len::Integer) = _linspace(promote(start, stop)..., len) Base.linspace(start, stop::Quantity{<:Real}, len::Integer) = diff --git a/src/temperature.jl b/src/temperature.jl index 61eb54dd..e28802f4 100644 --- a/src/temperature.jl +++ b/src/temperature.jl @@ -1,3 +1,9 @@ +""" + offsettemp(::Unit) +For temperature units, this function is used to set the scale offset. +""" +offsettemp(::Unit) = 0 + """ uconvert{T,D,U}(a::Units, x::Quantity{T,typeof(𝚯),<:TemperatureUnits}) In this method, we are special-casing temperature conversion to respect scale @@ -5,6 +11,7 @@ offsets, if they do not appear in combination with other dimensions. """ @generated function uconvert(a::Units, x::Quantity{T,typeof(𝚯),<:TemperatureUnits}) where {T} + # TODO: test, may be able to get bad things to happen here when T<:LogScaled if a == typeof(unit(x)) :(Quantity(x.val, a)) else diff --git a/src/types.jl b/src/types.jl index d6032076..b71ac87f 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,3 +1,11 @@ + +""" + abstract type Unitlike end +Represents units or dimensions. Dimensions are unit-like in the sense that they are +not numbers but you can multiply or divide them and exponentiate by rationals. +""" +abstract type Unitlike end + """ struct Dimension{D} power::Rational{Int} @@ -11,6 +19,15 @@ parameter `N` of a [`Dimensions{N}`](@ref) object. struct Dimension{D} power::Rational{Int} end +@inline name(x::Dimension{D}) where {D} = D +@inline power(x::Dimension) = x.power + +""" + struct Dimensions{N} <: Unitlike +Instances of this object represent dimensions, possibly combinations thereof. +""" +struct Dimensions{N} <: Unitlike end +const NoDims = Dimensions{()}() """ struct Unit{U,D} @@ -31,13 +48,10 @@ struct Unit{U,D} tens::Int power::Rational{Int} end - -""" - abstract type Unitlike end -Represents units or dimensions. Dimensions are unit-like in the sense that they are -not numbers but you can multiply or divide them and exponentiate by rationals. -""" -abstract type Unitlike end +@inline name(x::Unit{U}) where {U} = U +@inline tens(x::Unit) = x.tens +@inline power(x::Unit) = x.power +@inline dimension(u::Unit{U,D}) where {U,D} = D()^u.power """ abstract type Units{N,D} <: Unitlike end @@ -60,6 +74,8 @@ Unitful.Unit{:Second,typeof(𝐓)}(0,-1//1,1.0,1//1)),typeof(𝐋/𝐓)}` is ret """ struct FreeUnits{N,D} <: Units{N,D} end FreeUnits(::Units{N,D}) where {N,D} = FreeUnits{N,D}() +const NoUnits = FreeUnits{(), Dimensions{()}}() +(y::FreeUnits)(x::Number) = uconvert(y,x) """ struct ContextUnits{N,D,P} <: Units{N,D} @@ -74,6 +90,7 @@ function ContextUnits(x::Units{N,D}, y::Units) where {N,D} ContextUnits{N,D,typeof(FreeUnits(y))}() end ContextUnits(u::Units{N,D}) where {N,D} = ContextUnits{N,D,typeof(FreeUnits(upreferred(u)))}() +(y::ContextUnits)(x::Number) = uconvert(y,x) """ struct FixedUnits{N,D} <: Units{N,D} end @@ -84,12 +101,6 @@ conversions. See [Advanced promotion mechanisms](@ref) in the docs for details. struct FixedUnits{N,D} <: Units{N,D} end FixedUnits(::Units{N,D}) where {N,D} = FixedUnits{N,D}() -""" - struct Dimensions{N} <: Unitlike -Instances of this object represent dimensions, possibly combinations thereof. -""" -struct Dimensions{N} <: Unitlike end - """" struct Quantity{T,D,U} <: Number A quantity, which has dimensions and units specified in the type signature. @@ -104,7 +115,7 @@ kept separate to permit convenient dispatch on dimensions. """ struct Quantity{T,D,U} <: Number val::T - Quantity{T,D,U}(v::Number) where {T,D,U} = new(v) + Quantity{T,D,U}(v::Number) where {T,D,U} = new{T,D,U}(v) Quantity{T,D,U}(v::Quantity) where {T,D,U} = convert(Quantity{T,D,U}, v) end @@ -121,3 +132,59 @@ true ``` """ const DimensionlessQuantity{T,U} = Quantity{T, Dimensions{()}, U} + +""" + struct LogInfo{N,B,P} +Describes a logarithmic unit. Type parameters include: +- `N`: The name of the logarithmic unit, e.g. `:Decibel`, `:Neper`. +- `B`: The base of the logarithm. +- `P`: A prefactor to multiply the logarithm when the log is of a power ratio. +""" +struct LogInfo{N,B,P} end +""" + abstract type LogScaled{L<:LogInfo} <: Number end +Abstract supertype of [`Unitful.Level`](@ref) and [`Unitful.Gain`](@ref). It is only +used in promotion to put levels and gains onto a common log scale. +""" +abstract type LogScaled{L<:LogInfo} <: Number end + +""" + struct Level{L, S, T<:Number} <: 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`](@ref) in that `val` is a linear quantity. +""" +struct Level{L, S, T<:Number} <: LogScaled{L} + val::T + function Level{L,S,T}(x) where {L,S,T} + dimension(S) != dimension(x) && throw(DimensionError(S,x)) + return new{L,S,T}(x) + end +end +function Level{L,S}(val::Number) where {L,S} + dimension(S) != dimension(val) && throw(DimensionError(S, val)) + return Level{L,S,typeof(val)}(val) +end + +""" + struct Gain{L, 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`](@ref) in that `val` is stored after computing the logarithm. +""" +struct Gain{L, T<:Real} <: LogScaled{L} + val::T +end + +""" + struct MixedUnits{T<:LogScaled, U<:Units} + +Struct for representing mixed logarithmic / linear units. Primarily useful as an +intermediate for `uconvert`. `T` is `<: Level` or `<: Gain`. +""" +struct MixedUnits{T<:LogScaled, U<:Units} + units::U +end +MixedUnits{T}() where {T} = MixedUnits{T, typeof(NoUnits)}(NoUnits) +MixedUnits{T}(u::Units) where {T} = MixedUnits{T,typeof(u)}(u) diff --git a/src/units.jl b/src/units.jl new file mode 100644 index 00000000..6845fee1 --- /dev/null +++ b/src/units.jl @@ -0,0 +1,235 @@ +@generated function *(a0::FreeUnits, a::FreeUnits...) + + # Sort the units uniquely. This is a generated function so that we + # don't have to figure out the units each time. + linunits = Vector{Unit}() + + for x in (a0, a...) + xp = x.parameters[1] + append!(linunits, xp[1:end]) + end + + # linunits is an Array containing all of the Unit objects that were + # found in the type parameters of the FreeUnits objects (a0, a...) + sort!(linunits, by=x->power(x)) + sort!(linunits, by=x->tens(x)) + sort!(linunits, by=x->name(x)) + + # [m,m,cm,cm^2,cm^3,nm,m^4,µs,µs^2,s] + # reordered as: + # [nm,cm,cm^2,cm^3,m,m,m^4,µs,µs^2,s] + + # Collect powers of a given unit into `c` + c = Vector{Unit}() + if !isempty(linunits) + i = start(linunits) + oldstate = linunits[i] + p = 0//1 + while !done(linunits, i) + (state, i) = next(linunits, i) + if tens(state) == tens(oldstate) && name(state) == name(oldstate) + p += power(state) + else + if p != 0 + push!(c, Unit{name(oldstate),dimtype(oldstate)}(tens(oldstate), p)) + end + p = power(state) + end + oldstate = state + end + if p != 0 + push!(c, Unit{name(oldstate),dimtype(oldstate)}(tens(oldstate), p)) + end + end + # results in: + # [nm,cm^6,m^6,µs^3,s] + + d = (c...) + f = typeof(mapreduce(dimension, *, NoDims, d)) + :(FreeUnits{$d,$f}()) +end +*(a0::ContextUnits, a::ContextUnits...) = + ContextUnits(*(FreeUnits(a0), FreeUnits.(a)...), + *(FreeUnits(upreferred(a0)), FreeUnits.((upreferred).(a))...)) +FreeOrContextUnits = Union{FreeUnits, ContextUnits} +*(a0::FreeOrContextUnits, a::FreeOrContextUnits...) = + *(ContextUnits(a0), ContextUnits.(a)...) +*(a0::FixedUnits, a::FixedUnits...) = + FixedUnits(*(FreeUnits(a0), FreeUnits.(a)...)) + +""" +``` +*(a0::Units, a::Units...) +``` + +Given however many units, multiply them together. This is actually handled by +a few different methods, since we have `FreeUnits`, `ContextUnits`, and `FixedUnits`. + +Collect [`Unitful.Unit`](@ref) objects from the type parameter of the +[`Unitful.Units`](@ref) objects. For identical units including SI prefixes +(i.e. cm ≠ m), collect powers and sort uniquely by the name of the `Unit`. +The unique sorting permits easy unit comparisons. + +Examples: + +```jldoctest +julia> u"kg*m/s^2" +kg m s^-2 + +julia> u"m/s*kg/s" +kg m s^-2 + +julia> typeof(u"m/s*kg/s") == typeof(u"kg*m/s^2") +true +``` +""" +*(a0::Units, a::Units...) = FixedUnits(*(FreeUnits(a0), FreeUnits.(a)...)) +# Logic above is that if we're not using FreeOrContextUnits, at least one is FixedUnits. + +/(x::Units, y::Units) = *(x,inv(y)) +//(x::Units, y::Units) = x/y + +# Both methods needed for ambiguity resolution +^(x::Unit{U,D}, y::Integer) where {U,D} = Unit{U,D}(tens(x), power(x)*y) +^(x::Unit{U,D}, y::Number) where {U,D} = Unit{U,D}(tens(x), power(x)*y) + +# A word of caution: +# Exponentiation is not type-stable for `Units` objects. +# Dimensions get reconstructed anyway so we pass () for the D type parameter... +^(x::FreeUnits{N}, y::Integer) where {N} = *(FreeUnits{map(a->a^y, N), ()}()) +^(x::FreeUnits{N}, y::Number) where {N} = *(FreeUnits{map(a->a^y, N), ()}()) + +^(x::ContextUnits{N,D,P}, y::Integer) where {N,D,P} = + *(ContextUnits{map(a->a^y, N), (), typeof(P()^y)}()) +^(x::ContextUnits{N,D,P}, y::Number) where {N,D,P} = + *(ContextUnits{map(a->a^y, N), (), typeof(P()^y)}()) + +^(x::FixedUnits{N}, y::Integer) where {N} = *(FixedUnits{map(a->a^y, N), ()}()) +^(x::FixedUnits{N}, y::Number) where {N} = *(FixedUnits{map(a->a^y, N), ()}()) + +@generated function Base.literal_pow(::typeof(^), x::FreeUnits{N}, ::Type{Val{p}}) where {N,p} + y = *(FreeUnits{map(a->a^p, N), ()}()) + :($y) +end +@generated function Base.literal_pow(::typeof(^), x::ContextUnits{N,D,P}, ::Type{Val{p}}) where {N,D,P,p} + y = *(ContextUnits{map(a->a^p, N), (), typeof(P()^p)}()) + :($y) +end +@generated function Base.literal_pow(::typeof(^), x::FixedUnits{N}, ::Type{Val{p}}) where {N,p} + y = *(FixedUnits{map(a->a^p, N), ()}()) + :($y) +end + +# Since exponentiation is not type stable, we define a special `inv` method to enable fast +# division. For julia 0.6.0, the appropriate methods for ^ and * need to be defined before +# this one! +for (fun,pow) in ((:inv, -1//1), (:sqrt, 1//2), (:cbrt, 1//3)) + # The following are generated functions to ensure type stability. + @eval @generated function ($fun)(x::FreeUnits) + unittuple = map(x->x^($pow), x.parameters[1]) + y = *(FreeUnits{unittuple,()}()) # sort appropriately + :($y) + end + + @eval @generated function ($fun)(x::ContextUnits) + unittuple = map(x->x^($pow), x.parameters[1]) + promounit = ($fun)(x.parameters[3]()) + y = *(ContextUnits{unittuple,(),typeof(promounit)}()) # sort appropriately + :($y) + end + + @eval @generated function ($fun)(x::FixedUnits) + unittuple = map(x->x^($pow), x.parameters[1]) + y = *(FixedUnits{unittuple,()}()) # sort appropriately + :($y) + end +end + +function tensfactor(x::Unit) + p = power(x) + if isinteger(p) + p = Integer(p) + end + tens(x)*p +end + +@generated function tensfactor(x::Units) + tunits = x.parameters[1] + a = mapreduce(tensfactor, +, 0, tunits) + :($a) +end + +# This is type unstable but +# a) this method is not called by the user +# b) ultimately the instability will only be present at compile time as it is +# hidden behind a "generated function barrier" +function basefactor(inex, ex, eq, tens, p) + # Sometimes (x::Rational)^1 can fail for large rationals because the result + # is of type x*x so we do a hack here + function dpow(x,p) + if p == 0 + 1 + elseif p == 1 + x + elseif p == -1 + 1//x + else + x^p + end + end + + if isinteger(p) + p = Integer(p) + end + + eq_is_exact = false + output_ex_float = (10.0^tens * float(ex))^p + eq_raised = float(eq)^p + if isa(eq, Integer) || isa(eq, Rational) + output_ex_float *= eq_raised + eq_is_exact = true + end + + can_exact = (output_ex_float < typemax(Int)) + can_exact &= (1/output_ex_float < typemax(Int)) + can_exact &= isinteger(p) + + can_exact2 = (eq_raised < typemax(Int)) + can_exact2 &= (1/eq_raised < typemax(Int)) + can_exact2 &= isinteger(p) + + if can_exact + if eq_is_exact + # If we got here then p is an integer. + # Note that sometimes x^1 can cause an overflow error if x is large because + # of how power_by_squaring is implemented for Rationals, so we use dpow. + x = dpow(eq*ex*(10//1)^tens, p) + return (inex^p, isinteger(x) ? Int(x) : x) + else + x = dpow(ex*(10//1)^tens, p) + return ((inex*eq)^p, isinteger(x) ? Int(x) : x) + end + else + if eq_is_exact && can_exact2 + x = dpow(eq,p) + return ((inex * ex * 10.0^tens)^p, isinteger(x) ? Int(x) : x) + else + return ((inex * ex * 10.0^tens * eq)^p, 1) + end + end +end + +@inline basefactor(x::Unit{U}) where {U} = basefactor(basefactors[U]..., 1, 0, power(x)) + +function basefactor(x::Units{U}) where {U} + fact1 = map(basefactor, U) + inex1 = mapreduce(x->getfield(x,1), *, 1.0, fact1) + float_ex1 = mapreduce(x->float(getfield(x,2)), *, 1, fact1) + can_exact = (float_ex1 < typemax(Int)) + can_exact &= (1/float_ex1 < typemax(Int)) + if can_exact + inex1, mapreduce(x->getfield(x,2), *, 1, fact1) + else + inex1*float_ex1, 1 + end +end diff --git a/src/user.jl b/src/user.jl index 91185b2e..55258d7a 100644 --- a/src/user.jl +++ b/src/user.jl @@ -373,6 +373,7 @@ end replace_value(literal::Number) = literal +ustrcheck_bool(::MixedUnits) = true ustrcheck_bool(::Units) = true ustrcheck_bool(::Dimensions) = true ustrcheck_bool(::Quantity) = true diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 00000000..b22a4b3b --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,191 @@ +@inline isunitless(::Units) = false +@inline isunitless(::Units{()}) = true + +@inline numtype(::Quantity{T}) where {T} = T +@inline numtype(::Type{Quantity{T,D,U}}) where {T,D,U} = T +@inline dimtype(u::Unit{U,D}) where {U,D} = D + +""" + ustrip(x::Number) + ustrip(x::Quantity) +Returns the number out in front of any units. The value of `x` may differ from the number +out front of the units in the case of dimensionless quantities, e.g. `1m/mm != 1`. See +[`uconvert`](@ref) and the example below. Because the units are removed, information may be +lost and this should be used with some care. + +This function is mainly intended for compatibility with packages that don't know +how to handle quantities. + +```jldoctest +julia> ustrip(2u"μm/m") == 2 +true + +julia> uconvert(NoUnits, 2u"μm/m") == 2//1000000 +true +``` +""" +@inline ustrip(x::Number) = x / unit(x) +@inline ustrip(x::Quantity) = ustrip(x.val) + +""" + ustrip(x::Array{Q}) where {Q <: Quantity} +Strip units from an `Array` by reinterpreting to type `T`. The resulting +`Array` is a not a copy, but rather a unit-stripped view into array `x`. Because the units +are removed, information may be lost and this should be used with some care. + +This function is provided primarily for compatibility purposes; you could pass +the result to PyPlot, for example. + +```jldoctest +julia> a = [1u"m", 2u"m"] +2-element Array{Quantity{Int64, Dimensions:{𝐋}, Units:{m}},1}: + 1 m + 2 m + +julia> b = ustrip(a) +2-element Array{Int64,1}: + 1 + 2 + +julia> a[1] = 3u"m"; b +2-element Array{Int64,1}: + 3 + 2 +``` +""" +@inline ustrip(A::Array{Q}) where {Q <: Quantity} = reinterpret(numtype(Q), A) + +@deprecate(ustrip(A::AbstractArray{T}) where {T<:Number}, ustrip.(A)) + +""" + ustrip(A::Diagonal) + ustrip(A::Bidiagonal) + ustrip(A::Tridiagonal) + ustrip(A::SymTridiagonal) +Strip units from various kinds of matrices by calling `ustrip` on the underlying vectors. +""" +ustrip(A::Diagonal) = Diagonal(ustrip(A.diag)) +ustrip(A::Bidiagonal) = Bidiagonal(ustrip(A.dv), ustrip(A.ev), A.isupper) +ustrip(A::Tridiagonal) = Tridiagonal(ustrip(A.dl), ustrip(A.d), ustrip(A.du)) +ustrip(A::SymTridiagonal) = SymTridiagonal(ustrip(A.dv), ustrip(A.ev)) + +""" + unit(x::Quantity{T,D,U}) where {T,D,U} + unit(x::Type{Quantity{T,D,U}}) where {T,D,U} +Returns the units associated with a `Quantity` or `Quantity` type. + +Examples: + +```jldoctest +julia> unit(1.0u"m") == u"m" +true + +julia> unit(typeof(1.0u"m")) == u"m" +true +``` +""" +@inline unit(x::Quantity{T,D,U}) where {T,D,U} = U() +@inline unit(::Type{Quantity{T,D,U}}) where {T,D,U} = U() + + +""" + unit(x::Number) +Returns a `Unitful.Units{(), Dimensions{()}}` object to indicate that ordinary +numbers have no units. This is a singleton, which we export as `NoUnits`. +The unit is displayed as an empty string. + +Examples: + +```jldoctest +julia> typeof(unit(1.0)) +Unitful.FreeUnits{(),Unitful.Dimensions{()}} + +julia> typeof(unit(Float64)) +Unitful.FreeUnits{(),Unitful.Dimensions{()}} + +julia> unit(1.0) == NoUnits +true +``` +""" +@inline unit(x::Number) = NoUnits +@inline unit(x::Type{T}) where {T <: Number} = NoUnits + +""" + dimension(x::Number) + dimension(x::Type{T}) where {T<:Number} +Returns a `Unitful.Dimensions{()}` object to indicate that ordinary +numbers are dimensionless. This is a singleton, which we export as `NoDims`. +The dimension is displayed as an empty string. + +Examples: + +```jldoctest +julia> typeof(dimension(1.0)) +Unitful.Dimensions{()} +julia> typeof(dimension(Float64)) +Unitful.Dimensions{()} +julia> dimension(1.0) == NoDims +true +``` +""" +@inline dimension(x::Number) = NoDims +@inline dimension(x::Type{T}) where {T <: Number} = NoDims + +""" + dimension(u::Units{U,D}) where {U,D} +Returns a [`Unitful.Dimensions`](@ref) object corresponding to the dimensions +of the units, `D()`. For a dimensionless combination of units, a +`Unitful.Dimensions{()}` object is returned. + +Examples: + +```jldoctest +julia> dimension(u"m") +𝐋 + +julia> typeof(dimension(u"m")) +Unitful.Dimensions{(Unitful.Dimension{:Length}(1//1),)} + +julia> typeof(dimension(u"m/km")) +Unitful.Dimensions{()} +``` +""" +@inline dimension(u::Units{U,D}) where {U,D} = D() + +""" + dimension(x::Quantity{T,D}) where {T,D} + dimension(::Type{Quantity{T,D,U}}) where {T,D,U} +Returns a [`Unitful.Dimensions`](@ref) object `D()` corresponding to the +dimensions of quantity `x`. For a dimensionless [`Unitful.Quantity`](@ref), a +`Unitful.Dimensions{()}` object is returned. + +Examples: + +```jldoctest +julia> dimension(1.0u"m") +𝐋 + +julia> typeof(dimension(1.0u"m/μm")) +Unitful.Dimensions{()} +``` +""" +@inline dimension(x::Quantity{T,D}) where {T,D} = D() +@inline dimension(::Type{Quantity{T,D,U}}) where {T,D,U} = D() + +@deprecate(dimension(x::AbstractArray{T}) where {T<:Number}, dimension.(x)) +@deprecate(dimension(x::AbstractArray{T}) where {T<:Units}, dimension.(x)) + +""" + mutable struct DimensionError{T,S} <: Exception + x::T + y::S + end +Thrown when dimensions don't match in an operation that demands they do. +Display `x` and `y` in error message. +""" +mutable struct DimensionError{T,S} <: Exception + x::T + y::S +end +Base.showerror(io::IO, e::DimensionError) = + print(io, "DimensionError: $(e.x) and $(e.y) are not dimensionally compatible."); diff --git a/test/runtests.jl b/test/runtests.jl index b161b9eb..038469eb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -3,11 +3,11 @@ module UnitfulTests using Unitful using Base.Test -import Unitful: - DimensionError, - FreeUnits, - ContextUnits, - FixedUnits +import Unitful: DimensionError + +import Unitful: LogScaled, LogInfo, Level, Gain, MixedUnits, li_dB + +import Unitful: FreeUnits, ContextUnits, FixedUnits import Unitful: nm, μm, mm, cm, m, km, inch, ft, mi, @@ -16,7 +16,10 @@ import Unitful: °Ra, °F, °C, K, rad, °, ms, s, minute, hr, - J, A, N, mol, cd, V + J, A, N, mol, cd, V, + mW, W, Hz + +import Unitful: dB, dBm, dBV, dBSPL, Np import Unitful: 𝐋, 𝐓, 𝐍 @@ -29,8 +32,7 @@ import Unitful: Temperature, Action -import Unitful: - LengthUnits, AreaUnits, MassUnits +import Unitful: LengthUnits, AreaUnits, MassUnits @testset "Construction" begin @test isa(NoUnits, FreeUnits) @@ -292,6 +294,8 @@ end @test macroexpand(:(u"m/s")) == m/s @test macroexpand(:(u"1.0m/s")) == 1.0m/s @test macroexpand(:(u"m^-1")) == m^-1 + @test macroexpand(:(u"dB/Hz")) == dB/Hz + @test macroexpand(:(u"3.0dB/Hz")) == 3.0dB/Hz @test isa(macroexpand(:(u"N m")).args[1], ParseError) @test isa(macroexpand(:(u"abs(2)")).args[1], ErrorException) @@ -315,7 +319,8 @@ end @test @inferred(dimension(m/s)) === 𝐋/𝐓 @test @inferred(dimension(1u"mol")) === 𝐍 @test @inferred(dimension(μm/m)) === NoDims - @test dimension([1u"m", 1u"s"]) == [𝐋, 𝐓] + @test dimension.([1u"m", 1u"s"]) == [𝐋, 𝐓] + @test dimension.([u"m", u"s"]) == [𝐋, 𝐓] @test (𝐋/𝐓)^2 === 𝐋^2 / 𝐓^2 @test isa(m, LengthUnits) @test isa(ContextUnits(m,km), LengthUnits) @@ -510,7 +515,7 @@ end @test isapprox(1.0u"m",(1.0+eps(1.0))u"m") @test isapprox(1.0u"μm/m",1e-6) @test !isapprox(1.0u"μm/m",1e-7) - @test_throws DimensionError isapprox(1.0u"m",5) + @test !isapprox(1.0u"m",5) @test frexp(1.5m) == (0.75m, 1.0) @test unit(nextfloat(0.0m)) == m @test unit(prevfloat(0.0m)) == m @@ -555,7 +560,6 @@ end @test @inferred(fma(1.0, 1.0μm/m, 1.0μm/m)) === 2.0μm/m # llvm good @test @inferred(fma(2, 1.0, 1μm/m)) === 2.000001 # llvm BAD @test @inferred(fma(2, 1μm/m, 1mm/m)) === 501//500000 # llvm BAD - @test @inferred(muladd(2.0, 3.0m, 1.0m)) === 7.0m @test @inferred(muladd(2.0, 3.0m, 35mm)) === 6.035m @test @inferred(muladd(2.0m, 3.0, 35mm)) === 6.035m @@ -568,7 +572,6 @@ end @test @inferred(muladd(1.0, 1.0μm/m, 1.0μm/m)) === 2.0μm/m @test @inferred(muladd(2, 1.0, 1μm/m)) === 2.000001 @test @inferred(muladd(2, 1μm/m, 1mm/m)) === 501//500000 - @test_throws DimensionError fma(2m, 1/m, 1m) @test_throws DimensionError fma(2, 1m, 1V) end @@ -884,8 +887,9 @@ end @test @inferred([1m, 2m]' * [3/m, 4/m]) == 11 @test typeof([1m, 2m]' * [3/m, 4/m]) == Int @test typeof([1m, 2V]' * [3/m, 4/V]) == Int - @test @inferred([1V,2V]*[0.1/m, 0.4/m]') == [0.1V/m 0.4V/m; 0.2V/m 0.8V/m] + + # Probably broken as soon as we stopped using custom promote_op methods @test_broken @inferred([1m, 2V]' * [3/m, 4/V]) == [11] @test_broken @inferred([1m, 2V] * [3/m, 4/V]') == [3 4u"m*V^-1"; 6u"V*m^-1" 8] @@ -916,8 +920,8 @@ end @test typeof(5m .* [1m, 2m, 3m]) == Array{typeof(1u"m^2"),1} @test @inferred(eye(2)*V) == [1.0V 0.0V; 0.0V 1.0V] @test @inferred(V*eye(2)) == [1.0V 0.0V; 0.0V 1.0V] - @test @inferred(eye(2).*V) == [1.0V 0.0V; 0.0V 1.0V] - @test @inferred(V.*eye(2)) == [1.0V 0.0V; 0.0V 1.0V] + @test @inferred(eye(2).*V) == [1.0V 0.0V; 0.0V 1.0V] + @test @inferred(V.*eye(2)) == [1.0V 0.0V; 0.0V 1.0V] @test @inferred([1V 2V; 0V 3V].*2) == [2V 4V; 0V 6V] @test @inferred([1V, 2V] .* [true, false]) == [1V, 0V] @test @inferred([1.0m, 2.0m] ./ 3) == [1m/3, 2m/3] @@ -965,7 +969,8 @@ end end @testset ">> Unit stripping" begin @test @inferred(ustrip([1u"m", 2u"m"])) == [1,2] - @test @inferred(ustrip([1,2])) == [1,2] + @test_warn "deprecated" ustrip([1,2]) + @test ustrip.([1,2]) == [1,2] @test typeof(ustrip([1u"m", 2u"m"])) == Array{Int,1} @test typeof(ustrip(Diagonal([1,2]u"m"))) == Diagonal{Int} @test typeof(ustrip(Bidiagonal([1,2,3]u"m", [1,2]u"m", true))) == @@ -1008,6 +1013,188 @@ end @test errorstr(DimensionError(u"m",2)) == "DimensionError: m and 2 are not dimensionally compatible." end +@testset "Logarithmic quantities" begin + @testset "> Explicit construction" begin + @testset ">> Level" begin + # Outer constructor + @test Level{li_dB,1}(2) isa Level{li_dB,1,Int} + @test_throws DimensionError Level{li_dB,1}(2V) + + # Inner constructor + @test Level{li_dB,1,Int}(2) === Level{li_dB,1}(2) + end + + @testset ">> Gain" begin + @test Gain{li_dB}(1) isa Gain{li_dB,Int} + @test_throws MethodError Gain{li_dB}(1V) + @test_throws TypeError Gain{li_dB,typeof(1V)}(1V) + end + + end + + @testset "> Implicit construction" begin + @testset ">> Level" begin + @test 20*dBm == (@dB 100mW/mW) == (@dB 100mW/1mW) + @test 20*dBV == (@dB 10V/V) == (@dB 10V/1V) + end + + @testset ">> Gain" begin + @test_throws ArgumentError @eval @dB 2/1 + @test_throws ArgumentError @eval @dB 10 + @test 20*dB === dB*20 + end + + @testset ">> MixedUnits" begin + @test dBm === MixedUnits{Level{li_dB, 1mW}}() + @test dBm/Hz === MixedUnits{Level{li_dB, 1mW}}(Hz^-1) + end + end + + @testset "> Dimensional analysis" begin + @testset ">> Level" begin + @test dimension(1dBm) === dimension(1mW) + @test dimension(typeof(1dBm)) === dimension(1mW) + @test dimension(1dBV) === dimension(1V) + @test dimension(typeof(1dBV)) === dimension(1V) + @test dimension(1dB) === NoDims + @test dimension(typeof(1dB)) === NoDims + @test dimension(@dB 3V/2.14V) === dimension(1V) + @test dimension(typeof(@dB 3V/2.14V)) === dimension(1V) + end + + @testset ">> Quantity{<:Level}" begin + @test dimension(1dBm/Hz) === dimension(1mW/Hz) + @test dimension(typeof(1dBm/Hz)) === dimension(1mW/Hz) + @test dimension(1dB/Hz) === dimension(Hz^-1) + @test dimension(typeof(1dB/Hz)) === dimension(Hz^-1) + @test dimension((@dB 3V/2.14V)/Hz) === dimension(1V/Hz) + @test dimension(typeof((@dB 3V/2.14V)/Hz)) === dimension(1V/Hz) + end + end + + @testset "> Conversion" begin + @test uconvert(V, (@dB 3V/2.14V)) === 3V + @test uconvert(V, (@dB 3V/1V)) === 3V + @test uconvert(mW/Hz, 0dBm/Hz) == 1mW/Hz + @test uconvert(mW/Hz, (@dB 1mW/mW)/Hz) === 1mW/Hz + + @test uconvert(dB, 1Np) ≈ 8.685889638065037dB + @test convert(typeof(1.0dB), 1Np) ≈ 8.685889638065037dB + @test convert(typeof(1.0dBm), 1W) == 30.0dBm + @test_throws DimensionError convert(typeof(1.0dBm), 1V) + @test convert(typeof(3dB), 3dB) === 3dB + @test convert(typeof(3.0dB), 3dB) === 3.0dB + + @test isapprox(fieldratio(NoUnits, 6.02dB), 2.0, atol=0.001) + @test fieldratio(NoUnits, 1Np) ≈ e + @test fieldratio(Np, e) == 1Np + @test fieldratio(NoUnits, 1) == 1 + @test fieldratio(NoUnits, 20dB) == 10 + @test fieldratio(dB, 10) == 20dB + @test isapprox(powerratio(NoUnits, 3.01dB), 2.0, atol=0.001) + @test powerratio(NoUnits, 1Np) == e^2 + @test powerratio(Np, e^2) == 1Np + @test powerratio(NoUnits, 1) == 1 + @test powerratio(NoUnits, 20dB) == 100 + @test powerratio(dB, 100) == 20dB + + @test linear(@dB(1mW/mW)/Hz) === 1mW/Hz + @test linear(@dB(1.4V/2.8V)/s) === 1.4V/s + end + + @testset "> Equality" begin + @test !(20dBm == 20dB) + end + + @testset "> Addition and subtraction" begin + @testset ">> Level" begin + @test isapprox(10dBm + 10dBm, 13dBm; atol=0.02dBm) + @test !isapprox(10dBm + 10dBm, 13dBm; atol=0.00001dBm) + @test isapprox(13dBm, 20mW; atol = 0.1mW) + @test @dB(10mW/mW) + 1mW === 11mW + @test 1mW + @dB(10mW/mW) === 11mW + @test @dB(10mW/mW) + @dB(90mW/mW) === @dB(100mW/mW) + @test (@dB 10mW/3mW) + (@dB 11mW/2mW) === 21mW + @test (@dB 10mW/3mW) + 2mW === 12mW + @test (@dB 10mW/3mW) + 1W === 101u"kg*m^2/s^3"//100 + @test 20dB + 20dB == 40dB + @test 20dB + 20.2dB == 40.2dB + @test 1Np + 1.5Np == 2.5Np + @test_throws DimensionError (1dBm + 1dBV) + @test_throws DimensionError (1dBm + 1V) + end + + @testset ">> Gain" begin + @test 20dB + 10dB === 30dB + @test 20dB - 10dB === 10dB + @test_throws ErrorException 20dB * 20dB + @test_throws ErrorException 1dB + 1Np + end + + @testset ">> Level, meet Gain" begin + @test 10dBm + 30dB == 40dBm + @test 30dB + 10dBm == 40dBm + @test 10dBm - 30dB == -20dBm + @test isapprox(10dBm - 1Np, 1.314dBm; atol=0.001dBm) + + # cannot subtract levels from gains + @test_throws ErrorException 1Np - 10dBm + @test_throws ErrorException 30dB - 10dBm + end + end + + @testset "> Multiplication and division" begin + @testset ">> Level" begin + @test (0dBm) * 10 == (10dBm) + @test @dB(10V/V)*10 == 100V + @test @dB(10V/V)/20 == 0.5V + @test 10*@dB(10V/V) == 100V + @test 10/@dB(10V/V) == 1V^-1 + @test (0dBm) * (1W) == 1*mW*W + @test 100*((0dBm)/s) == (20dBm)/s + @test isapprox((3.01dBm)*(3.01dBm), 4mW^2, atol=0.01mW^2) + @test typeof((1dBm * big"2").val.val) == BigFloat + @test 10dBm/10Hz == 1mW/Hz + @test 10Hz/10dBm == 1Hz/mW + @test true*3dBm == 3dBm + @test false*3dBm == -Inf*dBm + @test 3dBm*true == 3dBm + @test 3dBm*false == -Inf*dBm + end + + @testset ">> Gain" begin + @test 3dB * 2 == 6dB + @test 3dB * 2.1 ≈ 6.3dB + @test 3dB * false == 0*dB + @test false * 3dB == 0*dB + end + + @testset ">> Quantity, meet Gain" begin + @test 1mW * 20dB == 100mW + @test 20dB * 1mW == 100mW + @test 1V * 20dB == 10V + @test 20dB * 1V == 10V + end + end + + @testset "> Unit stripping" begin + @test ustrip(500.0Np) === 500.0 + @test ustrip(20dB/Hz) === 20 + @test ustrip(20dB) === 20 + @test ustrip(13dBm) ≈ 13 + end + + @testset "> Thanks for signing up for Log Facts!" begin + @test_throws ErrorException 20dB == 100 + @test 20dBm ≈ 100mW + @test 20dBV ≈ 10V + @test 40dBV ≈ 100V + + # Maximum sound pressure level is a full swing of atmospheric pressure + @test isapprox(uconvert(dBSPL, 1u"atm"), 194dBSPL, atol=0.1dBSPL) + end +end + # Test that the @u_str macro will find units in other modules. module ShadowUnits using Unitful @@ -1030,6 +1217,7 @@ let fname = tempname() rm(fname, force=true) end end + @test_warn "ShadowUnits" eval(:(u"m")) # Test to make sure user macros are working properly From 6e3b9fc93996aa509139970fe41668348f17333e Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Thu, 28 Sep 2017 17:24:01 -0700 Subject: [PATCH 02/12] Make multiplying Level by Gain work; automatically build addition and multiplication tables in docs based on latest behavior. --- docs/src/logarithm.md | 496 +++++++++++++++++++++++++++++++++++++++--- src/logarithm.jl | 19 +- 2 files changed, 486 insertions(+), 29 deletions(-) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index ef72f9af..d4621612 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -172,27 +172,304 @@ Mathematical operations are forwarded to the logarithmic part, so that for examp `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 (entries marked by † -indicate prohibited operations): - -| * | 10 | Hz^-1 | dB | dBm | 1/Hz | 1mW | 3dB | 3dBm | -| ------------------- | ---- | ----- | ----- | ------ | ------ | ------- | -------- | --------- | -| **10** | 100 | 10/s | 10dB | 10dBm | 10/s | 10mW | 30dB | 13dBm | -| **Hz^-1** (unit) | | Hz^-2 | † | † | 1/Hz^2 | 1mW/Hz | (3dB)/Hz | (3dBm)/Hz | -| **dB** | | | † | † | † | † | † | † | -| **dBm** | | | | † | † | † | † | † | -| **1/Hz** (quantity) | | | | | 1/Hz^2 | 1mW/Hz | ‡ | ≈ 2mW/Hz | -| **1mW** (quantity) | | | | | | 1mW^2 | ≈2mW | ≈ 2mW^2 | -| **3dB** | | | | | | | † | † | -| **3dBm** | | | | | | | | ≈ 4mW^2 | +The behavior of multiplication is summarized in the following table, with entries marked by +† indicate prohibited operations. This table is populated automatically whenever the docs +are built. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\*10Hz^-1dBdBm1/Hz1mW3dB3dBm
10 +```@eval +using Unitful +Unitful.@_doctables 10*10 +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"Hz^-1" +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"dB" +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"1/Hz" +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"1mW" +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"3dB" +``` + +```@eval +using Unitful +Unitful.@_doctables 10*u"3dBm" +``` +
Hz^-1 + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"Hz^-1" +``` + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"1/Hz" +``` + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"1mW" +``` + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"3dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"Hz^-1"*u"3dBm" +``` +
dB +```@eval +using Unitful +Unitful.@_doctables u"dB"*u"dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dB"*u"dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dB"*u"1/Hz" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dB"*u"1mW" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dB"*u"3dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dB"*u"3dBm" +``` +
dBm +```@eval +using Unitful +Unitful.@_doctables u"dBm"*u"dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dBm"*u"1/Hz" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dBm"*u"1mW" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dBm"*u"3dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"dBm"*u"3dBm" +``` +
1/Hz +```@eval +using Unitful +Unitful.@_doctables u"1/Hz"*u"1/Hz" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1/Hz"*u"1mW" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1/Hz"*u"3dB" +``` +‡ + +```@eval +using Unitful +Unitful.@_doctables u"1/Hz"*u"3dBm" +``` +
1mW +```@eval +using Unitful +Unitful.@_doctables u"1mW"*u"1mW" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1mW"*u"3dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1mW"*u"3dBm" +``` +
3dB +```@eval +using Unitful +Unitful.@_doctables u"3dB"*u"3dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"3dB"*u"3dBm" +``` +
3dBm +```@eval +using Unitful +Unitful.@_doctables u"3dBm"*u"3dBm" +``` +
‡: `1/Hz * 3dB` is technically allowed but dumb things can happen when its unclear if a quantity is a root-power or power quantity: ```jldoctest -julia> 1/u"Hz" * 20u"dB" +julia> u"1/Hz" * u"3dB" WARNING: result may be incorrect. Define `Unitful.isrootpower(::Type{<:Unitful.LogInfo}, ::typeof(𝐓))` to fix. -100.0 Hz^-1 +1.9952623149688795 Hz^-1 ``` On the other hand, if it can be determined that a power quantity or root-power quantity @@ -258,17 +535,184 @@ julia> 20u"dBm" + @dB 1u"W"/u"W" ``` i.e. `1.1 W`. -Rules for addition are summarized in the following table (entries marked by † -indicate prohibited operations): - -| + | 100 | 20dB | 1Np | 10.0dBm | 10.0dBV | 1mW | -| ----------- | ------- | ------- | ------- | -------- | -------- | -------- | -| **100** | 200 | † | † | † | † | † | -| **20dB** | | 40dB | † | 30.0dBm | 30.0dBV | † | -| **1Np** | | | 2Np | ≈18.7dBm | ≈18.7dBV | ≈7.39mW | -| **10.0dBm** | | | | ≈13dBm | † | 11.0mW | -| **10.0dBV** | | | | | ≈16.0dBV | † | -| **1mW** | | | | | | 2mW | +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. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+10020dB1Np10.0dBm10.0dBv1mW
100 +```@eval +using Unitful +Unitful.@_doctables 100+100 +``` + +```@eval +using Unitful +Unitful.@_doctables 100+u"20dB" +``` + +```@eval +using Unitful +Unitful.@_doctables 100+u"1Np" +``` + +```@eval +using Unitful +Unitful.@_doctables 100+u"10.0dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables 100+u"10.0dBV" +``` + +```@eval +using Unitful +Unitful.@_doctables 100+u"1mW" +``` +
20dB +```@eval +using Unitful +Unitful.@_doctables u"20dB"+u"20dB" +``` + +```@eval +using Unitful +Unitful.@_doctables u"20dB"+u"1Np" +``` + +```@eval +using Unitful +Unitful.@_doctables u"20dB"+u"10.0dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables u"20dB"+u"10.0dBV" +``` + +```@eval +using Unitful +Unitful.@_doctables u"20dB"+u"1mW" +``` +
1Np +```@eval +using Unitful +Unitful.@_doctables u"1Np"+u"1Np" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1Np"+u"10.0dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1Np"+u"10.0dBV" +``` + +```@eval +using Unitful +Unitful.@_doctables u"1Np"+u"1mW" +``` +
10.0dBm +```@eval +using Unitful +Unitful.@_doctables u"10.0dBm"+u"10.0dBm" +``` + +```@eval +using Unitful +Unitful.@_doctables u"10.0dBm"+u"10.0dBV" +``` + +```@eval +using Unitful +Unitful.@_doctables u"10.0dBm"+u"1mW" +``` +
10.0dBm +```@eval +using Unitful +Unitful.@_doctables u"10.0dBV"+u"10.0dBV" +``` + +```@eval +using Unitful +Unitful.@_doctables u"10.0dBV"+u"1mW" +``` +
10.0dBm +```@eval +using Unitful +Unitful.@_doctables u"1mW"+u"1mW" +``` +
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 diff --git a/src/logarithm.jl b/src/logarithm.jl index 0c8ab061..25746ba9 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -185,13 +185,13 @@ Base. *(x::Level{L,S}, y::Number) where {L,S} = Level{L,S}(x.val * y) Base. *(x::Level{L,S}, y::Bool) where {L,S} = Level{L,S}(x.val * y) # for method ambiguity Base. *(x::Level{L,S}, y::Quantity) where {L,S} = *(x.val, y) Base. *(x::Level{L,S}, y::Level) where {L,S} = *(x.val, y.val) -Base. *(x::Level{L,S}, y::Gain) where {L,S} = error("logarithmic gains add, not multiply.") +Base. *(x::Level{L,S}, y::Gain) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(x)+y.val)) Base. *(x::Number, y::Gain) = *(y,x) Base. *(x::Bool, y::Gain) = *(y,x) # for method ambiguity Base. *(x::Gain{L}, y::Number) where {L} = Gain{L}(x.val * y) Base. *(x::Gain{L}, y::Bool) where {L} = Gain{L}(x.val * y) # for method ambiguity -Base. *(x::Gain{L}, y::Level) where {L} = error("logarithmic gains add, not multiply.") +Base. *(x::Gain{L}, y::Level) where {L} = Level{L,S}(fromlog(L, S, ustrip(x)+y.val)) Base. *(x::Gain{L}, y::Gain) where {L} = error("logarithmic gains add, not multiply.") Base. *(x::Quantity, y::Gain{L}) where {L} = @@ -203,7 +203,7 @@ Base. /(x::Number, y::Level) = x / y.val Base. /(x::Level{L,S}, y::Number) where {L,S} = Level{L,S}(x.val / y) Base. /(x::Level{L,S}, y::Quantity) where {L,S} = x.val / y Base. /(x::Level{L,S}, y::Level) where {L,S} = x.val / y.val -Base. /(x::Level{L,S}, y::Gain) where {L,S} = error("logarithmic gains subtract, not divide.") +Base. /(x::Level{L,S}, y::Gain) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(x) - y.val)) Base. /(x::Quantity, y::Gain) = error("logarithmic gains subtract, not divide.") Base. /(x::Quantity, y::Level) = x / y.val @@ -395,3 +395,16 @@ Base.isapprox(x::Gain, y::Gain; kwargs...) = isapprox(promote(x,y)...; kwargs... Base.isapprox(x::T, y::T; kwargs...) where {T <: Gain} = _isapprox(x, y; kwargs...) _isapprox(x::Gain{L,T}, y::Gain{L,T}; atol = Gain{L}(oneunit(T)), kwargs...) where {L,T} = isapprox(ustrip(x), ustrip(y); atol = ustrip(convert(Gain{L,T}, atol)), kwargs...) + +struct InvalidOp end +Base.show(io::IO, ::InvalidOp) = print(io, "†") + +macro _doctables(x) + return esc(quote + try + $x + catch + Unitful.InvalidOp() + end + end) +end From 889c0735d6fb32427d9e858631c30a291578726b Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Fri, 29 Sep 2017 09:09:15 -0700 Subject: [PATCH 03/12] Update docs typo. [skip ci] --- docs/src/logarithm.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index d4621612..b8d8ed77 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -679,7 +679,7 @@ Unitful.@_doctables u"10.0dBm"+u"1mW" -10.0dBm +10.0dBV @@ -698,7 +698,7 @@ Unitful.@_doctables u"10.0dBV"+u"1mW" -10.0dBm +1mW From 007362d8992dab25edd7b59f89c78e58b2c0cefd Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Fri, 20 Oct 2017 11:50:54 -0700 Subject: [PATCH 04/12] Support user creation of logarithmic scales and units. --- docs/src/logarithm.md | 6 +++ docs/src/newunits.md | 5 +-- src/Unitful.jl | 14 +++---- src/logarithm.jl | 71 +----------------------------------- src/pkgdefaults.jl | 24 ++++++++++++ src/user.jl | 85 +++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 19 +++++----- 7 files changed, 134 insertions(+), 90 deletions(-) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index b8d8ed77..27e4f331 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -801,6 +801,12 @@ 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 + +```@docs + Unitful.@logscale +``` + ## API ```@docs diff --git a/docs/src/newunits.md b/docs/src/newunits.md index 421b0b89..41cc123e 100644 --- a/docs/src/newunits.md +++ b/docs/src/newunits.md @@ -7,9 +7,8 @@ end # Defining new units !!! note - Logarithmic units cannot be defined by the user and should not be used in the `@refunit` - or `@unit` macros described below. This limitation will likely be lifted eventually, but - not until the interface for logarithmic units settles down. + Logarithmic units should not be used in the `@refunit` or `@unit` macros described below. + See the section on logarithmic scales for customization help. The package automatically generates a useful set of units and dimensions in the `Unitful` module in `src/pkgdefaults.jl`. diff --git a/src/Unitful.jl b/src/Unitful.jl index 1c208fe5..891d1716 100644 --- a/src/Unitful.jl +++ b/src/Unitful.jl @@ -11,8 +11,8 @@ import Base: sin, cos, tan, cot, sec, csc, atan2, cis, vecnorm import Base: mod, rem, div, fld, cld, trunc, round, sign, signbit import Base: isless, isapprox, isinteger, isreal, isinf, isfinite, isnan import Base: copysign, flipsign -import Base: prevfloat, nextfloat, maxintfloat, rat, step #, linspace -import Base: length, float, start, done, next, last, one, zero, colon#, range +import Base: prevfloat, nextfloat, maxintfloat, rat, step +import Base: length, float, start, done, next, last, one, zero, colon import Base: getindex, eltype, step, last, first, frexp import Base: Integer, Rational, typemin, typemax import Base: steprange_last, unsigned @@ -21,11 +21,11 @@ import Base.LinAlg: istril, istriu export logunit, unit, dimension, uconvert, ustrip, upreferred export @dimension, @derived_dimension, @refunit, @unit, @u_str -export Quantity -export DimensionlessQuantity -export NoUnits, NoDims +export Quantity, DimensionlessQuantity, NoUnits, NoDims -export powerratio, fieldratio, rootpowerratio, reflevel, linear, @dB, @Np +export powerratio, fieldratio, rootpowerratio, reflevel, linear +export @logscale, @logunit, @dB, @B, @cNp, @Np +export Level, Gain const unitmodules = Vector{Module}() const basefactors = Dict{Symbol,Tuple{Float64,Rational{Int}}}() @@ -43,8 +43,8 @@ include("promotion.jl") include("conversion.jl") include("range.jl") include("fastmath.jl") -include("pkgdefaults.jl") include("logarithm.jl") +include("pkgdefaults.jl") function __init__() # @u_str should be aware of units defined in module Unitful diff --git a/src/logarithm.jl b/src/logarithm.jl index 25746ba9..2e24e646 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -1,6 +1,3 @@ - -abbr(::LogInfo{:Decibel}) = "dB" -abbr(::LogInfo{:Neper}) = "Np" base(::LogInfo{N,B}) where {N,B} = B prefactor(::LogInfo{N,B,P}) where {N,B,P} = P @@ -10,13 +7,7 @@ dimension(x::Type{T}) where {L,S,T<:Level{L,S}} = dimension(S) logunit(x::Level{L,S}) where {L,S} = MixedUnits{Level{L,S}}() logunit(x::Type{T}) where {L,S,T<:Level{L,S}} = MixedUnits{Level{L,S}}() -function abbr(x::Level{L,S}) where {L,S} - if dimension(S) == NoDims - return abbr(L()) - else - return join([abbr(L()), " (", reflevel(x), ")"]) - end -end +abbr(x::Level{L,S}) where {L,S} = join([abbr(L()), " (", reflevel(x), ")"]) function uconvert(a::Units, x::Level) dimension(a) != dimension(x) && throw(DimensionError(a,x)) @@ -119,37 +110,11 @@ function uconvert(a::MixedUnits{<:Gain}, x::Number) error("perhaps you meant `($x)*($ustr)`?") end -for (_short,_long,_base,_pre) in ((:dB, :Decibel, 10, 10), - (:Np, :Neper, e, 1//2)) - li = Symbol("li_",_short) - @eval begin - const $li = LogInfo{$(QuoteNode(_long)),$_base,$_pre} - const $_short = MixedUnits{Gain{$li}}() - end -end - -abbr(::Level{li_dB, 1mW}) = "dBm" -abbr(::Level{li_dB, 1V}) = "dBV" -abbr(::Level{li_dB, sqrt(0.6)V}) = "dBu" -abbr(::Level{li_dB, 1μV}) = "dBμV" -abbr(::Level{li_dB, 20μPa}) = "dBSPL" - -const dBV = MixedUnits{Level{li_dB, 1V}}() -const dBu = MixedUnits{Level{li_dB, sqrt(0.6)V}}() -const dBμV = MixedUnits{Level{li_dB, 1μV}}() -const dBµV = dBμV # different character encoding of μ -const dBm = MixedUnits{Level{li_dB, 1mW}}() -const dBSPL = MixedUnits{Level{li_dB, 20μPa}}() - ustrip(x::Level{L,S}) where {L<:LogInfo, S} = tolog(L,S,x.val/reflevel(x)) ustrip(x::Gain) = x.val # TODO: some more dimensions? isrootpower(x,y) = isrootpower_warn(x,y) -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(W))) = false -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(V))) = true -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(A))) = true -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(Pa))) = true # Default to power or root-power as appropriate for the given logarithmic unit function isrootpower_warn(x,y) @@ -161,8 +126,6 @@ function isrootpower_warn(x,y) end isrootpower(t::Type{<:LogInfo}, ::typeof(NoDims)) = isrootpower(t) -isrootpower(::Type{li_dB}) = false -isrootpower(::Type{li_Np}) = true ==(x::Gain, y::Level) = ==(y,x) ==(x::Level, y::Gain) = false @@ -259,38 +222,6 @@ function Base.show(io::IO, x::Quantity{<:Union{Level,Gain},D,U}) where {D,U} nothing end -for li in (:B, :dB, :Np) - @eval begin - $(Expr(:export, Symbol("@",li))) - - macro ($li)(r::Union{Real,Symbol}) - throw(ArgumentError(join(["usage: `@", $(String(li)), " (a)/(b)`"]))) - end - - macro ($li)(expr::Expr) - s = $(Symbol("_", li)) - expr.args[1] != :/ && - throw(ArgumentError(join(["usage: `@", $(String(li)), " (a)/(b)`"]))) - length(expr.args) != 3 && - throw(ArgumentError(join(["usage: `@", $(String(li)), " (a)/(b)`"]))) - esc(quote - ($s)($(expr.args[2]), $(expr.args[3])) - end) - end - - function $(Symbol("_", li))(num::Number, den::Number) - dimension(num) != dimension(den) && throw(DimensionError(num,den)) - dimension(num) == NoDims && - throw(ArgumentError("cannot use this macro with dimensionless numbers.")) - return Level{$(Symbol("li_", li)), den}(num) - end - - function $(Symbol("_", li))(num::Number, den::Units) - $(Symbol("_", li))(num, 1*den) - end - end -end - """ powerratio(::Type{T}, x::Real) where {T<:Number} = convert(T, x) Returns the gain as a ratio of power quantities. diff --git a/src/pkgdefaults.jl b/src/pkgdefaults.jl index 165b7e6b..d1183446 100644 --- a/src/pkgdefaults.jl +++ b/src/pkgdefaults.jl @@ -163,6 +163,30 @@ Unitful.offsettemp(::Unitful.Unit{:Fahrenheit}) = 45967//100 # Force @unit lbf "lbf" PoundsForce 1lb*ge false +######### +# Logarithmic scales and units + +@logscale dB "dB" Decibel 10 10 +@logscale B "B" Bel 10 1 +@logscale Np "Np" Neper e 1//2 +@logscale cNp "cNp" Centineper e 50 + +@logunit dBm "dBm" Decibel 1mW +@logunit dBV "dBV" Decibel 1V +@logunit dBu "dBu" Decibel sqrt(0.6)V +@logunit dBμV "dBμV" Decibel 1μV +@logunit dBSPL "dBSPL" Decibel 20μPa +@logunit dBFS "dBFS" Decibel 1 + +const dBµV = dBμV # different character encoding of μ + +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(W))) = false +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(V))) = true +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(A))) = true +isrootpower(::Type{<:LogInfo}, ::typeof(dimension(Pa))) = true +isrootpower(::Type{Decibel}) = false +isrootpower(::Type{Neper}) = true + ######### # `using Unitful.DefaultSymbols` will bring the following into the calling namespace: diff --git a/src/user.jl b/src/user.jl index 55258d7a..45e59441 100644 --- a/src/user.jl +++ b/src/user.jl @@ -293,6 +293,91 @@ base SI units. @inline upreferred(::ContextUnits{N,D,P}) where {N,D,P} = ContextUnits(P(),P()) @inline upreferred(x::FixedUnits) = x +""" + @logscale(symb,abbr,name,base,prefactor) +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 + +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`. + +Note that `prefactor` is defined with respect to taking ratios of power quantities. As +usual, just divide by two if you want to refer to root-power / field quantities instead. + +Examples: +```jldoctest +julia> @logscale dΠ "dΠ" Decipies π 10 +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 V) +``` +""" +macro logscale(symb,abbr,name,base,prefactor) + # name is a symbol + # abbr is a string + li = Symbol("li_", name) + + quote + Unitful.abbr(::Unitful.LogInfo{$(QuoteNode(name))}) = $abbr + const $(esc(name)) = Unitful.LogInfo{$(QuoteNode(name)), $base, $prefactor} + const $(esc(symb)) = Unitful.MixedUnits{Unitful.Gain{$(esc(name))}}() + + macro $(esc(symb))(::Union{Real,Symbol}) + throw(ArgumentError(join(["usage: `@", $(String(symb)), " (a)/(b)`"]))) + end + + macro $(esc(symb))(expr::Expr) + # s = Symbol("_", $(esc(symb))) + expr.args[1] != :/ && + throw(ArgumentError(join(["usage: `@", $(String(symb)), " (a)/(b)`"]))) + length(expr.args) != 3 && + throw(ArgumentError(join(["usage: `@", $(String(symb)), " (a)/(b)`"]))) + return Expr(:call, $(esc(symb)), expr.args[2], expr.args[3]) + end + + function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Number) + dimension(num) != dimension(den) && throw(DimensionError(num,den)) + # dimension(num) == NoDims && + # throw(ArgumentError("cannot use this macro with dimensionless numbers.")) + return Level{$(esc(name)), den}(num) + end + + function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Units) + $(esc(symb))(num, 1*den) + end + + $(esc(symb)) + end +end + +""" + @logunit(symb, abbr, logscale, reflevel) +Defines a logarithmic unit. For examples see `src/pkgdefaults.jl`. +""" +macro logunit(symb, abbr, logscale, reflevel) + quote + Unitful.abbr(::Unitful.Level{$(esc(logscale)), $(esc(reflevel))}) = $abbr + const $(esc(symb)) = + Unitful.MixedUnits{Unitful.Level{$(esc(logscale)), $(esc(reflevel))}}() + end +end + """ @u_str(unit) String macro to easily recall units, dimensions, or quantities defined in diff --git a/test/runtests.jl b/test/runtests.jl index 038469eb..b836172c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,7 +5,7 @@ using Base.Test import Unitful: DimensionError -import Unitful: LogScaled, LogInfo, Level, Gain, MixedUnits, li_dB +import Unitful: LogScaled, LogInfo, Level, Gain, MixedUnits, Decibel import Unitful: FreeUnits, ContextUnits, FixedUnits @@ -1017,17 +1017,17 @@ end @testset "> Explicit construction" begin @testset ">> Level" begin # Outer constructor - @test Level{li_dB,1}(2) isa Level{li_dB,1,Int} - @test_throws DimensionError Level{li_dB,1}(2V) + @test Level{Decibel,1}(2) isa Level{Decibel,1,Int} + @test_throws DimensionError Level{Decibel,1}(2V) # Inner constructor - @test Level{li_dB,1,Int}(2) === Level{li_dB,1}(2) + @test Level{Decibel,1,Int}(2) === Level{Decibel,1}(2) end @testset ">> Gain" begin - @test Gain{li_dB}(1) isa Gain{li_dB,Int} - @test_throws MethodError Gain{li_dB}(1V) - @test_throws TypeError Gain{li_dB,typeof(1V)}(1V) + @test Gain{Decibel}(1) isa Gain{Decibel,Int} + @test_throws MethodError Gain{Decibel}(1V) + @test_throws TypeError Gain{Decibel,typeof(1V)}(1V) end end @@ -1039,14 +1039,13 @@ end end @testset ">> Gain" begin - @test_throws ArgumentError @eval @dB 2/1 @test_throws ArgumentError @eval @dB 10 @test 20*dB === dB*20 end @testset ">> MixedUnits" begin - @test dBm === MixedUnits{Level{li_dB, 1mW}}() - @test dBm/Hz === MixedUnits{Level{li_dB, 1mW}}(Hz^-1) + @test dBm === MixedUnits{Level{Decibel, 1mW}}() + @test dBm/Hz === MixedUnits{Level{Decibel, 1mW}}(Hz^-1) end end From c31e249a6b5c1634078fb2733716a73733e2b31b Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Fri, 20 Oct 2017 14:42:07 -0700 Subject: [PATCH 05/12] Some more tweaks. --- docs/src/logarithm.md | 23 +++++++++++++------ src/logarithm.jl | 51 ++++++++++++++++++++----------------------- src/pkgdefaults.jl | 20 ++++++++--------- src/types.jl | 1 + src/user.jl | 30 +++++++++++++++---------- test/runtests.jl | 2 +- 6 files changed, 69 insertions(+), 58 deletions(-) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index 27e4f331..bb03df64 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -25,10 +25,10 @@ julia> u"dB"*3 === 3u"dB" true ``` -Currently implemented are `dB`, `dBm`, `dBV`, `dBu`, `dBμV`, `dBSPL`, `Np`. +Currently implemented are `dB`, `B`, `dBm`, `dBV`, `dBu`, `dBμV`, `dBSPL`, `cNp`, `Np`. -One can also construct logarithmic quantities using the `@dB` or `@Np` macros to use -an arbitrary reference level: +One can also construct logarithmic quantities using the `@dB`, `@B`, `@cNp`, `@Np` macros to +use an arbitrary reference level: ```jldoctest julia> using Unitful: mW, V @@ -46,6 +46,16 @@ julia> @Np e*V/V # e = 2.71828... 1.0 Np (1 V) ``` +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: + +```jldoctest +julia> using Unitful: dB, mW, V + +julia> u"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). @@ -463,13 +473,12 @@ Unitful.@_doctables u"3dBm"*u"3dBm" -‡: `1/Hz * 3dB` is technically allowed but dumb things can happen when its unclear if a quantity -is a root-power or power quantity: +‡: `1/Hz * 3dB` could be allowed, technically, but we throw an error its unclear if a +quantity is a root-power or power quantity: ```jldoctest julia> u"1/Hz" * u"3dB" -WARNING: result may be incorrect. Define `Unitful.isrootpower(::Type{<:Unitful.LogInfo}, ::typeof(𝐓))` to fix. -1.9952623149688795 Hz^-1 +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 diff --git a/src/logarithm.jl b/src/logarithm.jl index 2e24e646..3d8af319 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -18,9 +18,9 @@ Base.convert(::Type{LogScaled{L1}}, x::Level{L2,S}) where {L1,L2,S} = Level{L1,S Base.convert(T::Type{<:Level}, x::Level) = T(x.val) """ - reflevel(x::Level{L,S}) where {L,S} = S - reflevel(::Type{Level{L,S}}) where {L,S} = S - reflevel(::Type{Level{L,S,T}}) where {L,S,T} = S + reflevel(x::Level{L,S}) + reflevel(::Type{Level{L,S}}) + reflevel(::Type{Level{L,S,T}}) Returns the reference level, e.g. ```jldoctest @@ -51,17 +51,19 @@ Base.convert(T::Type{Gain{L1,T1}}, x::Gain{L2,T2}) where {L1,L2,T1,T2} = T(_gcon Base.convert(::Type{LogScaled{L1}}, x::Gain{L2}) where {L1,L2} = Gain{L1}(_gconv(L1,L2,x)) function _gconv(L1,L2,x) if isrootpower(L1) == isrootpower(L2) - gain = tolog(L1,1,fromlog(L2,1,x.val)) + gain = tolog(L1,fromlog(L2,x.val)) elseif isrootpower(L1) && !isrootpower(L2) - gain = tolog(L1,1,fromlog(L2,1,0.5*x.val)) + gain = tolog(L1,fromlog(L2,0.5*x.val)) else - gain = tolog(L1,1,fromlog(L2,1,2*x.val)) + gain = tolog(L1,fromlog(L2,2*x.val)) end return gain end -tolog(L,S,x) = (1+isrootpower(L,dimension(S))) * prefactor(L()) * (logfn(L()))(x) -fromlog(L,S,x) = S * expfn(L())( x / ((1+isrootpower(L,dimension(S)))*prefactor(L())) ) +tolog(L,S,x) = (1+isrootpower(L,S)) * prefactor(L()) * (logfn(L()))(x) +tolog(L,x) = (1+isrootpower(L)) * prefactor(L()) * (logfn(L()))(x) +fromlog(L,S,x) = S * expfn(L())( x / ((1+isrootpower(L,S))*prefactor(L())) ) +fromlog(L,x) = expfn(L())( x / ((1+isrootpower(L))*prefactor(L())) ) function Base.show(io::IO, x::MixedUnits{T,U}) where {T,U} print(io, abbr(x)) @@ -113,19 +115,9 @@ end ustrip(x::Level{L,S}) where {L<:LogInfo, S} = tolog(L,S,x.val/reflevel(x)) ustrip(x::Gain) = x.val -# TODO: some more dimensions? -isrootpower(x,y) = isrootpower_warn(x,y) - -# Default to power or root-power as appropriate for the given logarithmic unit -function isrootpower_warn(x,y) - irp = isrootpower(x) - str = ifelse(irp, "root-power", "power") - warn("result may be incorrect. Define ", - "`Unitful.isrootpower(::Type{<:Unitful.LogInfo}, ::typeof($y))` to fix.") - return irp -end - -isrootpower(t::Type{<:LogInfo}, ::typeof(NoDims)) = isrootpower(t) +isrootpower(T::Type{<:LogInfo}, y) = isrootpower_dim(T, dimension(y)) +isrootpower_dim(::Type{<:LogInfo}, y) = + error("undefined behavior. Please file an issue with the code needed to reproduce.") ==(x::Gain, y::Level) = ==(y,x) ==(x::Level, y::Gain) = false @@ -155,10 +147,11 @@ Base. *(x::Bool, y::Gain) = *(y,x) # for met Base. *(x::Gain{L}, y::Number) where {L} = Gain{L}(x.val * y) Base. *(x::Gain{L}, y::Bool) where {L} = Gain{L}(x.val * y) # for method ambiguity Base. *(x::Gain{L}, y::Level) where {L} = Level{L,S}(fromlog(L, S, ustrip(x)+y.val)) -Base. *(x::Gain{L}, y::Gain) where {L} = error("logarithmic gains add, not multiply.") +Base. *(x::Gain{L}, y::Gain) where {L} = *(promote(x,y)...) +Base. *(x::Gain{L}, y::Gain{L}) where {L} = Gain{L}(x.val + y.val) # contentious? Base. *(x::Quantity, y::Gain{L}) where {L} = - isrootpower(L, dimension(x)) ? rootpowerratio(y) * x : powerratio(y) * x + isrootpower(L, x) ? rootpowerratio(y) * x : powerratio(y) * x Base. *(x::Gain, y::Quantity) = *(y,x) # Division @@ -167,6 +160,10 @@ Base. /(x::Level{L,S}, y::Number) where {L,S} = Level{L,S}(x.val / y) Base. /(x::Level{L,S}, y::Quantity) where {L,S} = x.val / y Base. /(x::Level{L,S}, y::Level) where {L,S} = x.val / y.val Base. /(x::Level{L,S}, y::Gain) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(x) - y.val)) + +Base. /(x::Gain{L}, y::Gain) where {L} = /(promote(x,y)...) +Base. /(x::Gain{L}, y::Gain{L}) where {L} = Gain{L}(x.val - y.val) + Base. /(x::Quantity, y::Gain) = error("logarithmic gains subtract, not divide.") Base. /(x::Quantity, y::Level) = x / y.val @@ -234,11 +231,11 @@ exponential attenuation. function powerratio end powerratio(x) = powerratio(NoUnits, x) powerratio(::Units{()}, x::Gain{L}) where {L} = - fromlog(L, 1, ifelse(isrootpower(L), 2, 1)*x.val) + fromlog(L, ifelse(isrootpower(L), 2, 1)*x.val) powerratio(::Units{()}, x::Real) = x powerratio(u::MixedUnits{<:Gain}, x::Gain) = uconvert(u, x) powerratio(u::T, x::Real) where {L, T <: MixedUnits{Gain{L}, <:Units{()}}} = - ifelse(isrootpower(L), 0.5, 1) * tolog(L, 1, x) * u + ifelse(isrootpower(L), 0.5, 1) * tolog(L, x) * u """ rootpowerratio(x::Gain) @@ -262,11 +259,11 @@ exponential attenuation. function rootpowerratio end rootpowerratio(x) = rootpowerratio(NoUnits, x) rootpowerratio(::Units{()}, x::Gain{L}) where {L} = - fromlog(L, 1, ifelse(isrootpower(L), 1.0, 0.5)*x.val) + fromlog(L, ifelse(isrootpower(L), 1.0, 0.5)*x.val) rootpowerratio(::Units{()}, x::Real) = x rootpowerratio(u::MixedUnits{<:Gain}, x::Gain) = uconvert(u, x) rootpowerratio(u::T, x::Real) where {L, T <: MixedUnits{Gain{L}, <:Units{()}}} = - ifelse(isrootpower(L), 1, 2) * tolog(L, 1, x) * u + ifelse(isrootpower(L), 1, 2) * tolog(L, x) * u fieldratio = rootpowerratio diff --git a/src/pkgdefaults.jl b/src/pkgdefaults.jl index d1183446..63caef57 100644 --- a/src/pkgdefaults.jl +++ b/src/pkgdefaults.jl @@ -166,26 +166,24 @@ Unitful.offsettemp(::Unitful.Unit{:Fahrenheit}) = 45967//100 ######### # Logarithmic scales and units -@logscale dB "dB" Decibel 10 10 -@logscale B "B" Bel 10 1 -@logscale Np "Np" Neper e 1//2 -@logscale cNp "cNp" Centineper e 50 +@logscale dB "dB" Decibel 10 10 false +@logscale B "B" Bel 10 1 false +@logscale Np "Np" Neper e 1//2 true +@logscale cNp "cNp" Centineper e 50 true @logunit dBm "dBm" Decibel 1mW @logunit dBV "dBV" Decibel 1V @logunit dBu "dBu" Decibel sqrt(0.6)V @logunit dBμV "dBμV" Decibel 1μV @logunit dBSPL "dBSPL" Decibel 20μPa -@logunit dBFS "dBFS" Decibel 1 const dBµV = dBμV # different character encoding of μ -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(W))) = false -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(V))) = true -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(A))) = true -isrootpower(::Type{<:LogInfo}, ::typeof(dimension(Pa))) = true -isrootpower(::Type{Decibel}) = false -isrootpower(::Type{Neper}) = true +# TODO: some more dimensions? +isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(W))) = false +isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(V))) = true +isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(A))) = true +isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(Pa))) = true ######### diff --git a/src/types.jl b/src/types.jl index b71ac87f..beeb601b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -188,3 +188,4 @@ struct MixedUnits{T<:LogScaled, U<:Units} end MixedUnits{T}() where {T} = MixedUnits{T, typeof(NoUnits)}(NoUnits) MixedUnits{T}(u::Units) where {T} = MixedUnits{T,typeof(u)}(u) +(y::MixedUnits)(x::Number) = uconvert(y,x) diff --git a/src/user.jl b/src/user.jl index 45e59441..74335be7 100644 --- a/src/user.jl +++ b/src/user.jl @@ -294,7 +294,7 @@ base SI units. @inline upreferred(x::FixedUnits) = x """ - @logscale(symb,abbr,name,base,prefactor) + @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 @@ -307,12 +307,19 @@ This macro also defines another macro available as `@symb`. For example, `@dB` i of decibels. This can be used to construct `Level` objects at parse time. Usage is like `@dB 3V/1V`. -Note that `prefactor` is defined with respect to taking ratios of power quantities. As -usual, just divide by two if you want to refer to root-power / field quantities instead. +`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: ```jldoctest -julia> @logscale dΠ "dΠ" Decipies π 10 +julia> using Unitful: V, W + +julia> @logscale dΠ "dΠ" Decipies π 10 false dΠ julia> @dΠ π*V/1V @@ -325,17 +332,16 @@ julia> @dΠ π^2*V/1V 40.0 dΠ (1 V) julia> @dΠ π*W/1W -10.0 dΠ (1 V) +10.0 dΠ (1 W) ``` """ -macro logscale(symb,abbr,name,base,prefactor) - # name is a symbol - # abbr is a string - li = Symbol("li_", name) - +macro logscale(symb,abbr,name,base,prefactor,irp) quote Unitful.abbr(::Unitful.LogInfo{$(QuoteNode(name))}) = $abbr + const $(esc(name)) = Unitful.LogInfo{$(QuoteNode(name)), $base, $prefactor} + Unitful.isrootpower(::Type{$(esc(name))}) = $irp + const $(esc(symb)) = Unitful.MixedUnits{Unitful.Gain{$(esc(name))}}() macro $(esc(symb))(::Union{Real,Symbol}) @@ -353,8 +359,8 @@ macro logscale(symb,abbr,name,base,prefactor) function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Number) dimension(num) != dimension(den) && throw(DimensionError(num,den)) - # dimension(num) == NoDims && - # throw(ArgumentError("cannot use this macro with dimensionless numbers.")) + dimension(num) == NoDims && + throw(ArgumentError("cannot use with dimensionless numbers.")) return Level{$(esc(name)), den}(num) end diff --git a/test/runtests.jl b/test/runtests.jl index b836172c..3d956e27 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1125,8 +1125,8 @@ end @testset ">> Gain" begin @test 20dB + 10dB === 30dB + @test 20dB * 20dB === 40dB # support both +*, for sake of generic programming... @test 20dB - 10dB === 10dB - @test_throws ErrorException 20dB * 20dB @test_throws ErrorException 1dB + 1Np end From 1c389d2da963dc72741b31c32b6ead1e8b8b7363 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Sat, 21 Oct 2017 16:45:46 -0700 Subject: [PATCH 06/12] Add missing method. --- src/logarithm.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/logarithm.jl b/src/logarithm.jl index 3d8af319..44693e73 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -283,6 +283,7 @@ is for two reasons: linear(x::Quantity{<:Level,D,U}) where {D,U} = (x.val.val)*U() linear(x::Quantity{<:Gain}) = error("use powerratio or rootpowerratio instead.") linear(x::Level) = x.val +linear(x::Gain) = error("use powerratio or rootpowerratio instead.") linear(x::Number) = x """ @@ -324,9 +325,9 @@ Base.isapprox(x::T, y::T; kwargs...) where {T <: Gain} = _isapprox(x, y; kwargs. _isapprox(x::Gain{L,T}, y::Gain{L,T}; atol = Gain{L}(oneunit(T)), kwargs...) where {L,T} = isapprox(ustrip(x), ustrip(y); atol = ustrip(convert(Gain{L,T}, atol)), kwargs...) +# For documentation generation... struct InvalidOp end Base.show(io::IO, ::InvalidOp) = print(io, "†") - macro _doctables(x) return esc(quote try From 897bf63e124066a247a7f444f8572b8259759641 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Sat, 21 Oct 2017 22:03:42 -0700 Subject: [PATCH 07/12] Add support for dimensionless Levels (e.g. dBFS). Needs docs and tests. --- src/logarithm.jl | 87 +++++++++++++++++++++++++++++++--------------- src/pkgdefaults.jl | 1 + src/user.jl | 26 ++++++++++++-- 3 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/logarithm.jl b/src/logarithm.jl index 44693e73..c1ed9446 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -1,8 +1,18 @@ +struct IsRootPowerRatio{S,T} + val::T +end +IsRootPowerRatio{S}(x) where {S} = IsRootPowerRatio{S, typeof(x)}(x) +const PowerRatio{T} = IsRootPowerRatio{false,T} +const RootPowerRatio{T} = IsRootPowerRatio{true,T} +dimension(x::IsRootPowerRatio{S,T}) where {S,T} = dimension(T) +unwrap(x::IsRootPowerRatio) = x.val +unwrap(x) = x + base(::LogInfo{N,B}) where {N,B} = B prefactor(::LogInfo{N,B,P}) where {N,B,P} = P dimension(x::Level) = dimension(reflevel(x)) -dimension(x::Type{T}) where {L,S,T<:Level{L,S}} = dimension(S) +dimension(x::Type{T}) where {T<:Level} = dimension(reflevel(T)) logunit(x::Level{L,S}) where {L,S} = MixedUnits{Level{L,S}}() logunit(x::Type{T}) where {L,S,T<:Level{L,S}} = MixedUnits{Level{L,S}}() @@ -16,6 +26,11 @@ end uconvert(a::Units, x::Quantity{<:Level}) = uconvert(a, linear(x)) Base.convert(::Type{LogScaled{L1}}, x::Level{L2,S}) where {L1,L2,S} = Level{L1,S}(x.val) Base.convert(T::Type{<:Level}, x::Level) = T(x.val) +Base.convert(::Type{Quantity{T,D,U}}, x::Level) where {T,D,U} = + convert(Quantity{T,D,U}, x.val) +Base.convert(::Type{Quantity{T}}, x::Level) where {T<:Number} = convert(Quantity{T}, x.val) +Base.convert(::Type{T}, x::Quantity) where {L,S,T<:Level{L,S}} = T(x) +Base.convert(::Type{T}, x::Level) where {T<:Real} = T(x.val) """ reflevel(x::Level{L,S}) @@ -28,9 +43,10 @@ julia> reflevel(3u"dBm") 1 mW ``` """ -reflevel(x::Level{L,S}) where {L,S} = S -reflevel(::Type{Level{L,S}}) where {L,S} = S -reflevel(::Type{Level{L,S,T}}) where {L,S,T} = S +function reflevel end +reflevel(x::Level{L,S}) where {L,S} = unwrap(S) +reflevel(::Type{Level{L,S}}) where {L,S} = unwrap(S) +reflevel(::Type{Level{L,S,T}}) where {L,S,T} = unwrap(S) dimension(x::Gain) = NoDims dimension(x::Type{<:Gain}) = NoDims @@ -62,7 +78,7 @@ end tolog(L,S,x) = (1+isrootpower(L,S)) * prefactor(L()) * (logfn(L()))(x) tolog(L,x) = (1+isrootpower(L)) * prefactor(L()) * (logfn(L()))(x) -fromlog(L,S,x) = S * expfn(L())( x / ((1+isrootpower(L,S))*prefactor(L())) ) +fromlog(L,S,x) = unwrap(S) * expfn(L())( x / ((1+isrootpower(L,S))*prefactor(L())) ) fromlog(L,x) = expfn(L())( x / ((1+isrootpower(L))*prefactor(L())) ) function Base.show(io::IO, x::MixedUnits{T,U}) where {T,U} @@ -99,7 +115,7 @@ Base. /(x::MixedUnits, y::Number) = inv(y) * x function uconvert(a::MixedUnits{Level{L,S}}, x::Number) where {L,S} dimension(a) != dimension(x) && throw(DimensionError(a,x)) - q1 = uconvert(unit(S)*a.units, linear(x)) / a.units + q1 = uconvert(unit(unwrap(S))*a.units, linear(x)) / a.units return Level{L,S}(q1) * a.units end function uconvert(a::MixedUnits{Gain{L}}, x::Gain) where {L} @@ -111,11 +127,16 @@ function uconvert(a::MixedUnits{<:Gain}, x::Number) ustr = replace(string(a), " ", "*") error("perhaps you meant `($x)*($ustr)`?") end +function uconvert(a::MixedUnits{Gain{L1,<:Real}}, x::Level{L2,S}) where {L1,L2,S} + dimension(a) != dimension(x) && throw(DimensionError(a,x)) + return Level{L1,S}(x.val) +end -ustrip(x::Level{L,S}) where {L<:LogInfo, S} = tolog(L,S,x.val/reflevel(x)) +ustrip(x::Level{L,S}) where {L<:LogInfo,S} = tolog(L,S,x.val/reflevel(x)) ustrip(x::Gain) = x.val isrootpower(T::Type{<:LogInfo}, y) = isrootpower_dim(T, dimension(y)) +isrootpower(::Type{<:LogInfo}, y::IsRootPowerRatio{T}) where {T} = T isrootpower_dim(::Type{<:LogInfo}, y) = error("undefined behavior. Please file an issue with the code needed to reproduce.") @@ -159,7 +180,8 @@ Base. /(x::Number, y::Level) = x / y.val Base. /(x::Level{L,S}, y::Number) where {L,S} = Level{L,S}(x.val / y) Base. /(x::Level{L,S}, y::Quantity) where {L,S} = x.val / y Base. /(x::Level{L,S}, y::Level) where {L,S} = x.val / y.val -Base. /(x::Level{L,S}, y::Gain) where {L,S} = Level{L,S}(fromlog(L, S, ustrip(x) - y.val)) +Base. /(x::Level{L,S}, y::Gain) where {L,S} = + Level{L,S}(fromlog(L, S, ustrip(x) - y.val)) Base. /(x::Gain{L}, y::Gain) where {L} = /(promote(x,y)...) Base. /(x::Gain{L}, y::Gain{L}) where {L} = Gain{L}(x.val - y.val) @@ -187,6 +209,12 @@ end function Base.promote_rule(::Type{Quantity{T,D,U}}, ::Type{Level{L,R,S}}) where {L,R,S,T,D,U} return promote_type(S, Quantity{T,D,U}) end +function Base.promote_rule(::Type{Level{L,R,S}}, ::Type{T}) where {L,R,S,T<:Real} + return promote_type(S,T) +end +function Base.promote_rule(::Type{T}, ::Type{Level{L,R,S}}) where {L,R,S,T<:Real} + return promote_type(S,T) +end Base.promote_rule(::Type{G1}, ::Type{G2}) where {L,T1,T2, G1<:Gain{L,T1}, G2<:Gain{L,T2}} = Gain{L,promote_type(T1,T2)} @@ -194,11 +222,6 @@ Base.promote_rule(A::Type{G}, B::Type{N}) where {L,T1, G<:Gain{L,T1}, N<:Number} error("no automatic promotion of $A and $B.") Base.promote_rule(A::Type{G}, B::Type{L}) where {G<:Gain, L2, L<:Level{L2}} = LogScaled{L2} -Base.convert(::Type{Quantity{T,D,U}}, x::Level) where {T,D,U} = - convert(Quantity{T,D,U}, x.val) -Base.convert(::Type{Quantity{T}}, x::Level) where {T<:Number} = convert(Quantity{T}, x.val) -Base.convert(::Type{T}, x::Quantity) where {L,S, T<:Level{L,S}} = T(x) - function Base.show(io::IO, x::Gain) print(io, x.val, " ", abbr(x)) nothing @@ -220,12 +243,20 @@ function Base.show(io::IO, x::Quantity{<:Union{Level,Gain},D,U}) where {D,U} end """ - powerratio(::Type{T}, x::Real) where {T<:Number} = convert(T, x) -Returns the gain as a ratio of power quantities. + powerratio(x) +Treat `x` as a ratio of power quantities (field quantities) and unit-convert to no units. + + powerratio(u::Units{()}, x::Gain) + powerratio(u::MixedUnits{<:Gain}, x::Gain) +Treat `x` as a ratio of power quantities (field quantities) and unit-convert to `u`. + + powerratio(u::Units{()}, x::Real) + powerratio(u::MixedUnits{<:Gain, <:Units{()})}, x::Real) +Fall-back methods so that `powerratio` may be used with real numbers. It is important to note that this function is undefined for `Quantity{<:Gain}` types. It is -tempting to make this function transform `-20dB/m` into `0.01/m`, however this means -something fundamentally different than `-20dB/m`, and cannot be used to calculate +tempting to make this function transform `-20dB/m` into `0.1/m`, however this means +something fundamentally different than `-20dB/m`: `0.1/m` cannot be used to calculate exponential attenuation. """ function powerratio end @@ -238,20 +269,20 @@ powerratio(u::T, x::Real) where {L, T <: MixedUnits{Gain{L}, <:Units{()}}} = ifelse(isrootpower(L), 0.5, 1) * tolog(L, x) * u """ - rootpowerratio(x::Gain) -Returns the gain as a ratio of root-power quantities (field quantities), a `Real` number. + rootpowerratio(x) +Treat `x` as a ratio of root-power quantities (field quantities) and unit-convert to no units. - rootpowerratio(::Type{T}, x::Gain) where {T} -Returns the gain as a ratio of root-power quantities (field quantities), a `Real` number, -and converts to type `T`. + rootpowerratio(u::Units{()}, x::Gain) + rootpowerratio(u::MixedUnits{<:Gain}, x::Gain) +Treat `x` as a ratio of root-power quantities (field quantities) and unit-convert to `u`. - rootpowerratio(x::Real) = x - rootpowerratio(::Type{T}, x::Real) where {T} = convert(T, x) -Fall-back methods so that `rootpowerratio` may be used generically. + rootpowerratio(u::Units{()}, x::Real) + rootpowerratio(u::MixedUnits{<:Gain, <:Units{()})}, x::Real) +Fall-back methods so that `rootpowerratio` may be used with real numbers. It is important to note that this function is undefined for `Quantity{<:Gain}` types. It is tempting to make this function transform `-20dB/m` into `0.1/m`, however this means -something fundamentally different than `-20dB/m`, and cannot be used to calculate +something fundamentally different than `-20dB/m`: `0.1/m` cannot be used to calculate exponential attenuation. `fieldratio` and `rootpowerratio` are synonymous, so you can save some typing if you like. @@ -311,12 +342,12 @@ expfn(x::LogInfo{N,e}) where {N} = exp expfn(x::LogInfo{N,B}) where {N,B} = x->B^x Base.rtoldefault(::Type{Level{L,S,T}}) where {L,S,T} = - Base.rtoldefault(typeof(tolog(L,S,oneunit(T)/S))) + Base.rtoldefault(typeof(tolog(L,S,oneunit(T)/unwrap(S)))) Base.rtoldefault(::Type{Gain{L,T}}) where {L,T} = Base.rtoldefault(T) Base.isapprox(x::Level, y::Level; kwargs...) = isapprox(promote(x,y)...; kwargs...) Base.isapprox(x::T, y::T; kwargs...) where {T <: Level} = _isapprox(x, y; kwargs...) -_isapprox(x::Level{L,S,T}, y::Level{L,S,T}; atol = Level{L,S}(S), kwargs...) where {L,S,T} = +_isapprox(x::Level{L,S,T}, y::Level{L,S,T}; atol = Level{L,S}(reflevel(x)), kwargs...) where {L,S,T} = isapprox(ustrip(x), ustrip(y); atol = ustrip(convert(Level{L,S}, atol)), kwargs...) diff --git a/src/pkgdefaults.jl b/src/pkgdefaults.jl index 63caef57..5d2d704b 100644 --- a/src/pkgdefaults.jl +++ b/src/pkgdefaults.jl @@ -176,6 +176,7 @@ Unitful.offsettemp(::Unitful.Unit{:Fahrenheit}) = 45967//100 @logunit dBu "dBu" Decibel sqrt(0.6)V @logunit dBμV "dBμV" Decibel 1μV @logunit dBSPL "dBSPL" Decibel 20μPa +@logunit dBFS "dBFS" Decibel RootPowerRatio(1) const dBµV = dBμV # different character encoding of μ diff --git a/src/user.jl b/src/user.jl index 74335be7..619fef6c 100644 --- a/src/user.jl +++ b/src/user.jl @@ -349,7 +349,6 @@ macro logscale(symb,abbr,name,base,prefactor,irp) end macro $(esc(symb))(expr::Expr) - # s = Symbol("_", $(esc(symb))) expr.args[1] != :/ && throw(ArgumentError(join(["usage: `@", $(String(symb)), " (a)/(b)`"]))) length(expr.args) != 3 && @@ -357,17 +356,40 @@ macro logscale(symb,abbr,name,base,prefactor,irp) return Expr(:call, $(esc(symb)), expr.args[2], expr.args[3]) end + macro $(esc(symb))(expr::Expr, tf::Bool) + expr.args[1] != :/ && + throw(ArgumentError(join(["usage: `@", $(String(symb)), " (a)/(b)`"]))) + length(expr.args) != 3 && + throw(ArgumentError(join(["usage: `@", $(String(symb)), " (a)/(b)`"]))) + return Expr(:call, $(esc(symb)), expr.args[2], expr.args[3], tf) + end + function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Number) dimension(num) != dimension(den) && throw(DimensionError(num,den)) dimension(num) == NoDims && - throw(ArgumentError("cannot use with dimensionless numbers.")) + throw(ArgumentError(string("to use with dimensionless numbers, pass a ", + "final `Bool` argument: true if the ratio is a root-power ratio, ", + "false otherwise."))) return Level{$(esc(name)), den}(num) end + function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Number, irp::Bool) + dimension(num) != dimension(den) && throw(DimensionError(num,den)) + dimension(num) != NoDims && + throw(ArgumentError(string("can only be used with dimensionless numbers ", + "when passing a final Bool argument."))) + T = ifelse(irp, RootPowerRatio, PowerRatio) + return Level{$(esc(name)), T(den)}(num) + end + function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Units) $(esc(symb))(num, 1*den) end + function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Units, irp::Bool) + $(esc(symb))(num, 1*den, irp) + end + $(esc(symb)) end end From a4469e69406e9a328165705e044dde795d8c2a71 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Sun, 22 Oct 2017 15:19:09 -0700 Subject: [PATCH 08/12] Add some docs, tests, a few `isrootpower_dim` methods. --- docs/src/logarithm.md | 69 ++++++++++++++++++++++++++++++++++++------- src/logarithm.jl | 4 ++- src/pkgdefaults.jl | 3 ++ src/user.jl | 4 +-- test/runtests.jl | 5 ++-- 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index bb03df64..afe72afa 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -25,7 +25,8 @@ julia> u"dB"*3 === 3u"dB" true ``` -Currently implemented are `dB`, `B`, `dBm`, `dBV`, `dBu`, `dBμV`, `dBSPL`, `cNp`, `Np`. +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: @@ -46,13 +47,15 @@ julia> @Np e*V/V # e = 2.71828... 1.0 Np (1 V) ``` -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: +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): ```jldoctest julia> using Unitful: dB, mW, V -julia> u"dB"(10mW,mW) +julia> dB(10mW,mW) 10.0 dBm ``` @@ -60,10 +63,22 @@ In calculating the logarithms, the log function appropriate to the scale in ques (`log10` for decibels, `log` for nepers). There is an important difference in these two approaches to constructing logarithmic -quantities. When we construct `3dBm`, ultimately the power in `mW` is being stored, -resulting in a lossy conversion. However, -`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`. +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: + +```jldoctest +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, not other units or quantities. This is done to avoid issues with commutativity and associativity, @@ -72,11 +87,40 @@ is because `dB` acts more like a constructor than a proper unit. In this package documentation, we take some pains to avoid using the term "logarithmic units" where possible, and the usage and design of this package reflects that. -### Logarithmic quantities with no reference level specified - 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. +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: + +```jldoctest +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: + +```jldoctest +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, @@ -111,6 +155,9 @@ quantities, or `Level`s for short: Unitful.Level ``` +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`: ```@docs diff --git a/src/logarithm.jl b/src/logarithm.jl index c1ed9446..2127107d 100644 --- a/src/logarithm.jl +++ b/src/logarithm.jl @@ -2,6 +2,8 @@ struct IsRootPowerRatio{S,T} val::T end IsRootPowerRatio{S}(x) where {S} = IsRootPowerRatio{S, typeof(x)}(x) +Base.show(io::IO, x::IsRootPowerRatio{S}) where {S} = + print(io, ifelse(S, "root-power ratio", "power ratio"), " with reference ", x.val) const PowerRatio{T} = IsRootPowerRatio{false,T} const RootPowerRatio{T} = IsRootPowerRatio{true,T} dimension(x::IsRootPowerRatio{S,T}) where {S,T} = dimension(T) @@ -17,7 +19,7 @@ dimension(x::Type{T}) where {T<:Level} = dimension(reflevel(T)) logunit(x::Level{L,S}) where {L,S} = MixedUnits{Level{L,S}}() logunit(x::Type{T}) where {L,S,T<:Level{L,S}} = MixedUnits{Level{L,S}}() -abbr(x::Level{L,S}) where {L,S} = join([abbr(L()), " (", reflevel(x), ")"]) +abbr(x::Level{L,S}) where {L,S} = join([abbr(L()), " (", S, ")"]) function uconvert(a::Units, x::Level) dimension(a) != dimension(x) && throw(DimensionError(a,x)) diff --git a/src/pkgdefaults.jl b/src/pkgdefaults.jl index 5d2d704b..3d73a217 100644 --- a/src/pkgdefaults.jl +++ b/src/pkgdefaults.jl @@ -185,6 +185,9 @@ isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(W))) = false isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(V))) = true isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(A))) = true isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(Pa))) = true +isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(W/m^2/Hz))) = false # spectral flux dens. +isrootpower_dim(::Type{<:LogInfo}, ::typeof(dimension(W/m^2))) = false # intensity +isrootpower_dim(::Type{<:LogInfo}, ::typeof(𝐋^3)) = false # reflectivity ######### diff --git a/src/user.jl b/src/user.jl index 619fef6c..4e1b1284 100644 --- a/src/user.jl +++ b/src/user.jl @@ -376,8 +376,8 @@ macro logscale(symb,abbr,name,base,prefactor,irp) function (::$(esc(:typeof))($(esc(symb))))(num::Number, den::Number, irp::Bool) dimension(num) != dimension(den) && throw(DimensionError(num,den)) dimension(num) != NoDims && - throw(ArgumentError(string("can only be used with dimensionless numbers ", - "when passing a final Bool argument."))) + throw(ArgumentError(string("when passing a final Bool argument, ", + "this can only be used with dimensionless numbers."))) T = ifelse(irp, RootPowerRatio, PowerRatio) return Level{$(esc(name)), T(den)}(num) end diff --git a/test/runtests.jl b/test/runtests.jl index 3d956e27..38d94b43 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1034,8 +1034,9 @@ end @testset "> Implicit construction" begin @testset ">> Level" begin - @test 20*dBm == (@dB 100mW/mW) == (@dB 100mW/1mW) - @test 20*dBV == (@dB 10V/V) == (@dB 10V/1V) + @test 20*dBm == (@dB 100mW/mW) == (@dB 100mW/1mW) == dB(100mW,mW) == dB(100mW,1mW) + @test 20*dBV == (@dB 10V/V) == (@dB 10V/1V) == dB(10V,V) == dB(10V,1V) + @test_throws ArgumentError @dB 10V/V true end @testset ">> Gain" begin From b72197a1d14da4768a87e1b1a496661f2b726afa Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Sun, 22 Oct 2017 15:41:04 -0700 Subject: [PATCH 09/12] remove old unit aliases --- src/user.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/user.jl b/src/user.jl index 4e1b1284..97ea95d5 100644 --- a/src/user.jl +++ b/src/user.jl @@ -51,14 +51,12 @@ macro dimension(symb, abbr, name) s = Symbol(symb) x = Expr(:quote, name) uname = Symbol(name,"Units") - uname_old = Symbol(name,"Unit") funame = Symbol(name,"FreeUnits") esc(quote Unitful.abbr(::Unitful.Dimension{$x}) = $abbr const $s = Unitful.Dimensions{(Unitful.Dimension{$x}(1),)}() const ($name){T,U} = Unitful.Quantity{T,typeof($s),U} const ($uname){U} = Unitful.Units{U,typeof($s)} - const ($uname_old){U} = Unitful.Units{U,typeof($s)} const ($funame){U} = Unitful.FreeUnits{U,typeof($s)} $s end) @@ -81,12 +79,10 @@ Usage examples: """ macro derived_dimension(name, dims) uname = Symbol(name,"Units") - uname_old = Symbol(name,"Unit") funame = Symbol(name,"FreeUnits") esc(quote const ($name){T,U} = Unitful.Quantity{T,typeof($dims),U} const ($uname){U} = Unitful.Units{U,typeof($dims)} - const ($uname_old){U} = Unitful.Units{U,typeof($dims)} const ($funame){U} = Unitful.FreeUnits{U,typeof($dims)} nothing end) From f4c73d6e17de4765affd8209f482b3731221bc44 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Sun, 22 Oct 2017 16:29:11 -0700 Subject: [PATCH 10/12] Type aliases for Quantitys have been generalized; for example, we now have 3dBm isa Unitful.Power. --- src/user.jl | 20 ++++++++++++-------- test/runtests.jl | 6 +++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/user.jl b/src/user.jl index 97ea95d5..789a79ed 100644 --- a/src/user.jl +++ b/src/user.jl @@ -32,9 +32,9 @@ This macro extends [`Unitful.abbr`](@ref) to display the new dimension in an abbreviated format using the string `abbr`. Type aliases are created that allow the user to dispatch on -[`Unitful.Quantity`](@ref) and [`Unitful.Units`](@ref) objects of the newly -defined dimension. The type alias for quantities is simply given by `name`, -and the type alias for units is given by `name*"Units"`, e.g. `LengthUnits`. +[`Unitful.Quantity`](@ref), [`Unitful.Level`](@ref) and [`Unitful.Units`](@ref) objects +of the newly defined dimension. The type alias for quantities or levels is simply given by +`name`, and the type alias for units is given by `name*"Units"`, e.g. `LengthUnits`. Note that there is also `LengthFreeUnits`, for example, which is an alias for dispatching on `FreeUnits` with length dimensions. The aliases are not exported. @@ -55,7 +55,9 @@ macro dimension(symb, abbr, name) esc(quote Unitful.abbr(::Unitful.Dimension{$x}) = $abbr const $s = Unitful.Dimensions{(Unitful.Dimension{$x}(1),)}() - const ($name){T,U} = Unitful.Quantity{T,typeof($s),U} + const ($name){T,U} = Union{ + Unitful.Quantity{T,typeof($s),U}, + Unitful.Level{L,S,Unitful.Quantity{T,typeof($s),U}} where {L,S}} const ($uname){U} = Unitful.Units{U,typeof($s)} const ($funame){U} = Unitful.FreeUnits{U,typeof($s)} $s @@ -64,9 +66,9 @@ end """ @derived_dimension(name, dims) -Creates type aliases to allow dispatch on [`Unitful.Quantity`](@ref) and -[`Unitful.Units`](@ref) objects of a derived dimension, like area, which is just -length squared. The type aliases are not exported. +Creates type aliases to allow dispatch on [`Unitful.Quantity`](@ref), +[`Unitful.Level`](@ref), and [`Unitful.Units`](@ref) objects of a derived dimension, +like area, which is just length squared. The type aliases are not exported. `dims` is a [`Unitful.Dimensions`](@ref) object. @@ -81,7 +83,9 @@ macro derived_dimension(name, dims) uname = Symbol(name,"Units") funame = Symbol(name,"FreeUnits") esc(quote - const ($name){T,U} = Unitful.Quantity{T,typeof($dims),U} + const ($name){T,U} = Union{ + Unitful.Quantity{T,typeof($dims),U}, + Unitful.Level{L,S,Unitful.Quantity{T,typeof($dims),U}} where {L,S}} const ($uname){U} = Unitful.Units{U,typeof($dims)} const ($funame){U} = Unitful.FreeUnits{U,typeof($dims)} nothing diff --git a/test/runtests.jl b/test/runtests.jl index 38d94b43..1c627504 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -30,7 +30,8 @@ import Unitful: Mass, Current, Temperature, - Action + Action, + Power import Unitful: LengthUnits, AreaUnits, MassUnits @@ -349,6 +350,9 @@ end @test isa(1cd, Luminosity) @test isa(2π*rad*1.0m, Length) @test isa(u"h", Action) + @test isa(3u"dBm", Power) + @test isa(3u"dBm*Hz*s", Power) + end @testset "Mathematics" begin From 64e1f0383e5e76b5d506b1eed967e614d28f1740 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Sun, 22 Oct 2017 17:04:55 -0700 Subject: [PATCH 11/12] Fix out-of-date docs --- docs/src/logarithm.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index afe72afa..37622c83 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -80,12 +80,11 @@ julia> (@dB 10mW/1mW) + (@dB 10mW/2mW) Addition will be discussed more later. -Note that logarithmic "units" can only multiply or be multiplied by pure numbers, not -other 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. In this package and in the -documentation, we take some pains to avoid using the term "logarithmic units" where possible, -and the usage and design of this package reflects that. +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 From bc142d28a38b425e1ded346d027d1ebb6afddfe9 Mon Sep 17 00:00:00 2001 From: Andrew Keller Date: Mon, 23 Oct 2017 09:17:17 -0700 Subject: [PATCH 12/12] Add note to documentation [skip ci] --- docs/src/logarithm.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/src/logarithm.md b/docs/src/logarithm.md index 37622c83..7c443e0f 100644 --- a/docs/src/logarithm.md +++ b/docs/src/logarithm.md @@ -4,6 +4,9 @@ DocTestSetup = quote end ``` +!!! note + Logarithmic scales are new to Unitful and should be considered experimental. + Unitful provides a way to use logarithmically-scaled quantities as of v0.4.0. 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