From 7056ae26d066a1c293f034508d7814775a59eb99 Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Sat, 2 Jan 2021 13:38:10 +0100 Subject: [PATCH 01/24] add logrange [skip ci] --- base/range.jl | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/base/range.jl b/base/range.jl index 9480b46d8fcf1..04fbf856f0dd2 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1488,3 +1488,49 @@ julia> mod(3, 0:2) # mod(3, 3) """ mod(i::Integer, r::OneTo) = mod1(i, last(r)) mod(i::Integer, r::AbstractUnitRange{<:Integer}) = mod(i-first(r), length(r)) + first(r) + +""" + logrange(start => stop, length) + +Returns an iterator which runs from `start` to `stop` with `length` elements, +spaced logarithmically rather than linearly as for [`range`](@ref). +That is, the ratio of successive elements is constant, not the difference. + +!!! compat "Julia 1.3" + This function requires at least Julia 1.7. + +# Examples +``` +julia> foreach(println, logrange(2 => 16, 4)) +2.0 +4.0 +8.0 +16.0 + +julia> collect(logrange(1000 => 1, 4)) +4-element Vector{Float64}: + 1.0 + 9.999999999999998 + 99.99999999999997 + 1000.0 + +julia> ans ≈ 10 .^ (3:-1:0) +true + +julia> collect(logrange(-1 => -2f0, 3)) +3-element Vector{Float32}: + -1.0 + -1.4142135 + -2.0 +``` +""" +function logrange(p::Pair{<:Real, <:Real}, n::Integer) + lo, hi = promote(p.first, p.second) + if lo>0 && hi>0 + (exp(x) for x in range(log(lo), log(hi), length=n)) + elseif lo<0 && hi<0 + (-exp(x) for x in range(log(-lo), log(-hi), length=n)) + else + throw(DomainError(p, "logrange requires that first and last elements are both positive, or both negative")) + end +end From bb5446aa77170fb78701be9defb17abe4a5450cb Mon Sep 17 00:00:00 2001 From: Michael Abbott Date: Sun, 3 Jan 2021 21:51:51 +0100 Subject: [PATCH 02/24] second implementation, now squashed: move to iterators.jl, upgrade implementation mistakes? imports a few fixes add docstring to type, and export it, like LinRange move back to to ranges.jl delete some type special cases docs change notation to avoid Pair, add ratio keyword --- base/exports.jl | 2 + base/range.jl | 156 +++++++++++++++++++++++++++++------- doc/src/base/collections.md | 2 + doc/src/base/math.md | 1 + test/ranges.jl | 42 ++++++++++ 5 files changed, 176 insertions(+), 27 deletions(-) diff --git a/base/exports.jl b/base/exports.jl index fbb5b449719d6..73db70abb6036 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -58,6 +58,7 @@ export IOBuffer, IOStream, LinRange, + LogRange, Irrational, LazyString, Matrix, @@ -676,6 +677,7 @@ export enumerate, # re-exported from Iterators zip, only, + logrange, # object identity and equality copy, diff --git a/base/range.jl b/base/range.jl index 04fbf856f0dd2..acbe89fb774b2 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1489,48 +1489,150 @@ julia> mod(3, 0:2) # mod(3, 3) mod(i::Integer, r::OneTo) = mod1(i, last(r)) mod(i::Integer, r::AbstractUnitRange{<:Integer}) = mod(i-first(r), length(r)) + first(r) + """ - logrange(start => stop, length) + logrange(start, stop; length, ratio) -Returns an iterator which runs from `start` to `stop` with `length` elements, -spaced logarithmically rather than linearly as for [`range`](@ref). -That is, the ratio of successive elements is constant, not the difference. +Create an iterator whose elements are spaced logarithmically, rather than linearly +as for [`range`](@ref), i.e. the ratio of successive elements is constant, not the difference. -!!! compat "Julia 1.3" - This function requires at least Julia 1.7. +One of two keywords must be provided: + +* Given the `length`, the necessary ratio is computed accurately to reach the final and + intermediate values rationally. To avoid this overhead, see the [`LogRange`](@ref) constructor. + +* Otherwise, given the `ratio`, the final value is the last one `<= stop`. + +!!! compat "Julia 1.7" + This function requires at least Julia 1.7. + +This is similar to `geomspace` in Python, and to `PowerRange` in Mathematica. # Examples -``` -julia> foreach(println, logrange(2 => 16, 4)) -2.0 -4.0 -8.0 -16.0 - -julia> collect(logrange(1000 => 1, 4)) -4-element Vector{Float64}: - 1.0 - 9.999999999999998 - 99.99999999999997 +```jldoctest +julia> for x in logrange(2, 19; ratio=2) + println(x) + end +2 +4 +8 +16 + +julia> collect(logrange(1000, 1; length=7)) +7-element Vector{Float64}: 1000.0 + 316.22776601683796 + 100.0 + 31.622776601683793 + 10.0 + 3.1622776601683795 + 1.0 -julia> ans ≈ 10 .^ (3:-1:0) +julia> ans ≈ 10 .^ (3:-0.5:0) true -julia> collect(logrange(-1 => -2f0, 3)) +julia> collect(logrange(-1, -2f0, length=3)) 3-element Vector{Float32}: -1.0 -1.4142135 -2.0 + +julia> map(rad2deg∘angle, logrange(1, -1+0im, length=5)) +5-element Vector{Float64}: + 0.0 + 45.0 + 90.0 + 135.0 + 180.0 +``` +""" +function logrange(start::Number, stop::Number; + length::Union{Nothing,Integer}=nothing, ratio::Union{Nothing,Number}=nothing) + if length !== nothing + length >= 2 || throw(ArgumentError("logrange must have length of at least 2")) + _start, _stop = map(float, promote(start, stop)) + _ratio = ratio_nth_root(stop, start, Int(length)-1) + if ratio !== nothing + check = oftype(float(ratio), _ratio) + ratio == check || throw(ArgumentError("ratio = $ratio does not match $check computed from length = $length")) + end + LogRange(_start, _ratio, Int(length)) + elseif ratio !== nothing + _start, _ratio = promote(start, ratio) + num = trunc(Int, log(ratio, stop/_start)) + fin = prod(_ratio for _ in 1:num; init=start) + _length = fin <= stop ? num+1 : num + LogRange(_start, _ratio, _length) + end +end + +ratio_nth_root(a::Number, b::Number, n::Int) = (a/b)^(1/n) +ratio_nth_root(a::Float32, b::Float32, n::Int) = (Float64(a)/Float64(b))^(1/n) +function ratio_nth_root(a::Real, b::Real, m::Int) + over = TwicePrecision(a) / TwicePrecision(b) + r1 = TwicePrecision((over.hi)^(1/m)) + r1pow = prod(r1 for _ in 1:m-1) + r2 = (m-1)*r1/m + over / (m * r1pow) +end + +""" + LogRange{T,S}(start::T, ratio::S, len::Int) + +Iterator which multiplies by `ratio` at each step, giving in total `len` objects. +Constructing this directly instead of using [`logrange`](@ref) avoids the cost of +computing `ratio` to high precision. + +Note that its output is of type `T`, so iterating for intance `LogRange(2, pi, 10)` will +lead to an `InexactError`, since `2*pi` cannot be converted to `Int`. + +# Examples + +```jldoctest +julia> collect(LogRange(0.1, sqrt(10), 5)) # no corrections for rounding errors +5-element Vector{Float64}: + 0.1 + 0.316227766016838 + 1.0000000000000002 + 3.1622776601683804 + 10.000000000000004 + +julia> r = logrange(0.1, 10, length=5) # type T != type S +LogRange{Float64, Base.TwicePrecision{Float64}}(0.1, Base.TwicePrecision{Float64}(3.162277660168379, 2.0941562178568784e-16), 5) + +julia> collect(r) +5-element Vector{Float64}: + 0.1 + 0.31622776601683794 + 1.0 + 3.1622776601683795 + 10.0 ``` """ -function logrange(p::Pair{<:Real, <:Real}, n::Integer) - lo, hi = promote(p.first, p.second) - if lo>0 && hi>0 - (exp(x) for x in range(log(lo), log(hi), length=n)) - elseif lo<0 && hi<0 - (-exp(x) for x in range(log(-lo), log(-hi), length=n)) +struct LogRange{T,S} + start::T + ratio::S + len::Int +end + +length(r::LogRange) = r.len +size(r::LogRange) = (r.len,) + +function iterate(r::LogRange{T,S}) where {T,S} + y = convert(promote_type(T,S), r.start) + r.start, (y,1) +end + +function iterate(r::LogRange{T}, (x,n)) where {T} + if n >= r.len + nothing else - throw(DomainError(p, "logrange requires that first and last elements are both positive, or both negative")) + y = x * r.ratio + convert(T,y), (y,n+1) end end + +eltype(::Type{<:LogRange{T}}) where {T} = T +eltype(r::LogRange{T}) where {T} = T + +ndims(::Type{<:LogRange}) = 1 +ndims(r::LogRange) = 1 diff --git a/doc/src/base/collections.md b/doc/src/base/collections.md index 9d68d55be3459..3ea37d949ab00 100644 --- a/doc/src/base/collections.md +++ b/doc/src/base/collections.md @@ -48,6 +48,7 @@ Fully implemented by: * [`Set`](@ref) * [`Pair`](@ref) * [`NamedTuple`](@ref) + * [`LogRange`](@ref) ## Constructors and Types @@ -58,6 +59,7 @@ Base.AbstractUnitRange Base.StepRange Base.UnitRange Base.LinRange +Base.LogRange ``` ## General Collections diff --git a/doc/src/base/math.md b/doc/src/base/math.md index 86a7e4d8d3173..8f7f0572b5f15 100644 --- a/doc/src/base/math.md +++ b/doc/src/base/math.md @@ -37,6 +37,7 @@ Base.:(>>>) Base.bitrotate Base.:(:) Base.range +Base.logrange Base.OneTo Base.StepRangeLen Base.:(==) diff --git a/test/ranges.jl b/test/ranges.jl index 549078bab45fe..16d61fb46d38d 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2435,6 +2435,7 @@ end @test ((x + 1) in StepRangeLen(x, 0, rand(1:100))) == false end +<<<<<<< HEAD @testset "issue #42528" begin struct Fix42528 <: Unsigned val::UInt @@ -2456,6 +2457,7 @@ end @test_throws DomainError Fix42528(0x0) - Fix42528(0x1) end +<<<<<<< HEAD let r = Ptr{Cvoid}(20):-UInt(2):Ptr{Cvoid}(10) @test isempty(r) @test length(r) == 0 @@ -2492,6 +2494,7 @@ end @test test_firstindex(StepRange{Union{Int64,Int128},Int}(Int64(1), 1, Int128(0))) end +<<<<<<< HEAD @testset "PR 49516" begin struct PR49516 <: Signed n::Int @@ -2603,3 +2606,42 @@ end errmsg = ("deliberately unsupported for CartesianIndex", "StepRangeLen") @test_throws errmsg range(CartesianIndex(1), step=CartesianIndex(1), length=3) end + +@testset "logrange" begin + @test collect(logrange(2, 16, length=4)) == [2, 4, 8, 16] + @test collect(logrange(1000, 1, length=4)) == [1000, 100, 10, 1] + @test collect(logrange(-1, -4, length=3)) == [-1, -2, -4] + @test collect(logrange(1, -1+0im, length=3)) == [1, im, -1] + + @test collect(logrange(1/8, 8.0, length=7)) == [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] + @test collect(logrange(1, 10^10, length=21))[1:2:end] == 10 .^ (0:10) + @test collect(logrange(0.789, 123_456, length=135_790))[[begin, end]] == [0.789, 123_456] + + @test collect(logrange(1, 10, length=3)) isa Vector{Float64} + @test collect(logrange(1, 10, length=Int32(3))) isa Vector{Float64} + @test collect(logrange(1, 10f0, length=3)) isa Vector{Float32} + @test collect(logrange(1f0, 10, length=3)) isa Vector{Float32} + @test collect(logrange(1f0, 10+im, length=3)) isa Vector{ComplexF32} + @test collect(logrange(1f0, 10.0+im, length=3)) isa Vector{ComplexF64} + @test collect(logrange(1, big(10), length=3)) isa Vector{BigFloat} + + @test length(logrange(2, 16, length=4)) == 4 + @test size(logrange(2, 16, length=4)) == (4,) + @test ndims(logrange(2, 16, length=4)) == 1 + @test ndims(typeof(logrange(2, 16, length=4))) == 1 + @test eltype(logrange(2, 16, length=4)) == Float64 + @test eltype(typeof(logrange(2, 16, length=4))) == Float64 + @test Base.IteratorSize(typeof(logrange(2=>3, length=4))) == Base.HasLength() + @test Base.IteratorEltype(typeof(logrange(2, 16, length=4))) == Base.HasEltype() + + @test_throws ArgumentError logrange(1, 10, length=1) # allows only length >= 2 + @test_throws DomainError logrange(1, -1, length=3) # needs complex numbers + + @test collect(logrange(2, 16, ratio=2)) == [2, 4, 8, 16] + @test collect(logrange(1, 10, ratio=sqrt(10))) == [1, sqrt(10)] + @test last(collect(logrange(1, 10, ratio=sqrt(10)))) * sqrt(10) > 10 + @test collect(logrange(1, 10, ratio=prevfloat(sqrt(10)))) ≈ [1, sqrt(10), 10] + + @test collect(logrange(2, 16, length=4, ratio=2)) == [2, 4, 8, 16] # over-constrained + @test_throws ArgumentError @test collect(logrange(2, 16, length=4, ratio=3)) +end From 657c3787f04c5372cf26592a0a52d80dace453c0 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Thu, 21 Oct 2021 07:35:14 -0400 Subject: [PATCH 03/24] change to AbstractVector implementation, squashed: docstring, first, showarg spaces + examples fix tests better examples fix a bad rebase space supertype replace with version which hits endpoints avoid macro avoid another macro rm line don't test out of bounds access move negative handling restore boundscheck macro compact printing simplify, handle 0, allow empty print like LinRange whitespace doctest move show skip a test on Int32 systems move work to loginterp function eltype one more Int32 problem comment v1.9 --- base/exports.jl | 1 - base/range.jl | 196 +++++++++++++++++-------------------------- base/show.jl | 7 ++ doc/src/base/math.md | 1 - test/ranges.jl | 94 ++++++++++++--------- 5 files changed, 140 insertions(+), 159 deletions(-) diff --git a/base/exports.jl b/base/exports.jl index 73db70abb6036..b72075d09793a 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -677,7 +677,6 @@ export enumerate, # re-exported from Iterators zip, only, - logrange, # object identity and equality copy, diff --git a/base/range.jl b/base/range.jl index acbe89fb774b2..48a9c36068596 100644 --- a/base/range.jl +++ b/base/range.jl @@ -620,7 +620,7 @@ parameters `pre` and `post` characters for each printed row, `sep` separator string between printed elements, `hdots` string for the horizontal ellipsis. """ -function print_range(io::IO, r::AbstractRange, +function print_range(io::IO, r::AbstractArray, pre::AbstractString = " ", sep::AbstractString = ", ", post::AbstractString = "", @@ -1491,148 +1491,108 @@ mod(i::Integer, r::AbstractUnitRange{<:Integer}) = mod(i-first(r), length(r)) + """ - logrange(start, stop; length, ratio) + LogRange(start, stop, length) -Create an iterator whose elements are spaced logarithmically, rather than linearly -as for [`range`](@ref), i.e. the ratio of successive elements is constant, not the difference. +Construct a specialized array whose elements are spaced logarithmically +between the given endpoints. That is, the ratio of successive elements is +a constant, calculated from the length. -One of two keywords must be provided: +Like [`LinRange`](@ref), floating-point error may cause it to miss intermediate +rational values, but the first and last elements should be exactly those provided. -* Given the `length`, the necessary ratio is computed accurately to reach the final and - intermediate values rationally. To avoid this overhead, see the [`LogRange`](@ref) constructor. +This is similar to `geomspace` in Python. Unlike `PowerRange` in Mathematica, +you specify the number of elements not the ratio. +Unlike `logspace` in Python and Matlab, the `start` and `stop` arguments are +always the first and last elements of the result, not powers applied to some base. -* Otherwise, given the `ratio`, the final value is the last one `<= stop`. - -!!! compat "Julia 1.7" - This function requires at least Julia 1.7. - -This is similar to `geomspace` in Python, and to `PowerRange` in Mathematica. +!!! compat "Julia 1.9" + This function requires at least Julia 1.9. # Examples ```jldoctest -julia> for x in logrange(2, 19; ratio=2) - println(x) - end -2 -4 -8 -16 - -julia> collect(logrange(1000, 1; length=7)) -7-element Vector{Float64}: - 1000.0 - 316.22776601683796 - 100.0 - 31.622776601683793 - 10.0 - 3.1622776601683795 - 1.0 - -julia> ans ≈ 10 .^ (3:-0.5:0) -true +julia> LogRange(10, 4000, 3) +3-element LogRange{Float64}: + 10.0, 200.0, 4000.0 -julia> collect(logrange(-1, -2f0, length=3)) -3-element Vector{Float32}: - -1.0 - -1.4142135 - -2.0 +julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean +true -julia> map(rad2deg∘angle, logrange(1, -1+0im, length=5)) +julia> LogRange(-250_000, -0.04, 5) |> collect 5-element Vector{Float64}: - 0.0 - 45.0 - 90.0 - 135.0 - 180.0 -``` -""" -function logrange(start::Number, stop::Number; - length::Union{Nothing,Integer}=nothing, ratio::Union{Nothing,Number}=nothing) - if length !== nothing - length >= 2 || throw(ArgumentError("logrange must have length of at least 2")) - _start, _stop = map(float, promote(start, stop)) - _ratio = ratio_nth_root(stop, start, Int(length)-1) - if ratio !== nothing - check = oftype(float(ratio), _ratio) - ratio == check || throw(ArgumentError("ratio = $ratio does not match $check computed from length = $length")) - end - LogRange(_start, _ratio, Int(length)) - elseif ratio !== nothing - _start, _ratio = promote(start, ratio) - num = trunc(Int, log(ratio, stop/_start)) - fin = prod(_ratio for _ in 1:num; init=start) - _length = fin <= stop ? num+1 : num - LogRange(_start, _ratio, _length) - end -end + -250000.0 + -5000.000000000003 + -100.0 + -1.9999999999999998 + -0.04 -ratio_nth_root(a::Number, b::Number, n::Int) = (a/b)^(1/n) -ratio_nth_root(a::Float32, b::Float32, n::Int) = (Float64(a)/Float64(b))^(1/n) -function ratio_nth_root(a::Real, b::Real, m::Int) - over = TwicePrecision(a) / TwicePrecision(b) - r1 = TwicePrecision((over.hi)^(1/m)) - r1pow = prod(r1 for _ in 1:m-1) - r2 = (m-1)*r1/m + over / (m * r1pow) -end - -""" - LogRange{T,S}(start::T, ratio::S, len::Int) - -Iterator which multiplies by `ratio` at each step, giving in total `len` objects. -Constructing this directly instead of using [`logrange`](@ref) avoids the cost of -computing `ratio` to high precision. - -Note that its output is of type `T`, so iterating for intance `LogRange(2, pi, 10)` will -lead to an `InexactError`, since `2*pi` cannot be converted to `Int`. +julia> LogRange(√2, 32, 10) +10-element LogRange{Float64}: + 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 -# Examples - -```jldoctest -julia> collect(LogRange(0.1, sqrt(10), 5)) # no corrections for rounding errors -5-element Vector{Float64}: - 0.1 - 0.316227766016838 - 1.0000000000000002 - 3.1622776601683804 - 10.000000000000004 - -julia> r = logrange(0.1, 10, length=5) # type T != type S -LogRange{Float64, Base.TwicePrecision{Float64}}(0.1, Base.TwicePrecision{Float64}(3.162277660168379, 2.0941562178568784e-16), 5) +julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) +true -julia> collect(r) -5-element Vector{Float64}: - 0.1 - 0.31622776601683794 - 1.0 - 3.1622776601683795 - 10.0 +julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) +true ``` """ -struct LogRange{T,S} +struct LogRange{T} <: AbstractArray{T,1} start::T - ratio::S + stop::T len::Int + function LogRange(start::T1, stop::T2, len::Integer) where {T1<:Number, T2<:Number} + T = float(promote_type(T1, T2)) + lo = iszero(start) ? T(NaN) : T(start) + hi = iszero(stop) ? T(NaN) : T(stop) + if len < 0 + throw(ArgumentError("LogRange can't have negative length")) + elseif len == 0 + return new{T}(lo, hi, len) + elseif len == 1 && start != stop + throw(ArgumentError("endpoints of LogRange differ, with length = 1")) + end + if T <: Real && (start<0) ⊻ (stop<0) + throw(DomainError((start, stop), + "LogRange will only return complex results if called with a complex argument")) + end + new{T}(lo, hi, len) + end end -length(r::LogRange) = r.len size(r::LogRange) = (r.len,) -function iterate(r::LogRange{T,S}) where {T,S} - y = convert(promote_type(T,S), r.start) - r.start, (y,1) +first(r::LogRange) = r.start +last(r::LogRange) = r.stop + +function getindex(r::LogRange, i::Integer) + @inline + @boundscheck checkbounds(r, i) + unsafe_getindex(r, i) end -function iterate(r::LogRange{T}, (x,n)) where {T} - if n >= r.len - nothing +function unsafe_getindex(r::LogRange, i::Integer) + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) + if r.len == 1 + return r.start + end + if eltype(r) <: Real && r.start < 0 # constructor guarantees r.stop < 0 too + -loginterp(-r.start, -r.stop, Int(i), r.len) else - y = x * r.ratio - convert(T,y), (y,n+1) + loginterp(r.start, r.stop, Int(i), r.len) end end -eltype(::Type{<:LogRange{T}}) where {T} = T -eltype(r::LogRange{T}) where {T} = T +function loginterp(lo::T, hi::T, j::Int, n::Int) where {T} + @inline + convert(T, lo^((n-j)/(n-1)) * hi^((j-1)/(n-1))) +end -ndims(::Type{<:LogRange}) = 1 -ndims(r::LogRange) = 1 +function show(io::IO, r::LogRange) # compact + print(io, "LogRange(") + show(io, first(r)) + print(io, ", ") + show(io, last(r)) + print(io, ", ") + show(io, length(r)) + print(io, ')') +end diff --git a/base/show.jl b/base/show.jl index a5bf5f73ba0ed..217b22ab39ef8 100644 --- a/base/show.jl +++ b/base/show.jl @@ -23,6 +23,13 @@ function show(io::IO, ::MIME"text/plain", r::LinRange) print_range(io, r) end +function show(io::IO, ::MIME"text/plain", r::LogRange) # display LogRange like LinRange + isempty(r) && return show(io, r) + summary(io, r) + println(io, ":") + print_range(io, r, " ", ", ", "", " \u2026 ") +end + function _isself(ft::DataType) ftname = ft.name isdefined(ftname, :mt) || return false diff --git a/doc/src/base/math.md b/doc/src/base/math.md index 8f7f0572b5f15..86a7e4d8d3173 100644 --- a/doc/src/base/math.md +++ b/doc/src/base/math.md @@ -37,7 +37,6 @@ Base.:(>>>) Base.bitrotate Base.:(:) Base.range -Base.logrange Base.OneTo Base.StepRangeLen Base.:(==) diff --git a/test/ranges.jl b/test/ranges.jl index 16d61fb46d38d..8663c01bc9309 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2435,7 +2435,6 @@ end @test ((x + 1) in StepRangeLen(x, 0, rand(1:100))) == false end -<<<<<<< HEAD @testset "issue #42528" begin struct Fix42528 <: Unsigned val::UInt @@ -2457,7 +2456,6 @@ end @test_throws DomainError Fix42528(0x0) - Fix42528(0x1) end -<<<<<<< HEAD let r = Ptr{Cvoid}(20):-UInt(2):Ptr{Cvoid}(10) @test isempty(r) @test length(r) == 0 @@ -2494,6 +2492,7 @@ end @test test_firstindex(StepRange{Union{Int64,Int128},Int}(Int64(1), 1, Int128(0))) end +<<<<<<< HEAD <<<<<<< HEAD @testset "PR 49516" begin struct PR49516 <: Signed @@ -2607,41 +2606,58 @@ end @test_throws errmsg range(CartesianIndex(1), step=CartesianIndex(1), length=3) end -@testset "logrange" begin - @test collect(logrange(2, 16, length=4)) == [2, 4, 8, 16] - @test collect(logrange(1000, 1, length=4)) == [1000, 100, 10, 1] - @test collect(logrange(-1, -4, length=3)) == [-1, -2, -4] - @test collect(logrange(1, -1+0im, length=3)) == [1, im, -1] - - @test collect(logrange(1/8, 8.0, length=7)) == [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] - @test collect(logrange(1, 10^10, length=21))[1:2:end] == 10 .^ (0:10) - @test collect(logrange(0.789, 123_456, length=135_790))[[begin, end]] == [0.789, 123_456] - - @test collect(logrange(1, 10, length=3)) isa Vector{Float64} - @test collect(logrange(1, 10, length=Int32(3))) isa Vector{Float64} - @test collect(logrange(1, 10f0, length=3)) isa Vector{Float32} - @test collect(logrange(1f0, 10, length=3)) isa Vector{Float32} - @test collect(logrange(1f0, 10+im, length=3)) isa Vector{ComplexF32} - @test collect(logrange(1f0, 10.0+im, length=3)) isa Vector{ComplexF64} - @test collect(logrange(1, big(10), length=3)) isa Vector{BigFloat} - - @test length(logrange(2, 16, length=4)) == 4 - @test size(logrange(2, 16, length=4)) == (4,) - @test ndims(logrange(2, 16, length=4)) == 1 - @test ndims(typeof(logrange(2, 16, length=4))) == 1 - @test eltype(logrange(2, 16, length=4)) == Float64 - @test eltype(typeof(logrange(2, 16, length=4))) == Float64 - @test Base.IteratorSize(typeof(logrange(2=>3, length=4))) == Base.HasLength() - @test Base.IteratorEltype(typeof(logrange(2, 16, length=4))) == Base.HasEltype() - - @test_throws ArgumentError logrange(1, 10, length=1) # allows only length >= 2 - @test_throws DomainError logrange(1, -1, length=3) # needs complex numbers - - @test collect(logrange(2, 16, ratio=2)) == [2, 4, 8, 16] - @test collect(logrange(1, 10, ratio=sqrt(10))) == [1, sqrt(10)] - @test last(collect(logrange(1, 10, ratio=sqrt(10)))) * sqrt(10) > 10 - @test collect(logrange(1, 10, ratio=prevfloat(sqrt(10)))) ≈ [1, sqrt(10), 10] - - @test collect(logrange(2, 16, length=4, ratio=2)) == [2, 4, 8, 16] # over-constrained - @test_throws ArgumentError @test collect(logrange(2, 16, length=4, ratio=3)) +@testset "LogRange" begin + # basic idea + @test LogRange(2, 16, 4) ≈ [2, 4, 8, 16] + @test LogRange(1/8, 8.0, 7) ≈ [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] + @test LogRange(1000, 1, 4) ≈ [1000, 100, 10, 1] + @test LogRange(1, 10^9, 19)[1:2:end] ≈ 10 .^ (0:9) + + # negative & complex + @test LogRange(-1, -4, 3) == [-1, -2, -4] + @test LogRange(1, -1+0.0im, 3) == [1, im, -1] + @test LogRange(1, -1-0.0im, 3) == [1, -im, -1] + + # endpoints + @test LogRange(0.1f0, 100, 33)[1] === 0.1f0 + @test LogRange(0.789, 123_456, 135_790)[[begin, end]] == [0.789, 123_456] + @test LogRange(nextfloat(0f0), floatmax(Float32), typemax(Int))[end] === floatmax(Float32) + @test LogRange(nextfloat(Float16(0)), floatmax(Float16), 66_000)[end] === floatmax(Float16) + @test first(LogRange(pi, 2pi, 3000)) === LogRange(pi, 2pi, 3000)[1] === Float64(pi) + @test last(LogRange(-0.01, -0.1, 3000)) === last(LogRange(-0.01, -0.1, 3000))[end] === -0.1 + if Int == Int64 + @test LogRange(0.1, 1000, 2^54)[end] === 1000.0 + @test LogRange(-0.1, -1000, 2^55)[end] === -1000.0 + end + + # empty, only, NaN, Inf + @test first(LogRange(1, 2, 0)) === 1.0 + @test last(LogRange(1, 2, 0)) === 2.0 + @test isnan(first(LogRange(0, 2, 0))) + @test only(LogRange(2pi, 2pi, 1)) === LogRange(2pi, 2pi, 1)[1] === 2pi + @test isnan(LogRange(1, NaN, 3)[2]) + @test isinf(LogRange(1, Inf, 3)[2]) + @test isnan(LogRange(0, 2, 3)[1]) + + # types + @test eltype(LogRange(1, 10, 3)) == Float64 + @test eltype(LogRange(1, 10, Int32(3))) == Float64 + @test eltype(LogRange(1, 10f0, 3)) == Float32 + @test eltype(LogRange(1f0, 10, 3)) == Float32 + @test eltype(LogRange(1f0, 10+im, 3)) == ComplexF32 + @test eltype(LogRange(1f0, 10.0+im, 3)) == ComplexF64 + @test eltype(LogRange(1, big(10), 3)) == BigFloat + @test LogRange(big"0.3", big(pi), 50)[1] == big"0.3" + @test LogRange(big"0.3", big(pi), 50)[end] == big(pi) + + # errors + @test_throws ArgumentError LogRange(1, 10, -1) + @test_throws ArgumentError LogRange(1, 10, 1) # endpoints must differ + @test_throws DomainError LogRange(1, -1, 3) # needs complex numbers + @test_throws ArgumentError LogRange(1, 10, 2)[true] + @test_throws BoundsError LogRange(1, 10, 2)[3] + + # printing + @test repr(LogRange(1,2,3)) == "LogRange(1.0, 2.0, 3)" + @test repr("text/plain", LogRange(1,2,3)) == "3-element LogRange{Float64}:\n 1.0, 1.41421, 2.0" end From b04c452ae858c1744e6eb694dccc6f46641a56d2 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Tue, 31 May 2022 12:20:49 -0400 Subject: [PATCH 04/24] example chosen to show tiny errors does not, anymore --- base/range.jl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/base/range.jl b/base/range.jl index 48a9c36068596..fa18ad7c5a4b1 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1517,22 +1517,22 @@ julia> LogRange(10, 4000, 3) julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean true -julia> LogRange(-250_000, -0.04, 5) |> collect +julia> LogRange(-62_500, -0.01, 5) |> collect # endpoints are always exact 5-element Vector{Float64}: - -250000.0 - -5000.000000000003 - -100.0 - -1.9999999999999998 - -0.04 + -62500.0 + -1250.0 + -25.0 + -0.49999999999999994 + -0.01 julia> LogRange(√2, 32, 10) 10-element LogRange{Float64}: 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 -julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) +julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) # was logspace(0,3,4) in Julia < 1.0 true -julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) +julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) # complex numbers true ``` """ From 220e4388103258c2dbc2847c21fdbaf2b6d58fae Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:26:09 -0500 Subject: [PATCH 05/24] Apply 3 suggestions Co-authored-by: Lilith Orion Hafner --- base/range.jl | 4 ++-- test/ranges.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/base/range.jl b/base/range.jl index fa18ad7c5a4b1..69dde4213c5f1 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1529,7 +1529,7 @@ julia> LogRange(√2, 32, 10) 10-element LogRange{Float64}: 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 -julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) # was logspace(0,3,4) in Julia < 1.0 +julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) true julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) # complex numbers @@ -1549,7 +1549,7 @@ struct LogRange{T} <: AbstractArray{T,1} elseif len == 0 return new{T}(lo, hi, len) elseif len == 1 && start != stop - throw(ArgumentError("endpoints of LogRange differ, with length = 1")) + throw(ArgumentError("LogRange($start, $stop, $len): endpoints differ")) end if T <: Real && (start<0) ⊻ (stop<0) throw(DomainError((start, stop), diff --git a/test/ranges.jl b/test/ranges.jl index 8663c01bc9309..837d2ac1433ae 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2652,7 +2652,7 @@ end # errors @test_throws ArgumentError LogRange(1, 10, -1) - @test_throws ArgumentError LogRange(1, 10, 1) # endpoints must differ + @test_throws ArgumentError LogRange(1, 10, 1) # endpoints must not differ @test_throws DomainError LogRange(1, -1, 3) # needs complex numbers @test_throws ArgumentError LogRange(1, 10, 2)[true] @test_throws BoundsError LogRange(1, 10, 2)[3] From ca9aec7d994961e4754d99e404941213db6109e0 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Tue, 8 Nov 2022 13:37:59 -0500 Subject: [PATCH 06/24] rm unsafe_getindex, use LazyString --- base/range.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/base/range.jl b/base/range.jl index 69dde4213c5f1..a400410b5a93f 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1545,11 +1545,13 @@ struct LogRange{T} <: AbstractArray{T,1} lo = iszero(start) ? T(NaN) : T(start) hi = iszero(stop) ? T(NaN) : T(stop) if len < 0 - throw(ArgumentError("LogRange can't have negative length")) + throw(ArgumentError(LazyString( + "LogRange(", start, ", ", stop, ", ", len, "): can't have negative length"))) elseif len == 0 return new{T}(lo, hi, len) elseif len == 1 && start != stop - throw(ArgumentError("LogRange($start, $stop, $len): endpoints differ")) + throw(ArgumentError(LazyString( + "LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1"))) end if T <: Real && (start<0) ⊻ (stop<0) throw(DomainError((start, stop), @@ -1564,14 +1566,9 @@ size(r::LogRange) = (r.len,) first(r::LogRange) = r.start last(r::LogRange) = r.stop -function getindex(r::LogRange, i::Integer) +function getindex(r::LogRange, i::Int) @inline @boundscheck checkbounds(r, i) - unsafe_getindex(r, i) -end - -function unsafe_getindex(r::LogRange, i::Integer) - i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) if r.len == 1 return r.start end From b0091cd0482d48215ff91dc7e8dd8cf212318446 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Tue, 8 Nov 2022 15:32:16 -0500 Subject: [PATCH 07/24] allow super-Float64 precision in powers --- base/range.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/base/range.jl b/base/range.jl index a400410b5a93f..ad4c0de0d428a 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1573,15 +1573,16 @@ function getindex(r::LogRange, i::Int) return r.start end if eltype(r) <: Real && r.start < 0 # constructor guarantees r.stop < 0 too - -loginterp(-r.start, -r.stop, Int(i), r.len) + -loginterp(-r.start, -r.stop, i, r.len) else - loginterp(r.start, r.stop, Int(i), r.len) + loginterp(r.start, r.stop, i, r.len) end end function loginterp(lo::T, hi::T, j::Int, n::Int) where {T} @inline - convert(T, lo^((n-j)/(n-1)) * hi^((j-1)/(n-1))) + S = promote_type(Float64, T) + convert(T, lo^(S(n-j)/S(n-1)) * hi^(S(j-1)/S(n-1))) end function show(io::IO, r::LogRange) # compact From 3d75211e13a862c78a117e5df34fc58b13d1fa4d Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Wed, 9 Nov 2022 14:52:43 -0500 Subject: [PATCH 08/24] new implementation, using Math.exp_impl etc --- base/range.jl | 96 +++++++++++++++++++++++++++++++------------------- test/ranges.jl | 25 ++++++++++--- 2 files changed, 80 insertions(+), 41 deletions(-) diff --git a/base/range.jl b/base/range.jl index ad4c0de0d428a..802a8f7f45015 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1491,102 +1491,124 @@ mod(i::Integer, r::AbstractUnitRange{<:Integer}) = mod(i-first(r), length(r)) + """ - LogRange(start, stop, length) + LogRange(start, stop, length) <: AbstractVector Construct a specialized array whose elements are spaced logarithmically between the given endpoints. That is, the ratio of successive elements is a constant, calculated from the length. -Like [`LinRange`](@ref), floating-point error may cause it to miss intermediate -rational values, but the first and last elements should be exactly those provided. +Like [`LinRange`](@ref), the first and last elements will be exactly those +provided, but intermediate values may have small floating-point errors +(especially for complex numbers). This is similar to `geomspace` in Python. Unlike `PowerRange` in Mathematica, you specify the number of elements not the ratio. Unlike `logspace` in Python and Matlab, the `start` and `stop` arguments are always the first and last elements of the result, not powers applied to some base. -!!! compat "Julia 1.9" - This function requires at least Julia 1.9. +!!! compat "Julia 1.10" + This function requires at least Julia 1.10. # Examples ```jldoctest julia> LogRange(10, 4000, 3) -3-element LogRange{Float64}: +3-element LogRange{Float64, Base.TwicePrecision{Float64}}: 10.0, 200.0, 4000.0 julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean true -julia> LogRange(-62_500, -0.01, 5) |> collect # endpoints are always exact -5-element Vector{Float64}: - -62500.0 - -1250.0 - -25.0 - -0.49999999999999994 - -0.01 - julia> LogRange(√2, 32, 10) -10-element LogRange{Float64}: +10-element LogRange{Float64, Base.TwicePrecision{Float64}}: 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) true +julia> LogRange(-27, -3f0, 5) # allows negative numbers +5-element LogRange{Float32, Float64}: + -27.0, -15.5885, -9.0, -5.19615, -3.0 + julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) # complex numbers true ``` """ -struct LogRange{T} <: AbstractArray{T,1} +struct LogRange{T,X} <: AbstractArray{T,1} start::T stop::T len::Int - function LogRange(start::T1, stop::T2, len::Integer) where {T1<:Number, T2<:Number} - T = float(promote_type(T1, T2)) - lo = iszero(start) ? T(NaN) : T(start) - hi = iszero(stop) ? T(NaN) : T(stop) + extra::Tuple{X,X} + function LogRange(start::Number, stop::Number, length::Integer) + T = float(promote_type(typeof(start), typeof(stop))) + a = iszero(start) ? T(NaN) : T(start) + b = iszero(stop) ? T(NaN) : T(stop) + len = Int(length) if len < 0 throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): can't have negative length"))) - elseif len == 0 - return new{T}(lo, hi, len) elseif len == 1 && start != stop throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1"))) - end - if T <: Real && (start<0) ⊻ (stop<0) + elseif iszero(start) || iszero(stop) + elseif T <: Real && (start<0) ⊻ (stop<0) throw(DomainError((start, stop), "LogRange will only return complex results if called with a complex argument")) end - new{T}(lo, hi, len) + ex = if T <: Real && start + stop < 0 # start+stop allows for LogRange(-0.0, -2, 3) + _logrange_extra(-a, -b, len) + else + _logrange_extra(a, b, len) + end + new{T,typeof(ex[1])}(a, b, len, ex) end end size(r::LogRange) = (r.len,) +length(r::LogRange) = r.len first(r::LogRange) = r.start last(r::LogRange) = r.stop +function _logrange_extra(a::Number, b::Number, len::Int) + loga = log(1.0 * a) # widen to at least Float64 + logb = log(1.0 * b) + (loga/(len-1), logb/(len-1)) +end +function _logrange_extra(a::Float64, b::Float64, len::Int) + loga = TwicePrecision(Math._log_ext(reinterpret(UInt64, a))...) + logb = TwicePrecision(Math._log_ext(reinterpret(UInt64, b))...) + # The reason not to do linear interpolation on log(a)..log(b) in `getindex` + # is that division is quite slow, so we do it once on construction: + (loga/(len-1), logb/(len-1)) +end + function getindex(r::LogRange, i::Int) @inline @boundscheck checkbounds(r, i) - if r.len == 1 - return r.start - end - if eltype(r) <: Real && r.start < 0 # constructor guarantees r.stop < 0 too - -loginterp(-r.start, -r.stop, i, r.len) - else - loginterp(r.start, r.stop, i, r.len) - end + i == 1 && return r.start + i == r.len && return r.stop + # `unsafe_getindex` has almost perfect accuracy for Float32 and Float64, but not + # guaranteed, and not for ComplexF64. Hence the explicit start/stop cases. + return unsafe_getindex(r, i) end -function loginterp(lo::T, hi::T, j::Int, n::Int) where {T} +function unsafe_getindex(r::LogRange{T}, i::Int) where {T} + @inline + logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] + x = T(exp(logx)) + T <: Real ? copysign(x, r.start) : x +end +function unsafe_getindex(r::LogRange{Float64, TwicePrecision{Float64}}, i::Int) @inline - S = promote_type(Float64, T) - convert(T, lo^(S(n-j)/S(n-1)) * hi^(S(j-1)/S(n-1))) + tot = r.start + r.stop + isfinite(tot) || return tot + logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] + x = Math.exp_impl(logx.hi, logx.lo, Val(:ℯ))::Float64 + return copysign(x, r.start) end function show(io::IO, r::LogRange) # compact - print(io, "LogRange(") + print(io, "LogRange(") # type parameters are implied by knowing r.start show(io, first(r)) print(io, ", ") show(io, last(r)) diff --git a/test/ranges.jl b/test/ranges.jl index 837d2ac1433ae..fd994cd351fc6 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2615,8 +2615,8 @@ end # negative & complex @test LogRange(-1, -4, 3) == [-1, -2, -4] - @test LogRange(1, -1+0.0im, 3) == [1, im, -1] - @test LogRange(1, -1-0.0im, 3) == [1, -im, -1] + @test LogRange(1, -1+0.0im, 3) ≈ [1, im, -1] + @test LogRange(1, -1-0.0im, 3) ≈ [1, -im, -1] # endpoints @test LogRange(0.1f0, 100, 33)[1] === 0.1f0 @@ -2633,11 +2633,28 @@ end # empty, only, NaN, Inf @test first(LogRange(1, 2, 0)) === 1.0 @test last(LogRange(1, 2, 0)) === 2.0 + @test collect(LogRange(1, 2, 0)) == Float64[] @test isnan(first(LogRange(0, 2, 0))) @test only(LogRange(2pi, 2pi, 1)) === LogRange(2pi, 2pi, 1)[1] === 2pi @test isnan(LogRange(1, NaN, 3)[2]) - @test isinf(LogRange(1, Inf, 3)[2]) + @test isnan(LogRange(NaN, 2, 3)[2]) + @test isnan(LogRange(1f0, NaN32, 3)[2]) + @test isnan(LogRange(NaN32, 2f0, 3)[2]) @test isnan(LogRange(0, 2, 3)[1]) + @test isnan(LogRange(0, -2, 3)[1]) + @test isnan(LogRange(-0.0, +2.0, 3)[1]) + @test isnan(LogRange(0f0, 2f0, 3)[1]) + @test isnan(LogRange(0f0, -2f0, 3)[1]) + @test isnan(LogRange(-0f0, 2f0, 3)[1]) + @test isinf(LogRange(1, Inf, 3)[2]) + @test -Inf === LogRange(-1, -Inf, 3)[2] + @test isinf(LogRange(1f0, Inf32, 3)[2]) + @test -Inf32 === LogRange(-1f0, -Inf32, 3)[2] + # constant + @test LogRange(1, 1, 3) == fill(1.0, 3) + @test LogRange(-1f0, -1f0, 3) == fill(-1f0, 3) + @test all(isnan, LogRange(0.0, -0.0, 3)) + @test all(isnan, LogRange(-0f0, 0f0, 3)) # types @test eltype(LogRange(1, 10, 3)) == Float64 @@ -2659,5 +2676,5 @@ end # printing @test repr(LogRange(1,2,3)) == "LogRange(1.0, 2.0, 3)" - @test repr("text/plain", LogRange(1,2,3)) == "3-element LogRange{Float64}:\n 1.0, 1.41421, 2.0" + @test repr("text/plain", LogRange(1,2,3)) == "3-element LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" end From d96bffc22e62467ec62dd4a10a895f8301d8c8d7 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sat, 12 Nov 2022 07:32:38 -0500 Subject: [PATCH 09/24] use _log_twice64_unchecked to handle subnormals --- base/range.jl | 10 +++++----- base/twiceprecision.jl | 18 ++++++++++++++++++ test/ranges.jl | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/base/range.jl b/base/range.jl index 802a8f7f45015..ca9219b7051c5 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1575,8 +1575,8 @@ function _logrange_extra(a::Number, b::Number, len::Int) (loga/(len-1), logb/(len-1)) end function _logrange_extra(a::Float64, b::Float64, len::Int) - loga = TwicePrecision(Math._log_ext(reinterpret(UInt64, a))...) - logb = TwicePrecision(Math._log_ext(reinterpret(UInt64, b))...) + loga = _log_twice64_unchecked(a) + logb = _log_twice64_unchecked(b) # The reason not to do linear interpolation on log(a)..log(b) in `getindex` # is that division is quite slow, so we do it once on construction: (loga/(len-1), logb/(len-1)) @@ -1598,13 +1598,13 @@ function unsafe_getindex(r::LogRange{T}, i::Int) where {T} x = T(exp(logx)) T <: Real ? copysign(x, r.start) : x end -function unsafe_getindex(r::LogRange{Float64, TwicePrecision{Float64}}, i::Int) +function unsafe_getindex(r::LogRange{T, TwicePrecision{Float64}}, i::Int) where T @inline tot = r.start + r.stop isfinite(tot) || return tot logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] - x = Math.exp_impl(logx.hi, logx.lo, Val(:ℯ))::Float64 - return copysign(x, r.start) + x = Math.exp_impl(logx.hi, logx.lo, Val(:ℯ)) + return copysign(T(x), r.start) end function show(io::IO, r::LogRange) # compact diff --git a/base/twiceprecision.jl b/base/twiceprecision.jl index 955bfc97b16ff..590a1954d2735 100644 --- a/base/twiceprecision.jl +++ b/base/twiceprecision.jl @@ -785,3 +785,21 @@ _tp_prod(t::TwicePrecision) = t x.hi < y.hi || ((x.hi == y.hi) & (x.lo < y.lo)) isbetween(a, x, b) = a <= x <= b || b <= x <= a + +# For NaN/Inf/negative this returns junk +function _log_twice64_unchecked(x::Float64) + xu = reinterpret(UInt64, x) + if xu < (UInt64(1)<<52) # x is subnormal + xu = reinterpret(UInt64, x * 0x1p52) # normalize x + xu &= ~sign_mask(Float64) + xu -= UInt64(52) << 52 # mess with the exponent + end + TwicePrecision(Math._log_ext(xu)...) +end + +function _log_twice64(x::Float64) + isfinite(x) || return TwicePrecision(x, x) + _log_twice64_unchecked(x) +end + +_exp_float64(x::TwicePrecision{Float64}) = Math.exp_impl(x.hi, x.lo, Val(:ℯ)) diff --git a/test/ranges.jl b/test/ranges.jl index fd994cd351fc6..cd4eccd4b7ecb 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2656,6 +2656,10 @@ end @test all(isnan, LogRange(0.0, -0.0, 3)) @test all(isnan, LogRange(-0f0, 0f0, 3)) + # subnormal Float64 + x = LogRange(1e-320, 1e-300, 21) .* 1e300 + @test x ≈ LogRange(1e-20, 1, 21) rtol=1e-6 + # types @test eltype(LogRange(1, 10, 3)) == Float64 @test eltype(LogRange(1, 10, Int32(3))) == Float64 @@ -2678,3 +2682,22 @@ end @test repr(LogRange(1,2,3)) == "LogRange(1.0, 2.0, 3)" @test repr("text/plain", LogRange(1,2,3)) == "3-element LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" end + +@testset "_log_twice64_unchecked" begin + # it roughly works + @test big(Base._log_twice64_unchecked(exp(1))) ≈ 1.0 + @test big(Base._log_twice64_unchecked(exp(123))) ≈ 123.0 + + # it gets high accuracy + @test abs(big(log(4.0)) - log(big(4.0))) < 1e-16 + @test abs(big(Base._log_twice64_unchecked(4.0)) - log(big(4.0))) < 1e-30 + + # it handles subnormals + @test abs(big(Base._log_twice64_unchecked(1e-310)) - log(big(1e-310))) < 1e-20 + + # it accepts negative, NaN, etc without complaint: + @test Base._log_twice64_unchecked(-0.0).lo isa Float64 + @test Base._log_twice64_unchecked(-1.23).lo isa Float64 + @test Base._log_twice64_unchecked(NaN).lo isa Float64 + @test Base._log_twice64_unchecked(Inf).lo isa Float64 +end From 29c2050f5b144377a15ac6135369351d6b303e43 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sat, 12 Nov 2022 20:40:53 -0500 Subject: [PATCH 10/24] make LogRange{T} constructor work, like LinRange{T} --- base/range.jl | 30 +++++++++++++++++++++--------- test/ranges.jl | 1 + 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/base/range.jl b/base/range.jl index ca9219b7051c5..bb123984bbe7d 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1533,13 +1533,12 @@ julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) # complex numbers true ``` """ -struct LogRange{T,X} <: AbstractArray{T,1} +struct LogRange{T<:Number,X} <: AbstractArray{T,1} start::T stop::T len::Int extra::Tuple{X,X} - function LogRange(start::Number, stop::Number, length::Integer) - T = float(promote_type(typeof(start), typeof(stop))) + function LogRange{T}(start::T, stop::T, length::Int) where {T<:Number} a = iszero(start) ? T(NaN) : T(start) b = iszero(stop) ? T(NaN) : T(stop) len = Int(length) @@ -1554,6 +1553,10 @@ struct LogRange{T,X} <: AbstractArray{T,1} throw(DomainError((start, stop), "LogRange will only return complex results if called with a complex argument")) end + if T <: Integer || T <: Complex{<:Integer} + # LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998) + throw(ArgumentError("LogRange{T} does not support integer types")) + end ex = if T <: Real && start + stop < 0 # start+stop allows for LogRange(-0.0, -2, 3) _logrange_extra(-a, -b, len) else @@ -1563,6 +1566,14 @@ struct LogRange{T,X} <: AbstractArray{T,1} end end +function LogRange{T}(start::Number, stop::Number, len::Integer) where {T} + LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len)) +end +function LogRange(start::Number, stop::Number, len::Integer) + T = float(promote_type(typeof(start), typeof(stop))) + LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len)) +end + size(r::LogRange) = (r.len,) length(r::LogRange) = r.len @@ -1595,8 +1606,8 @@ end function unsafe_getindex(r::LogRange{T}, i::Int) where {T} @inline logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] - x = T(exp(logx)) - T <: Real ? copysign(x, r.start) : x + x = exp(logx) + T <: Real ? copysign(T(x), r.start) : T(x) end function unsafe_getindex(r::LogRange{T, TwicePrecision{Float64}}, i::Int) where T @inline @@ -1607,11 +1618,12 @@ function unsafe_getindex(r::LogRange{T, TwicePrecision{Float64}}, i::Int) where return copysign(T(x), r.start) end -function show(io::IO, r::LogRange) # compact - print(io, "LogRange(") # type parameters are implied by knowing r.start - show(io, first(r)) +function show(io::IO, r::LogRange{T}) where {T} + print(io, "LogRange{", T, "}(") + ioc = IOContext(io, :typeinfo => T) + show(ioc, first(r)) print(io, ", ") - show(io, last(r)) + show(ioc, last(r)) print(io, ", ") show(io, length(r)) print(io, ')') diff --git a/test/ranges.jl b/test/ranges.jl index cd4eccd4b7ecb..b0811d26ee13f 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2677,6 +2677,7 @@ end @test_throws DomainError LogRange(1, -1, 3) # needs complex numbers @test_throws ArgumentError LogRange(1, 10, 2)[true] @test_throws BoundsError LogRange(1, 10, 2)[3] + @test_throws ArgumentError LogRange{Int}(1,4,5) # no integer ranges # printing @test repr(LogRange(1,2,3)) == "LogRange(1.0, 2.0, 3)" From c4edccda0c6a148e02fd1cc69b4b4820c0abd15f Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sat, 12 Nov 2022 20:42:29 -0500 Subject: [PATCH 11/24] lower-case logrange, as a friendlier interface? --- base/exports.jl | 1 + base/range.jl | 95 ++++++++++++++++++++++++++++++++++++-------- doc/src/base/math.md | 2 + test/ranges.jl | 8 +++- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/base/exports.jl b/base/exports.jl index b72075d09793a..5f5480db091f9 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -413,6 +413,7 @@ export isperm, issorted, last, + logrange, mapslices, max, maximum!, diff --git a/base/range.jl b/base/range.jl index bb123984bbe7d..c7bf4a20120d2 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1491,47 +1491,110 @@ mod(i::Integer, r::AbstractUnitRange{<:Integer}) = mod(i-first(r), length(r)) + """ - LogRange(start, stop, length) <: AbstractVector + logrange(start, stop, length) + logrange(start, stop; length) Construct a specialized array whose elements are spaced logarithmically between the given endpoints. That is, the ratio of successive elements is a constant, calculated from the length. -Like [`LinRange`](@ref), the first and last elements will be exactly those -provided, but intermediate values may have small floating-point errors -(especially for complex numbers). - This is similar to `geomspace` in Python. Unlike `PowerRange` in Mathematica, you specify the number of elements not the ratio. Unlike `logspace` in Python and Matlab, the `start` and `stop` arguments are always the first and last elements of the result, not powers applied to some base. -!!! compat "Julia 1.10" - This function requires at least Julia 1.10. +See also [`range`](@ref) for linearly spaced points. # Examples ```jldoctest -julia> LogRange(10, 4000, 3) +julia> logrange(10, 4000, length=3) 3-element LogRange{Float64, Base.TwicePrecision{Float64}}: 10.0, 200.0, 4000.0 julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean true -julia> LogRange(√2, 32, 10) -10-element LogRange{Float64, Base.TwicePrecision{Float64}}: - 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 +julia> range(10, 40, length=3)[2] ≈ (10 + 40)/2 # arithmetic mean +true + +julia> logrange(1f0, 32f0, 11) +11-element LogRange{Float32, Float64}: + 1.0, 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 -julia> LogRange(1, 1000, 4) ≈ 10 .^ (0:3) +julia> logrange(1, 1000, length=4) ≈ 10 .^ (0:3) true -julia> LogRange(-27, -3f0, 5) # allows negative numbers -5-element LogRange{Float32, Float64}: - -27.0, -15.5885, -9.0, -5.19615, -3.0 +julia> logrange(-27, -3, length=7) # allows negative numbers +7-element LogRange{Float64, Base.TwicePrecision{Float64}}: + -27.0, -18.7208, -12.9802, -9.0, -6.24025, -4.32675, -3.0 +``` + +!!! compat "Julia 1.10" + This function requires at least Julia 1.10. +""" +logrange(start::Number, stop::Number, length::Integer) = LogRange(start, stop, Int(length)) +logrange(start::Number, stop::Number; length::Integer) = LogRange(start, stop, Int(length)) + + +""" + LogRange{T}(start, stop, len) <: AbstractVector{T} -julia> LogRange(1, -1+0im, 5) ≈ cis.(LinRange(0, pi, 5)) # complex numbers +A range whose elements are spaced logarithmically between `start` and `stop`, +with spacing controlled by `len`. Returned by [`logrange`](@ref). + +Like [`LinRange`](@ref), the first and last elements will be exactly those +provided, but intermediate values may have small floating-point errors. +These are calculated using the logs of the endpoints, which are +stored on construction, often in higher precision than `T`. + +Negative values of `start` and `stop` are allowed, but both must have the +same sign. For complex `T`, all points lie on the same branch of `log` +as used by `log(start)` and `log(stop)`. + +# Examples +```jldoctest +julia> LogRange(1, 4, 5) +5-element LogRange{Float64, Base.TwicePrecision{Float64}}: + 1.0, 1.41421, 2.0, 2.82843, 4.0 + +julia> LogRange{Float16}(-1, -4, 5) +5-element LogRange{Float16, Float64}: + -1.0, -1.414, -2.0, -2.828, -4.0 + +julia> LogRange(1e-310, 1e-300, 11)[1:2:end] |> collect +6-element Vector{Float64}: + 1.0e-310 + 9.999999999999974e-309 + 9.999999999999981e-307 + 9.999999999999988e-305 + 9.999999999999994e-303 + 1.0e-300 + +julia> prevfloat(1e-308, 5) == ans[2] true + +julia> LogRange(1, -1 +0.0im, 5) |> collect # does not hit 1.0im exactly +5-element Vector{ComplexF64}: + 1.0 + 0.0im + 0.7071067811865476 + 0.7071067811865475im + 6.123233995736766e-17 + 1.0im + -0.7071067811865475 + 0.7071067811865476im + -1.0 + 0.0im + +julia> ans ≈ cis.(LinRange(0, pi, 5)) +true + +julia> LogRange(2, Inf, 5) +5-element LogRange{Float64, Base.TwicePrecision{Float64}}: + 2.0, Inf, Inf, Inf, Inf + +julia> LogRange(0, 4, 5) +5-element LogRange{Float64, Base.TwicePrecision{Float64}}: + NaN, NaN, NaN, NaN, 4.0 ``` + +!!! compat "Julia 1.10" + This type requires at least Julia 1.10. """ struct LogRange{T<:Number,X} <: AbstractArray{T,1} start::T diff --git a/doc/src/base/math.md b/doc/src/base/math.md index 86a7e4d8d3173..c1b18c0b66d86 100644 --- a/doc/src/base/math.md +++ b/doc/src/base/math.md @@ -39,6 +39,8 @@ Base.:(:) Base.range Base.OneTo Base.StepRangeLen +Base.logrange +Base.LogRange Base.:(==) Base.:(!=) Base.:(!==) diff --git a/test/ranges.jl b/test/ranges.jl index b0811d26ee13f..f9cb4e43f824b 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2608,9 +2608,9 @@ end @testset "LogRange" begin # basic idea - @test LogRange(2, 16, 4) ≈ [2, 4, 8, 16] + @test logrange(2, 16, 4) ≈ [2, 4, 8, 16] @test LogRange(1/8, 8.0, 7) ≈ [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] - @test LogRange(1000, 1, 4) ≈ [1000, 100, 10, 1] + @test logrange(1000, 1, 4) ≈ [1000, 100, 10, 1] @test LogRange(1, 10^9, 19)[1:2:end] ≈ 10 .^ (0:9) # negative & complex @@ -2671,6 +2671,10 @@ end @test LogRange(big"0.3", big(pi), 50)[1] == big"0.3" @test LogRange(big"0.3", big(pi), 50)[end] == big(pi) + # more constructors + @test logrange(1,2,3) === LogRange(1,2,3) == LogRange{Float64}(1,2,3) + @test logrange(1f0, 2f0, length=3) == LogRange{Float32}(1,2,3) + # errors @test_throws ArgumentError LogRange(1, 10, -1) @test_throws ArgumentError LogRange(1, 10, 1) # endpoints must not differ From 4a735e11ac45e8dbdcf724ee6c0589b5cbf76ddc Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:01:18 -0500 Subject: [PATCH 12/24] tidy up, rm unsafe_getindex, add _exp_allowing_twice64 --- base/range.jl | 31 +++++++++++-------------------- base/twiceprecision.jl | 14 ++++++-------- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/base/range.jl b/base/range.jl index c7bf4a20120d2..91c6f04525e4e 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1561,7 +1561,7 @@ julia> LogRange{Float16}(-1, -4, 5) 5-element LogRange{Float16, Float64}: -1.0, -1.414, -2.0, -2.828, -4.0 -julia> LogRange(1e-310, 1e-300, 11)[1:2:end] |> collect +julia> LogRange(1e-310, 1e-300, 11)[1:2:end] 6-element Vector{Float64}: 1.0e-310 9.999999999999974e-309 @@ -1573,7 +1573,7 @@ julia> LogRange(1e-310, 1e-300, 11)[1:2:end] |> collect julia> prevfloat(1e-308, 5) == ans[2] true -julia> LogRange(1, -1 +0.0im, 5) |> collect # does not hit 1.0im exactly +julia> LogRange(1, -1 +0.0im, 5) 5-element Vector{ComplexF64}: 1.0 + 0.0im 0.7071067811865476 + 0.7071067811865475im @@ -1602,6 +1602,8 @@ struct LogRange{T<:Number,X} <: AbstractArray{T,1} len::Int extra::Tuple{X,X} function LogRange{T}(start::T, stop::T, length::Int) where {T<:Number} + # LogRange(0, 1, 100) could be == [0,0,0,0,...,1], that's the limit start -> 0, + # but seems more likely to give silent surprises than returning NaN. a = iszero(start) ? T(NaN) : T(start) b = iszero(stop) ? T(NaN) : T(stop) len = Int(length) @@ -1651,34 +1653,23 @@ end function _logrange_extra(a::Float64, b::Float64, len::Int) loga = _log_twice64_unchecked(a) logb = _log_twice64_unchecked(b) - # The reason not to do linear interpolation on log(a)..log(b) in `getindex` - # is that division is quite slow, so we do it once on construction: + # The reason not to do linear interpolation on log(a)..log(b) in `getindex` is + # that division of TwicePrecision is quite slow, so do it once on construction: (loga/(len-1), logb/(len-1)) end -function getindex(r::LogRange, i::Int) +function getindex(r::LogRange{T}, i::Int) where {T} @inline @boundscheck checkbounds(r, i) i == 1 && return r.start i == r.len && return r.stop - # `unsafe_getindex` has almost perfect accuracy for Float32 and Float64, but not - # guaranteed, and not for ComplexF64. Hence the explicit start/stop cases. - return unsafe_getindex(r, i) -end - -function unsafe_getindex(r::LogRange{T}, i::Int) where {T} - @inline - logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] - x = exp(logx) - T <: Real ? copysign(T(x), r.start) : T(x) -end -function unsafe_getindex(r::LogRange{T, TwicePrecision{Float64}}, i::Int) where T - @inline tot = r.start + r.stop isfinite(tot) || return tot + # Main path uses Math.exp_impl for TwicePrecision, but is not perfectly + # accurate, nor does it handle NaN/Inf as desired, hence the cases above. logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] - x = Math.exp_impl(logx.hi, logx.lo, Val(:ℯ)) - return copysign(T(x), r.start) + x = _exp_allowing_twice64(logx) + return T <: Real ? copysign(T(x), r.start) : T(x) end function show(io::IO, r::LogRange{T}) where {T} diff --git a/base/twiceprecision.jl b/base/twiceprecision.jl index 590a1954d2735..0de6270cafb2d 100644 --- a/base/twiceprecision.jl +++ b/base/twiceprecision.jl @@ -786,7 +786,12 @@ _tp_prod(t::TwicePrecision) = t isbetween(a, x, b) = a <= x <= b || b <= x <= a -# For NaN/Inf/negative this returns junk +# These functions exist for use in LogRange: + +_exp_allowing_twice64(x::Number) = exp(x) +_exp_allowing_twice64(x::TwicePrecision{Float64}) = Math.exp_impl(x.hi, x.lo, Val(:ℯ)) + +# No error on negative x, and for NaN/Inf this returns junk: function _log_twice64_unchecked(x::Float64) xu = reinterpret(UInt64, x) if xu < (UInt64(1)<<52) # x is subnormal @@ -796,10 +801,3 @@ function _log_twice64_unchecked(x::Float64) end TwicePrecision(Math._log_ext(xu)...) end - -function _log_twice64(x::Float64) - isfinite(x) || return TwicePrecision(x, x) - _log_twice64_unchecked(x) -end - -_exp_float64(x::TwicePrecision{Float64}) = Math.exp_impl(x.hi, x.lo, Val(:ℯ)) From 530a5c6fbc0866f2c08ec04b01615b35c1216a09 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:26:19 -0500 Subject: [PATCH 13/24] rm some doc lines --- doc/src/base/collections.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/doc/src/base/collections.md b/doc/src/base/collections.md index 3ea37d949ab00..9d68d55be3459 100644 --- a/doc/src/base/collections.md +++ b/doc/src/base/collections.md @@ -48,7 +48,6 @@ Fully implemented by: * [`Set`](@ref) * [`Pair`](@ref) * [`NamedTuple`](@ref) - * [`LogRange`](@ref) ## Constructors and Types @@ -59,7 +58,6 @@ Base.AbstractUnitRange Base.StepRange Base.UnitRange Base.LinRange -Base.LogRange ``` ## General Collections From bb23185db5a35b466e1be96a74c6ead6127b3f24 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Fri, 18 Nov 2022 16:54:26 -0500 Subject: [PATCH 14/24] fix doc/repr tests --- base/range.jl | 24 ++++++++++++------------ test/ranges.jl | 4 +--- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/base/range.jl b/base/range.jl index 91c6f04525e4e..af4e08b4a166e 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1529,8 +1529,8 @@ julia> logrange(-27, -3, length=7) # allows negative numbers -27.0, -18.7208, -12.9802, -9.0, -6.24025, -4.32675, -3.0 ``` -!!! compat "Julia 1.10" - This function requires at least Julia 1.10. +!!! compat "Julia 1.11" + This function requires at least Julia 1.11. """ logrange(start::Number, stop::Number, length::Integer) = LogRange(start, stop, Int(length)) logrange(start::Number, stop::Number; length::Integer) = LogRange(start, stop, Int(length)) @@ -1573,15 +1573,15 @@ julia> LogRange(1e-310, 1e-300, 11)[1:2:end] julia> prevfloat(1e-308, 5) == ans[2] true -julia> LogRange(1, -1 +0.0im, 5) -5-element Vector{ComplexF64}: - 1.0 + 0.0im - 0.7071067811865476 + 0.7071067811865475im - 6.123233995736766e-17 + 1.0im - -0.7071067811865475 + 0.7071067811865476im - -1.0 + 0.0im +julia> LogRange{ComplexF32}(1, -1 +0.0im, 5) |> collect +5-element Vector{ComplexF32}: + 1.0f0 + 0.0f0im + 0.70710677f0 + 0.70710677f0im + 6.123234f-17 + 1.0f0im + -0.70710677f0 + 0.70710677f0im + -1.0f0 + 0.0f0im -julia> ans ≈ cis.(LinRange(0, pi, 5)) +julia> ans ≈ cis.(LinRange{Float32}(0, pi, 5)) true julia> LogRange(2, Inf, 5) @@ -1593,8 +1593,8 @@ julia> LogRange(0, 4, 5) NaN, NaN, NaN, NaN, 4.0 ``` -!!! compat "Julia 1.10" - This type requires at least Julia 1.10. +!!! compat "Julia 1.11" + This type requires at least Julia 1.11. """ struct LogRange{T<:Number,X} <: AbstractArray{T,1} start::T diff --git a/test/ranges.jl b/test/ranges.jl index f9cb4e43f824b..cef51301a9fa3 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2492,8 +2492,6 @@ end @test test_firstindex(StepRange{Union{Int64,Int128},Int}(Int64(1), 1, Int128(0))) end -<<<<<<< HEAD -<<<<<<< HEAD @testset "PR 49516" begin struct PR49516 <: Signed n::Int @@ -2684,7 +2682,7 @@ end @test_throws ArgumentError LogRange{Int}(1,4,5) # no integer ranges # printing - @test repr(LogRange(1,2,3)) == "LogRange(1.0, 2.0, 3)" + @test repr(LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" @test repr("text/plain", LogRange(1,2,3)) == "3-element LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" end From 07ae3047f0d5f65c1a053ae0b1262d95bc72dc5a Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:17:36 -0500 Subject: [PATCH 15/24] don't export LogRange type --- base/exports.jl | 1 - base/range.jl | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/base/exports.jl b/base/exports.jl index 5f5480db091f9..e97abd1003591 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -58,7 +58,6 @@ export IOBuffer, IOStream, LinRange, - LogRange, Irrational, LazyString, Matrix, diff --git a/base/range.jl b/base/range.jl index af4e08b4a166e..264dfbff45d64 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1508,7 +1508,7 @@ See also [`range`](@ref) for linearly spaced points. # Examples ```jldoctest julia> logrange(10, 4000, length=3) -3-element LogRange{Float64, Base.TwicePrecision{Float64}}: +3-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: 10.0, 200.0, 4000.0 julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean @@ -1518,14 +1518,14 @@ julia> range(10, 40, length=3)[2] ≈ (10 + 40)/2 # arithmetic mean true julia> logrange(1f0, 32f0, 11) -11-element LogRange{Float32, Float64}: +11-element Base.LogRange{Float32, Float64}: 1.0, 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0 julia> logrange(1, 1000, length=4) ≈ 10 .^ (0:3) true julia> logrange(-27, -3, length=7) # allows negative numbers -7-element LogRange{Float64, Base.TwicePrecision{Float64}}: +7-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: -27.0, -18.7208, -12.9802, -9.0, -6.24025, -4.32675, -3.0 ``` @@ -1553,15 +1553,15 @@ as used by `log(start)` and `log(stop)`. # Examples ```jldoctest -julia> LogRange(1, 4, 5) +julia> logrange(1, 4, 5) 5-element LogRange{Float64, Base.TwicePrecision{Float64}}: 1.0, 1.41421, 2.0, 2.82843, 4.0 -julia> LogRange{Float16}(-1, -4, 5) +julia> Base.LogRange{Float16}(-1, -4, 5) 5-element LogRange{Float16, Float64}: -1.0, -1.414, -2.0, -2.828, -4.0 -julia> LogRange(1e-310, 1e-300, 11)[1:2:end] +julia> logrange(1e-310, 1e-300, 11)[1:2:end] 6-element Vector{Float64}: 1.0e-310 9.999999999999974e-309 From d5f71e9de0e9411b670f7e6ea4a71d114964f1a5 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Thu, 18 Jan 2024 16:32:59 -0500 Subject: [PATCH 16/24] documentation, esp complex --- base/range.jl | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/base/range.jl b/base/range.jl index 264dfbff45d64..cf9e05637cc2c 100644 --- a/base/range.jl +++ b/base/range.jl @@ -70,6 +70,7 @@ Valid invocations of range are: * Call `range` with one of `stop` or `length`. `start` and `step` will be assumed to be one. See Extended Help for additional details on the returned type. +See also [`logrange`](@ref) for logarithmically spaced points. # Examples ```jldoctest @@ -550,6 +551,8 @@ julia> collect(LinRange(-0.1, 0.3, 5)) 0.19999999999999998 0.3 ``` + +See also [`logrange`](@ref) for logarithmically spaced points. """ struct LinRange{T,L<:Integer} <: AbstractRange{T} start::T @@ -1548,8 +1551,7 @@ These are calculated using the logs of the endpoints, which are stored on construction, often in higher precision than `T`. Negative values of `start` and `stop` are allowed, but both must have the -same sign. For complex `T`, all points lie on the same branch of `log` -as used by `log(start)` and `log(stop)`. +same sign. All values are then negative. # Examples ```jldoctest @@ -1573,7 +1575,24 @@ julia> logrange(1e-310, 1e-300, 11)[1:2:end] julia> prevfloat(1e-308, 5) == ans[2] true -julia> LogRange{ComplexF32}(1, -1 +0.0im, 5) |> collect +julia> logrange(2, Inf, 5) +5-element LogRange{Float64, Base.TwicePrecision{Float64}}: + 2.0, Inf, Inf, Inf, Inf + +julia> logrange(0, 4, 5) +5-element LogRange{Float64, Base.TwicePrecision{Float64}}: + NaN, NaN, NaN, NaN, 4.0 +``` + +For complex `T`, all points lie on the same branch of [`log`](@ref) as is used by `log(start)` +and `log(stop)`. That is, all branch cuts are on the negative real axis. + +If this is not what you want, then adjust the arguments. For instance `start * logrange(1, stop/start, length)` +places the cut where `stop/start` is negative real, i.e. where [`angle`](@ref)`(stop/start) ≈ pi`. +This choice appears to match what `geomspace` in Python does. + +```jldoctest +julia> Base.LogRange{ComplexF32}(1, -1 +0.0im, 5) |> collect 5-element Vector{ComplexF32}: 1.0f0 + 0.0f0im 0.70710677f0 + 0.70710677f0im @@ -1584,13 +1603,22 @@ julia> LogRange{ComplexF32}(1, -1 +0.0im, 5) |> collect julia> ans ≈ cis.(LinRange{Float32}(0, pi, 5)) true -julia> LogRange(2, Inf, 5) -5-element LogRange{Float64, Base.TwicePrecision{Float64}}: - 2.0, Inf, Inf, Inf, Inf +julia> lo = -1+0.01im; hi = -1-0.01im; # either side of branch cut -julia> LogRange(0, 4, 5) -5-element LogRange{Float64, Base.TwicePrecision{Float64}}: - NaN, NaN, NaN, NaN, 4.0 +julia> logrange(lo, hi, 5) # goes near to +1 +5-element Base.LogRange{ComplexF64, ComplexF64}: + -1.0+0.01im, 0.00500006+1.00004im, 1.00005+0.0im, 0.00500006-1.00004im, -1.0-0.01im + +julia> angle(hi/lo) # far from branch cut +0.01999933337333048 + +julia> lo .* logrange(1, hi/lo, 5) .|> ComplexF32 # stays near -1 +5-element Vector{ComplexF32}: + -1.0f0 + 0.01f0im + -1.0000376f0 + 0.0050000623f0im + -1.00005f0 - 1.7347235f-18im + -1.0000376f0 - 0.0050000623f0im + -1.0f0 - 0.01f0im ``` !!! compat "Julia 1.11" From 54b3a8fdcbe9cdd4ba4bd0b3b94fd36917c9aa87 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sat, 20 Jan 2024 18:52:04 -0500 Subject: [PATCH 17/24] more doc tweaks --- base/exports.jl | 1 + base/range.jl | 46 +++++++++++++++++++++++++++++++--------------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/base/exports.jl b/base/exports.jl index e97abd1003591..a31ea1b0fa842 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -1096,6 +1096,7 @@ public Generator, ImmutableDict, OneTo, + LogRange, AnnotatedString, AnnotatedChar, UUID, diff --git a/base/range.jl b/base/range.jl index cf9e05637cc2c..30233fd3786d7 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1506,8 +1506,6 @@ you specify the number of elements not the ratio. Unlike `logspace` in Python and Matlab, the `start` and `stop` arguments are always the first and last elements of the result, not powers applied to some base. -See also [`range`](@ref) for linearly spaced points. - # Examples ```jldoctest julia> logrange(10, 4000, length=3) @@ -1532,6 +1530,10 @@ julia> logrange(-27, -3, length=7) # allows negative numbers -27.0, -18.7208, -12.9802, -9.0, -6.24025, -4.32675, -3.0 ``` +See the [`LogRange`](@ref Base.LogRange) type for further details. + +See also [`range`](@ref) for linearly spaced points. + !!! compat "Julia 1.11" This function requires at least Julia 1.11. """ @@ -1555,7 +1557,7 @@ same sign. All values are then negative. # Examples ```jldoctest -julia> logrange(1, 4, 5) +julia> logrange(1, 4, length=5) 5-element LogRange{Float64, Base.TwicePrecision{Float64}}: 1.0, 1.41421, 2.0, 2.82843, 4.0 @@ -1584,15 +1586,26 @@ julia> logrange(0, 4, 5) NaN, NaN, NaN, NaN, 4.0 ``` +Note that integer eltype `T` is not allowed. +Use for instance `round.(Int, xs)`, or explicit powers of some integer base: + +```jldoctest +julia> xs = logrange(1, 512, 4) +4-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: + 1.0, 8.0, 64.0, 512.0 + +julia> 2 .^ (0:3:9) |> println +[1, 8, 64, 512] +``` + For complex `T`, all points lie on the same branch of [`log`](@ref) as is used by `log(start)` and `log(stop)`. That is, all branch cuts are on the negative real axis. -If this is not what you want, then adjust the arguments. For instance `start * logrange(1, stop/start, length)` +Other branches can be used by adjusting the arguments. For instance `start * logrange(1, stop/start, length)` places the cut where `stop/start` is negative real, i.e. where [`angle`](@ref)`(stop/start) ≈ pi`. -This choice appears to match what `geomspace` in Python does. ```jldoctest -julia> Base.LogRange{ComplexF32}(1, -1 +0.0im, 5) |> collect +julia> Base.LogRange{ComplexF32}(1, -1+0im, 5) |> collect 5-element Vector{ComplexF32}: 1.0f0 + 0.0f0im 0.70710677f0 + 0.70710677f0im @@ -1603,22 +1616,25 @@ julia> Base.LogRange{ComplexF32}(1, -1 +0.0im, 5) |> collect julia> ans ≈ cis.(LinRange{Float32}(0, pi, 5)) true -julia> lo = -1+0.01im; hi = -1-0.01im; # either side of branch cut +julia> lo = -1+0.01f0im; hi = -1-0.01f0im; + +julia> angle(lo), angle(hi) # either side of branch cut +(3.131593f0, -3.131593f0) julia> logrange(lo, hi, 5) # goes near to +1 -5-element Base.LogRange{ComplexF64, ComplexF64}: +5-element Base.LogRange{ComplexF32, ComplexF64}: -1.0+0.01im, 0.00500006+1.00004im, 1.00005+0.0im, 0.00500006-1.00004im, -1.0-0.01im -julia> angle(hi/lo) # far from branch cut -0.01999933337333048 - -julia> lo .* logrange(1, hi/lo, 5) .|> ComplexF32 # stays near -1 +julia> lo .* logrange(1, hi/lo, 5) # stays near -1 5-element Vector{ComplexF32}: -1.0f0 + 0.01f0im - -1.0000376f0 + 0.0050000623f0im - -1.00005f0 - 1.7347235f-18im + -1.0000374f0 + 0.0050000628f0im + -1.00005f0 + 0.0f0im -1.0000376f0 - 0.0050000623f0im - -1.0f0 - 0.01f0im + -1.0f0 - 0.009999999f0im + +julia> angle(hi/lo) # far from branch cut +0.01999933f0 ``` !!! compat "Julia 1.11" From 57fd52a8aae13500e11e9bc1c4ba37ddba166ee4 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:04:58 -0500 Subject: [PATCH 18/24] fix tests --- base/range.jl | 18 ++++--- test/ranges.jl | 124 ++++++++++++++++++++++++++----------------------- 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/base/range.jl b/base/range.jl index 30233fd3786d7..a0d0a361246ef 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1558,11 +1558,11 @@ same sign. All values are then negative. # Examples ```jldoctest julia> logrange(1, 4, length=5) -5-element LogRange{Float64, Base.TwicePrecision{Float64}}: +5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: 1.0, 1.41421, 2.0, 2.82843, 4.0 julia> Base.LogRange{Float16}(-1, -4, 5) -5-element LogRange{Float16, Float64}: +5-element Base.LogRange{Float16, Float64}: -1.0, -1.414, -2.0, -2.828, -4.0 julia> logrange(1e-310, 1e-300, 11)[1:2:end] @@ -1578,11 +1578,11 @@ julia> prevfloat(1e-308, 5) == ans[2] true julia> logrange(2, Inf, 5) -5-element LogRange{Float64, Base.TwicePrecision{Float64}}: +5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: 2.0, Inf, Inf, Inf, Inf julia> logrange(0, 4, 5) -5-element LogRange{Float64, Base.TwicePrecision{Float64}}: +5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: NaN, NaN, NaN, NaN, 4.0 ``` @@ -1621,9 +1621,13 @@ julia> lo = -1+0.01f0im; hi = -1-0.01f0im; julia> angle(lo), angle(hi) # either side of branch cut (3.131593f0, -3.131593f0) -julia> logrange(lo, hi, 5) # goes near to +1 -5-element Base.LogRange{ComplexF32, ComplexF64}: - -1.0+0.01im, 0.00500006+1.00004im, 1.00005+0.0im, 0.00500006-1.00004im, -1.0-0.01im +julia> logrange(lo, hi, 5) |> collect # goes near to +1 +5-element Vector{ComplexF32}: + -1.0f0 + 0.01f0im + 0.0050000623f0 + 1.0000376f0im + 1.00005f0 + 0.0f0im + 0.0050000623f0 - 1.0000376f0im + -1.0f0 - 0.01f0im julia> lo .* logrange(1, hi/lo, 5) # stays near -1 5-element Vector{ComplexF32}: diff --git a/test/ranges.jl b/test/ranges.jl index cef51301a9fa3..684320a2a14b0 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2604,86 +2604,94 @@ end @test_throws errmsg range(CartesianIndex(1), step=CartesianIndex(1), length=3) end -@testset "LogRange" begin +@testset "logrange" begin # basic idea @test logrange(2, 16, 4) ≈ [2, 4, 8, 16] - @test LogRange(1/8, 8.0, 7) ≈ [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] + @test logrange(1/8, 8.0, 7) ≈ [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0] @test logrange(1000, 1, 4) ≈ [1000, 100, 10, 1] - @test LogRange(1, 10^9, 19)[1:2:end] ≈ 10 .^ (0:9) + @test logrange(1, 10^9, 19)[1:2:end] ≈ 10 .^ (0:9) # negative & complex - @test LogRange(-1, -4, 3) == [-1, -2, -4] - @test LogRange(1, -1+0.0im, 3) ≈ [1, im, -1] - @test LogRange(1, -1-0.0im, 3) ≈ [1, -im, -1] + @test logrange(-1, -4, 3) == [-1, -2, -4] + @test logrange(1, -1+0.0im, 3) ≈ [1, im, -1] # branch cut first arg + @test logrange(1, -1-0.0im, 3) ≈ [1, -im, -1] + @test logrange(-1+1e-10im, 1, 3) ≈ [-1, im, 1] # branch cut second arg + @test logrange(-1-1e-10im, 1, 3) ≈ [-1, -im, 1] + @test logrange(im + 1e-10, -im + 1e-10, 3) ≈ [im, 1, -im] # no branch cut here + @test logrange(im - 1e-10, -im - 1e-10, 3) ≈ [im, 1, -im] + @test logrange(1+im, -1-im, 5) ≈ [1+im, sqrt(2), 1-im, -sqrt(2)*im, -1-im] + @test logrange(-1+im, -1-im, 3) ≈ [-1+im, sqrt(2), -1-im] # endpoints - @test LogRange(0.1f0, 100, 33)[1] === 0.1f0 - @test LogRange(0.789, 123_456, 135_790)[[begin, end]] == [0.789, 123_456] - @test LogRange(nextfloat(0f0), floatmax(Float32), typemax(Int))[end] === floatmax(Float32) - @test LogRange(nextfloat(Float16(0)), floatmax(Float16), 66_000)[end] === floatmax(Float16) - @test first(LogRange(pi, 2pi, 3000)) === LogRange(pi, 2pi, 3000)[1] === Float64(pi) - @test last(LogRange(-0.01, -0.1, 3000)) === last(LogRange(-0.01, -0.1, 3000))[end] === -0.1 + @test logrange(0.1f0, 100, 33)[1] === 0.1f0 + @test logrange(0.789, 123_456, 135_790)[[begin, end]] == [0.789, 123_456] + @test logrange(nextfloat(0f0), floatmax(Float32), typemax(Int))[end] === floatmax(Float32) + @test logrange(nextfloat(Float16(0)), floatmax(Float16), 66_000)[end] === floatmax(Float16) + @test first(logrange(pi, 2pi, 3000)) === logrange(pi, 2pi, 3000)[1] === Float64(pi) + @test last(logrange(-0.01, -0.1, 3000)) === last(logrange(-0.01, -0.1, 3000))[end] === -0.1 if Int == Int64 - @test LogRange(0.1, 1000, 2^54)[end] === 1000.0 - @test LogRange(-0.1, -1000, 2^55)[end] === -1000.0 + @test logrange(0.1, 1000, 2^54)[end] === 1000.0 + @test logrange(-0.1, -1000, 2^55)[end] === -1000.0 end # empty, only, NaN, Inf - @test first(LogRange(1, 2, 0)) === 1.0 - @test last(LogRange(1, 2, 0)) === 2.0 - @test collect(LogRange(1, 2, 0)) == Float64[] - @test isnan(first(LogRange(0, 2, 0))) - @test only(LogRange(2pi, 2pi, 1)) === LogRange(2pi, 2pi, 1)[1] === 2pi - @test isnan(LogRange(1, NaN, 3)[2]) - @test isnan(LogRange(NaN, 2, 3)[2]) - @test isnan(LogRange(1f0, NaN32, 3)[2]) - @test isnan(LogRange(NaN32, 2f0, 3)[2]) - @test isnan(LogRange(0, 2, 3)[1]) - @test isnan(LogRange(0, -2, 3)[1]) - @test isnan(LogRange(-0.0, +2.0, 3)[1]) - @test isnan(LogRange(0f0, 2f0, 3)[1]) - @test isnan(LogRange(0f0, -2f0, 3)[1]) - @test isnan(LogRange(-0f0, 2f0, 3)[1]) - @test isinf(LogRange(1, Inf, 3)[2]) - @test -Inf === LogRange(-1, -Inf, 3)[2] - @test isinf(LogRange(1f0, Inf32, 3)[2]) - @test -Inf32 === LogRange(-1f0, -Inf32, 3)[2] + @test first(logrange(1, 2, 0)) === 1.0 + @test last(logrange(1, 2, 0)) === 2.0 + @test collect(logrange(1, 2, 0)) == Float64[] + @test isnan(first(logrange(0, 2, 0))) + @test only(logrange(2pi, 2pi, 1)) === logrange(2pi, 2pi, 1)[1] === 2pi + @test isnan(logrange(1, NaN, 3)[2]) + @test isnan(logrange(NaN, 2, 3)[2]) + @test isnan(logrange(1f0, NaN32, 3)[2]) + @test isnan(logrange(NaN32, 2f0, 3)[2]) + @test isnan(logrange(0, 2, 3)[1]) + @test isnan(logrange(0, -2, 3)[1]) + @test isnan(logrange(-0.0, +2.0, 3)[1]) + @test isnan(logrange(0f0, 2f0, 3)[1]) + @test isnan(logrange(0f0, -2f0, 3)[1]) + @test isnan(logrange(-0f0, 2f0, 3)[1]) + @test isinf(logrange(1, Inf, 3)[2]) + @test -Inf === logrange(-1, -Inf, 3)[2] + @test isinf(logrange(1f0, Inf32, 3)[2]) + @test -Inf32 === logrange(-1f0, -Inf32, 3)[2] # constant - @test LogRange(1, 1, 3) == fill(1.0, 3) - @test LogRange(-1f0, -1f0, 3) == fill(-1f0, 3) - @test all(isnan, LogRange(0.0, -0.0, 3)) - @test all(isnan, LogRange(-0f0, 0f0, 3)) + @test logrange(1, 1, 3) == fill(1.0, 3) + @test logrange(-1f0, -1f0, 3) == fill(-1f0, 3) + @test all(isnan, logrange(0.0, -0.0, 3)) + @test all(isnan, logrange(-0f0, 0f0, 3)) # subnormal Float64 - x = LogRange(1e-320, 1e-300, 21) .* 1e300 - @test x ≈ LogRange(1e-20, 1, 21) rtol=1e-6 + x = logrange(1e-320, 1e-300, 21) .* 1e300 + @test x ≈ logrange(1e-20, 1, 21) rtol=1e-6 # types - @test eltype(LogRange(1, 10, 3)) == Float64 - @test eltype(LogRange(1, 10, Int32(3))) == Float64 - @test eltype(LogRange(1, 10f0, 3)) == Float32 - @test eltype(LogRange(1f0, 10, 3)) == Float32 - @test eltype(LogRange(1f0, 10+im, 3)) == ComplexF32 - @test eltype(LogRange(1f0, 10.0+im, 3)) == ComplexF64 - @test eltype(LogRange(1, big(10), 3)) == BigFloat - @test LogRange(big"0.3", big(pi), 50)[1] == big"0.3" - @test LogRange(big"0.3", big(pi), 50)[end] == big(pi) + @test eltype(logrange(1, 10, 3)) == Float64 + @test eltype(logrange(1, 10, Int32(3))) == Float64 + @test eltype(logrange(1, 10f0, 3)) == Float32 + @test eltype(logrange(1f0, 10, 3)) == Float32 + @test eltype(logrange(1f0, 10+im, 3)) == ComplexF32 + @test eltype(logrange(1f0, 10.0+im, 3)) == ComplexF64 + @test eltype(logrange(1, big(10), 3)) == BigFloat + @test logrange(big"0.3", big(pi), 50)[1] == big"0.3" + @test logrange(big"0.3", big(pi), 50)[end] == big(pi) # more constructors - @test logrange(1,2,3) === LogRange(1,2,3) == LogRange{Float64}(1,2,3) - @test logrange(1f0, 2f0, length=3) == LogRange{Float32}(1,2,3) + @test logrange(1,2,length=3) === Base.LogRange(1,2,3) == Base.LogRange{Float64}(1,2,3) + @test logrange(1f0, 2f0, length=3) == Base.LogRange{Float32}(1,2,3) # errors - @test_throws ArgumentError LogRange(1, 10, -1) - @test_throws ArgumentError LogRange(1, 10, 1) # endpoints must not differ - @test_throws DomainError LogRange(1, -1, 3) # needs complex numbers - @test_throws ArgumentError LogRange(1, 10, 2)[true] - @test_throws BoundsError LogRange(1, 10, 2)[3] - @test_throws ArgumentError LogRange{Int}(1,4,5) # no integer ranges + @test_throws UndefKeywordError logrange(1, 10) # no default length + @test_throws ArgumentError logrange(1, 10, -1) # negative length + @test_throws ArgumentError logrange(1, 10, 1) # endpoints must not differ + @test_throws DomainError logrange(1, -1, 3) # needs complex numbers + @test_throws ArgumentError logrange(1, 10, 2)[true] # bad index + @test_throws BoundsError logrange(1, 10, 2)[3] + @test_throws ArgumentError Base.LogRange{Int}(1,4,5) # no integer ranges + @test_throws MethodError Base.LogRange(1,4, length=5) # type does not take keyword # printing - @test repr(LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" - @test repr("text/plain", LogRange(1,2,3)) == "3-element LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" + @test repr(Base.LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" + @test repr("text/plain", Base.LogRange(1,2,3)) == "3-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" end @testset "_log_twice64_unchecked" begin From 2f7f0e9f42c32767733abe5c3526b9faa4a90ac1 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:36:23 -0500 Subject: [PATCH 19/24] add a note to AbstractRange requiring step --- base/range.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/base/range.jl b/base/range.jl index a0d0a361246ef..953e1554c5a48 100644 --- a/base/range.jl +++ b/base/range.jl @@ -253,10 +253,13 @@ end ## 1-dimensional ranges ## """ - AbstractRange{T} + AbstractRange{T} <: AbstractVector{T} -Supertype for ranges with elements of type `T`. -[`UnitRange`](@ref) and other types are subtypes of this. +Supertype for linear ranges with elements of type `T`. +[`UnitRange`](@ref), [`LinRange`](@ref) and other types are subtypes of this. + +All subtypes must define [`step`](@ref). +Thus [`LogRange`](@ref Base.LogRange) is not a subtype of `AbstractRange`. """ abstract type AbstractRange{T} <: AbstractArray{T,1} end From dc26b09043c4a9a156b4d90e523275e954890b25 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Fri, 9 Feb 2024 18:17:27 -0500 Subject: [PATCH 20/24] disallow negative numbers --- base/range.jl | 20 ++++---------------- test/ranges.jl | 10 +--------- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/base/range.jl b/base/range.jl index 953e1554c5a48..73b68a966940d 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1527,10 +1527,6 @@ julia> logrange(1f0, 32f0, 11) julia> logrange(1, 1000, length=4) ≈ 10 .^ (0:3) true - -julia> logrange(-27, -3, length=7) # allows negative numbers -7-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: - -27.0, -18.7208, -12.9802, -9.0, -6.24025, -4.32675, -3.0 ``` See the [`LogRange`](@ref Base.LogRange) type for further details. @@ -1555,18 +1551,15 @@ provided, but intermediate values may have small floating-point errors. These are calculated using the logs of the endpoints, which are stored on construction, often in higher precision than `T`. -Negative values of `start` and `stop` are allowed, but both must have the -same sign. All values are then negative. - # Examples ```jldoctest julia> logrange(1, 4, length=5) 5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: 1.0, 1.41421, 2.0, 2.82843, 4.0 -julia> Base.LogRange{Float16}(-1, -4, 5) +julia> Base.LogRange{Float16}(1, 4, 5) 5-element Base.LogRange{Float16, Float64}: - -1.0, -1.414, -2.0, -2.828, -4.0 + 1.0, 1.414, 2.0, 2.828, 4.0 julia> logrange(1e-310, 1e-300, 11)[1:2:end] 6-element Vector{Float64}: @@ -1664,8 +1657,7 @@ struct LogRange{T<:Number,X} <: AbstractArray{T,1} elseif len == 1 && start != stop throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1"))) - elseif iszero(start) || iszero(stop) - elseif T <: Real && (start<0) ⊻ (stop<0) + elseif T <: Real && ((start<0) || (stop<0)) throw(DomainError((start, stop), "LogRange will only return complex results if called with a complex argument")) end @@ -1673,11 +1665,7 @@ struct LogRange{T<:Number,X} <: AbstractArray{T,1} # LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998) throw(ArgumentError("LogRange{T} does not support integer types")) end - ex = if T <: Real && start + stop < 0 # start+stop allows for LogRange(-0.0, -2, 3) - _logrange_extra(-a, -b, len) - else - _logrange_extra(a, b, len) - end + ex = _logrange_extra(a, b, len) new{T,typeof(ex[1])}(a, b, len, ex) end end diff --git a/test/ranges.jl b/test/ranges.jl index 684320a2a14b0..d014c1a29c38d 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2612,7 +2612,6 @@ end @test logrange(1, 10^9, 19)[1:2:end] ≈ 10 .^ (0:9) # negative & complex - @test logrange(-1, -4, 3) == [-1, -2, -4] @test logrange(1, -1+0.0im, 3) ≈ [1, im, -1] # branch cut first arg @test logrange(1, -1-0.0im, 3) ≈ [1, -im, -1] @test logrange(-1+1e-10im, 1, 3) ≈ [-1, im, 1] # branch cut second arg @@ -2628,10 +2627,8 @@ end @test logrange(nextfloat(0f0), floatmax(Float32), typemax(Int))[end] === floatmax(Float32) @test logrange(nextfloat(Float16(0)), floatmax(Float16), 66_000)[end] === floatmax(Float16) @test first(logrange(pi, 2pi, 3000)) === logrange(pi, 2pi, 3000)[1] === Float64(pi) - @test last(logrange(-0.01, -0.1, 3000)) === last(logrange(-0.01, -0.1, 3000))[end] === -0.1 if Int == Int64 @test logrange(0.1, 1000, 2^54)[end] === 1000.0 - @test logrange(-0.1, -1000, 2^55)[end] === -1000.0 end # empty, only, NaN, Inf @@ -2645,18 +2642,12 @@ end @test isnan(logrange(1f0, NaN32, 3)[2]) @test isnan(logrange(NaN32, 2f0, 3)[2]) @test isnan(logrange(0, 2, 3)[1]) - @test isnan(logrange(0, -2, 3)[1]) @test isnan(logrange(-0.0, +2.0, 3)[1]) @test isnan(logrange(0f0, 2f0, 3)[1]) - @test isnan(logrange(0f0, -2f0, 3)[1]) - @test isnan(logrange(-0f0, 2f0, 3)[1]) @test isinf(logrange(1, Inf, 3)[2]) - @test -Inf === logrange(-1, -Inf, 3)[2] @test isinf(logrange(1f0, Inf32, 3)[2]) - @test -Inf32 === logrange(-1f0, -Inf32, 3)[2] # constant @test logrange(1, 1, 3) == fill(1.0, 3) - @test logrange(-1f0, -1f0, 3) == fill(-1f0, 3) @test all(isnan, logrange(0.0, -0.0, 3)) @test all(isnan, logrange(-0f0, 0f0, 3)) @@ -2684,6 +2675,7 @@ end @test_throws ArgumentError logrange(1, 10, -1) # negative length @test_throws ArgumentError logrange(1, 10, 1) # endpoints must not differ @test_throws DomainError logrange(1, -1, 3) # needs complex numbers + @test_throws DomainError logrange(-1, -2, 3) # not supported, for now @test_throws ArgumentError logrange(1, 10, 2)[true] # bad index @test_throws BoundsError logrange(1, 10, 2)[3] @test_throws ArgumentError Base.LogRange{Int}(1,4,5) # no integer ranges From 2a156f1dc88c2a45dfd1f35c841b7d1f2e458fed Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sun, 11 Feb 2024 22:17:18 -0500 Subject: [PATCH 21/24] disallow complex numbers --- base/range.jl | 65 +++++++++----------------------------------------- test/ranges.jl | 13 +--------- 2 files changed, 12 insertions(+), 66 deletions(-) diff --git a/base/range.jl b/base/range.jl index 73b68a966940d..3afdee6ea3b61 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1536,8 +1536,8 @@ See also [`range`](@ref) for linearly spaced points. !!! compat "Julia 1.11" This function requires at least Julia 1.11. """ -logrange(start::Number, stop::Number, length::Integer) = LogRange(start, stop, Int(length)) -logrange(start::Number, stop::Number; length::Integer) = LogRange(start, stop, Int(length)) +logrange(start::Real, stop::Real, length::Integer) = LogRange(start, stop, Int(length)) +logrange(start::Real, stop::Real; length::Integer) = logrange(start, stop, length) """ @@ -1594,58 +1594,15 @@ julia> 2 .^ (0:3:9) |> println [1, 8, 64, 512] ``` -For complex `T`, all points lie on the same branch of [`log`](@ref) as is used by `log(start)` -and `log(stop)`. That is, all branch cuts are on the negative real axis. - -Other branches can be used by adjusting the arguments. For instance `start * logrange(1, stop/start, length)` -places the cut where `stop/start` is negative real, i.e. where [`angle`](@ref)`(stop/start) ≈ pi`. - -```jldoctest -julia> Base.LogRange{ComplexF32}(1, -1+0im, 5) |> collect -5-element Vector{ComplexF32}: - 1.0f0 + 0.0f0im - 0.70710677f0 + 0.70710677f0im - 6.123234f-17 + 1.0f0im - -0.70710677f0 + 0.70710677f0im - -1.0f0 + 0.0f0im - -julia> ans ≈ cis.(LinRange{Float32}(0, pi, 5)) -true - -julia> lo = -1+0.01f0im; hi = -1-0.01f0im; - -julia> angle(lo), angle(hi) # either side of branch cut -(3.131593f0, -3.131593f0) - -julia> logrange(lo, hi, 5) |> collect # goes near to +1 -5-element Vector{ComplexF32}: - -1.0f0 + 0.01f0im - 0.0050000623f0 + 1.0000376f0im - 1.00005f0 + 0.0f0im - 0.0050000623f0 - 1.0000376f0im - -1.0f0 - 0.01f0im - -julia> lo .* logrange(1, hi/lo, 5) # stays near -1 -5-element Vector{ComplexF32}: - -1.0f0 + 0.01f0im - -1.0000374f0 + 0.0050000628f0im - -1.00005f0 + 0.0f0im - -1.0000376f0 - 0.0050000623f0im - -1.0f0 - 0.009999999f0im - -julia> angle(hi/lo) # far from branch cut -0.01999933f0 -``` - !!! compat "Julia 1.11" This type requires at least Julia 1.11. """ -struct LogRange{T<:Number,X} <: AbstractArray{T,1} +struct LogRange{T<:Real,X} <: AbstractArray{T,1} start::T stop::T len::Int extra::Tuple{X,X} - function LogRange{T}(start::T, stop::T, length::Int) where {T<:Number} + function LogRange{T}(start::T, stop::T, length::Int) where {T<:Real} # LogRange(0, 1, 100) could be == [0,0,0,0,...,1], that's the limit start -> 0, # but seems more likely to give silent surprises than returning NaN. a = iszero(start) ? T(NaN) : T(start) @@ -1657,11 +1614,11 @@ struct LogRange{T<:Number,X} <: AbstractArray{T,1} elseif len == 1 && start != stop throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1"))) - elseif T <: Real && ((start<0) || (stop<0)) + elseif start < 0 || stop < 0 throw(DomainError((start, stop), - "LogRange will only return complex results if called with a complex argument")) + "LogRange(start, stop, length) does not accept negative numbers")) end - if T <: Integer || T <: Complex{<:Integer} + if T <: Integer # LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998) throw(ArgumentError("LogRange{T} does not support integer types")) end @@ -1670,10 +1627,10 @@ struct LogRange{T<:Number,X} <: AbstractArray{T,1} end end -function LogRange{T}(start::Number, stop::Number, len::Integer) where {T} +function LogRange{T}(start::Real, stop::Real, len::Integer) where {T} LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len)) end -function LogRange(start::Number, stop::Number, len::Integer) +function LogRange(start::Real, stop::Real, len::Integer) T = float(promote_type(typeof(start), typeof(stop))) LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len)) end @@ -1684,7 +1641,7 @@ length(r::LogRange) = r.len first(r::LogRange) = r.start last(r::LogRange) = r.stop -function _logrange_extra(a::Number, b::Number, len::Int) +function _logrange_extra(a::Real, b::Real, len::Int) loga = log(1.0 * a) # widen to at least Float64 logb = log(1.0 * b) (loga/(len-1), logb/(len-1)) @@ -1708,7 +1665,7 @@ function getindex(r::LogRange{T}, i::Int) where {T} # accurate, nor does it handle NaN/Inf as desired, hence the cases above. logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] x = _exp_allowing_twice64(logx) - return T <: Real ? copysign(T(x), r.start) : T(x) + return T(x) end function show(io::IO, r::LogRange{T}) where {T} diff --git a/test/ranges.jl b/test/ranges.jl index d014c1a29c38d..d7e598de61ded 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2611,16 +2611,6 @@ end @test logrange(1000, 1, 4) ≈ [1000, 100, 10, 1] @test logrange(1, 10^9, 19)[1:2:end] ≈ 10 .^ (0:9) - # negative & complex - @test logrange(1, -1+0.0im, 3) ≈ [1, im, -1] # branch cut first arg - @test logrange(1, -1-0.0im, 3) ≈ [1, -im, -1] - @test logrange(-1+1e-10im, 1, 3) ≈ [-1, im, 1] # branch cut second arg - @test logrange(-1-1e-10im, 1, 3) ≈ [-1, -im, 1] - @test logrange(im + 1e-10, -im + 1e-10, 3) ≈ [im, 1, -im] # no branch cut here - @test logrange(im - 1e-10, -im - 1e-10, 3) ≈ [im, 1, -im] - @test logrange(1+im, -1-im, 5) ≈ [1+im, sqrt(2), 1-im, -sqrt(2)*im, -1-im] - @test logrange(-1+im, -1-im, 3) ≈ [-1+im, sqrt(2), -1-im] - # endpoints @test logrange(0.1f0, 100, 33)[1] === 0.1f0 @test logrange(0.789, 123_456, 135_790)[[begin, end]] == [0.789, 123_456] @@ -2660,8 +2650,6 @@ end @test eltype(logrange(1, 10, Int32(3))) == Float64 @test eltype(logrange(1, 10f0, 3)) == Float32 @test eltype(logrange(1f0, 10, 3)) == Float32 - @test eltype(logrange(1f0, 10+im, 3)) == ComplexF32 - @test eltype(logrange(1f0, 10.0+im, 3)) == ComplexF64 @test eltype(logrange(1, big(10), 3)) == BigFloat @test logrange(big"0.3", big(pi), 50)[1] == big"0.3" @test logrange(big"0.3", big(pi), 50)[end] == big(pi) @@ -2676,6 +2664,7 @@ end @test_throws ArgumentError logrange(1, 10, 1) # endpoints must not differ @test_throws DomainError logrange(1, -1, 3) # needs complex numbers @test_throws DomainError logrange(-1, -2, 3) # not supported, for now + @test_throws MethodError logrange(1, 2+3im, length=4) # not supported, for now @test_throws ArgumentError logrange(1, 10, 2)[true] # bad index @test_throws BoundsError logrange(1, 10, 2)[3] @test_throws ArgumentError Base.LogRange{Int}(1,4,5) # no integer ranges From ea12bdccd6a68c1cbc335fd10ae9bd235114e0d9 Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Sun, 11 Feb 2024 22:27:07 -0500 Subject: [PATCH 22/24] tidy error handling --- base/range.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/base/range.jl b/base/range.jl index 3afdee6ea3b61..5e20179a34fbe 100644 --- a/base/range.jl +++ b/base/range.jl @@ -1602,12 +1602,15 @@ struct LogRange{T<:Real,X} <: AbstractArray{T,1} stop::T len::Int extra::Tuple{X,X} - function LogRange{T}(start::T, stop::T, length::Int) where {T<:Real} + function LogRange{T}(start::T, stop::T, len::Int) where {T<:Real} + if T <: Integer + # LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998) + throw(ArgumentError("LogRange{T} does not support integer types")) + end # LogRange(0, 1, 100) could be == [0,0,0,0,...,1], that's the limit start -> 0, # but seems more likely to give silent surprises than returning NaN. a = iszero(start) ? T(NaN) : T(start) b = iszero(stop) ? T(NaN) : T(stop) - len = Int(length) if len < 0 throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): can't have negative length"))) @@ -1615,13 +1618,10 @@ struct LogRange{T<:Real,X} <: AbstractArray{T,1} throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1"))) elseif start < 0 || stop < 0 + # log would throw, but _log_twice64_unchecked does not throw(DomainError((start, stop), "LogRange(start, stop, length) does not accept negative numbers")) end - if T <: Integer - # LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998) - throw(ArgumentError("LogRange{T} does not support integer types")) - end ex = _logrange_extra(a, b, len) new{T,typeof(ex[1])}(a, b, len, ex) end From 9abaf5a4c3db3ec4300ceff4496180f6a024906d Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Mon, 12 Feb 2024 11:15:12 -0500 Subject: [PATCH 23/24] add to NEWS.md --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 4ad8a9b761dda..ae42aad025e38 100644 --- a/NEWS.md +++ b/NEWS.md @@ -83,6 +83,7 @@ New library functions * `in!(x, s::AbstractSet)` will return whether `x` is in `s`, and insert `x` in `s` if not. * The new `Libc.mkfifo` function wraps the `mkfifo` C function on Unix platforms ([#34587]). +* `logrange(start, stop; length)` makes a range of constant ratio, instead of constant step ([#39071]) * `copyuntil(out, io, delim)` and `copyline(out, io)` copy data into an `out::IO` stream ([#48273]). * `eachrsplit(string, pattern)` iterates split substrings right to left. * `Sys.username()` can be used to return the current user's username ([#51897]). From 454027046be7834d964e4635ae158b53547ded1c Mon Sep 17 00:00:00 2001 From: Michael Abbott <32575566+mcabbott@users.noreply.github.com> Date: Wed, 14 Feb 2024 20:44:52 -0500 Subject: [PATCH 24/24] throw errors for 0, Inf, NaN --- base/range.jl | 38 +++++++++++++++----------------------- test/ranges.jl | 23 ++++++++--------------- 2 files changed, 23 insertions(+), 38 deletions(-) diff --git a/base/range.jl b/base/range.jl index 5e20179a34fbe..4f92b305564cd 100644 --- a/base/range.jl +++ b/base/range.jl @@ -555,7 +555,7 @@ julia> collect(LinRange(-0.1, 0.3, 5)) 0.3 ``` -See also [`logrange`](@ref) for logarithmically spaced points. +See also [`Logrange`](@ref Base.LogRange) for logarithmically spaced points. """ struct LinRange{T,L<:Integer} <: AbstractRange{T} start::T @@ -1572,14 +1572,6 @@ julia> logrange(1e-310, 1e-300, 11)[1:2:end] julia> prevfloat(1e-308, 5) == ans[2] true - -julia> logrange(2, Inf, 5) -5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: - 2.0, Inf, Inf, Inf, Inf - -julia> logrange(0, 4, 5) -5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}: - NaN, NaN, NaN, NaN, 4.0 ``` Note that integer eltype `T` is not allowed. @@ -1607,23 +1599,25 @@ struct LogRange{T<:Real,X} <: AbstractArray{T,1} # LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998) throw(ArgumentError("LogRange{T} does not support integer types")) end - # LogRange(0, 1, 100) could be == [0,0,0,0,...,1], that's the limit start -> 0, - # but seems more likely to give silent surprises than returning NaN. - a = iszero(start) ? T(NaN) : T(start) - b = iszero(stop) ? T(NaN) : T(stop) - if len < 0 + if iszero(start) || iszero(stop) + throw(DomainError((start, stop), + "LogRange cannot start or stop at zero")) + elseif start < 0 || stop < 0 + # log would throw, but _log_twice64_unchecked does not + throw(DomainError((start, stop), + "LogRange does not accept negative numbers")) + elseif !isfinite(start) || !isfinite(stop) + throw(DomainError((start, stop), + "LogRange is only defined for finite start & stop")) + elseif len < 0 throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): can't have negative length"))) elseif len == 1 && start != stop throw(ArgumentError(LazyString( "LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1"))) - elseif start < 0 || stop < 0 - # log would throw, but _log_twice64_unchecked does not - throw(DomainError((start, stop), - "LogRange(start, stop, length) does not accept negative numbers")) end - ex = _logrange_extra(a, b, len) - new{T,typeof(ex[1])}(a, b, len, ex) + ex = _logrange_extra(start, stop, len) + new{T,typeof(ex[1])}(start, stop, len, ex) end end @@ -1659,10 +1653,8 @@ function getindex(r::LogRange{T}, i::Int) where {T} @boundscheck checkbounds(r, i) i == 1 && return r.start i == r.len && return r.stop - tot = r.start + r.stop - isfinite(tot) || return tot # Main path uses Math.exp_impl for TwicePrecision, but is not perfectly - # accurate, nor does it handle NaN/Inf as desired, hence the cases above. + # accurate, hence the special cases for endpoints above. logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2] x = _exp_allowing_twice64(logx) return T(x) diff --git a/test/ranges.jl b/test/ranges.jl index d7e598de61ded..4660a96dfc16a 100644 --- a/test/ranges.jl +++ b/test/ranges.jl @@ -2621,25 +2621,12 @@ end @test logrange(0.1, 1000, 2^54)[end] === 1000.0 end - # empty, only, NaN, Inf + # empty, only, constant @test first(logrange(1, 2, 0)) === 1.0 @test last(logrange(1, 2, 0)) === 2.0 @test collect(logrange(1, 2, 0)) == Float64[] - @test isnan(first(logrange(0, 2, 0))) @test only(logrange(2pi, 2pi, 1)) === logrange(2pi, 2pi, 1)[1] === 2pi - @test isnan(logrange(1, NaN, 3)[2]) - @test isnan(logrange(NaN, 2, 3)[2]) - @test isnan(logrange(1f0, NaN32, 3)[2]) - @test isnan(logrange(NaN32, 2f0, 3)[2]) - @test isnan(logrange(0, 2, 3)[1]) - @test isnan(logrange(-0.0, +2.0, 3)[1]) - @test isnan(logrange(0f0, 2f0, 3)[1]) - @test isinf(logrange(1, Inf, 3)[2]) - @test isinf(logrange(1f0, Inf32, 3)[2]) - # constant @test logrange(1, 1, 3) == fill(1.0, 3) - @test all(isnan, logrange(0.0, -0.0, 3)) - @test all(isnan, logrange(-0f0, 0f0, 3)) # subnormal Float64 x = logrange(1e-320, 1e-300, 21) .* 1e300 @@ -2669,10 +2656,16 @@ end @test_throws BoundsError logrange(1, 10, 2)[3] @test_throws ArgumentError Base.LogRange{Int}(1,4,5) # no integer ranges @test_throws MethodError Base.LogRange(1,4, length=5) # type does not take keyword + # (not sure if these should ideally be DomainError or ArgumentError) + @test_throws DomainError logrange(1, Inf, 3) + @test_throws DomainError logrange(0, 2, 3) + @test_throws DomainError logrange(1, NaN, 3) + @test_throws DomainError logrange(NaN, 2, 3) # printing - @test repr(Base.LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" + @test repr(Base.LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" # like 2-arg show @test repr("text/plain", Base.LogRange(1,2,3)) == "3-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0" + @test repr("text/plain", Base.LogRange(1,2,0)) == "LogRange{Float64}(1.0, 2.0, 0)" # empty case end @testset "_log_twice64_unchecked" begin