Skip to content

Commit 220f4d7

Browse files
authored
logrange (#821)
1 parent ca02356 commit 220f4d7

File tree

4 files changed

+301
-9
lines changed

4 files changed

+301
-9
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "Compat"
22
uuid = "34da2185-b29b-5c13-b0c7-acf172513d20"
3-
version = "4.13.0"
3+
version = "4.14.0"
44

55
[deps]
66
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ changes in `julia`.
7272

7373
* `allequal(f, itr)` and `allunique(f, itr)` methods. ([#47679]) (since Compat 4.13.0)
7474

75+
* `logrange(lo, hi; length)` is like `range` but with a constant ratio, not difference. ([#39071]) (since Compat 4.14.0) Note that on Julia 1.8 and earlier, the version from Compat has slightly lower floating-point accuracy than the one in Base (Julia 1.11 and later).
76+
7577
* `Iterators.cycle(itr, n)` is the lazy version of `repeat(vector, n)`. ([#47354]) (since Compat 4.13.0)
7678

7779
* `@compat public foo, bar` marks `foo` and `bar` as public in Julia 1.11+ and is a no-op in Julia 1.10 and earlier. ([#50105]) (since Compat 3.47.0, 4.10.0)
@@ -163,6 +165,7 @@ Note that you should specify the correct minimum version for `Compat` in the
163165
[#36229]: https://github.com/JuliaLang/julia/issues/36229
164166
[#37978]: https://github.com/JuliaLang/julia/issues/37978
165167
[#39037]: https://github.com/JuliaLang/julia/issues/39037
168+
[#39071]: https://github.com/JuliaLang/julia/pull/39071
166169
[#39245]: https://github.com/JuliaLang/julia/issues/39245
167170
[#39285]: https://github.com/JuliaLang/julia/issues/39285
168171
[#39794]: https://github.com/JuliaLang/julia/issues/39794

src/Compat.jl

Lines changed: 232 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ end
250250
if VERSION < v"1.8.0-DEV.487"
251251
export eachsplit
252252

253-
"""
253+
@doc """
254254
eachsplit(str::AbstractString, dlm; limit::Integer=0)
255255
eachsplit(str::AbstractString; limit::Integer=0)
256256
@@ -275,7 +275,8 @@ if VERSION < v"1.8.0-DEV.487"
275275
"Ma"
276276
"rch"
277277
```
278-
"""
278+
""" eachsplit
279+
279280
function eachsplit end
280281

281282
struct SplitIterator{S<:AbstractString,F}
@@ -421,7 +422,7 @@ end
421422
return nothing
422423
end
423424

424-
"""
425+
@doc """
425426
pkgversion(m::Module)
426427
427428
Return the version of the package that imported module `m`,
@@ -433,7 +434,8 @@ end
433434
434435
To get the version of the package that imported the current module
435436
the form `pkgversion(@__MODULE__)` can be used.
436-
"""
437+
""" pkgversion
438+
437439
function pkgversion(m::Module)
438440
path = pkgdir(m)
439441
path === nothing && return nothing
@@ -457,7 +459,7 @@ if VERSION < v"1.9.0-DEV.1163"
457459
import Base: IteratorSize, HasLength, HasShape, OneTo
458460
export stack
459461

460-
"""
462+
@doc """
461463
stack(iter; [dims])
462464
463465
Combine a collection of arrays (or other iterable objects) of equal size
@@ -547,10 +549,11 @@ if VERSION < v"1.9.0-DEV.1163"
547549
julia> hvcat(5, M...) |> size # hvcat puts matrices next to each other
548550
(14, 15)
549551
```
550-
"""
552+
""" stack
553+
551554
stack(iter; dims=:) = _stack(dims, iter)
552555

553-
"""
556+
@doc """
554557
stack(f, args...; [dims])
555558
556559
Apply a function to each element of a collection, and `stack` the result.
@@ -576,7 +579,8 @@ if VERSION < v"1.9.0-DEV.1163"
576579
1.0 2.0 3.0 10.0 20.0 30.0 0.1 0.2 0.3
577580
4.0 5.0 6.0 400.0 500.0 600.0 0.04 0.05 0.06
578581
```
579-
"""
582+
""" stack(f, iter)
583+
580584
stack(f, iter; dims=:) = _stack(dims, f(x) for x in iter)
581585
stack(f, xs, yzs...; dims=:) = _stack(dims, f(xy...) for xy in zip(xs, yzs...))
582586

@@ -803,6 +807,226 @@ if VERSION < v"1.11.0-DEV.1579"
803807
Iterators.cycle(xs, n::Integer) = Iterators.flatten(Iterators.repeated(xs, n))
804808
end
805809

810+
# https://github.com/JuliaLang/julia/pull/39071
811+
if !isdefined(Base, :logrange) # VERSION < v"1.12.0-DEV.2" or appropriate 1.11.x after backporting
812+
813+
export logrange
814+
815+
@doc """
816+
logrange(start, stop, length)
817+
logrange(start, stop; length)
818+
819+
Construct a specialized array whose elements are spaced logarithmically
820+
between the given endpoints. That is, the ratio of successive elements is
821+
a constant, calculated from the length.
822+
823+
This is similar to `geomspace` in Python. Unlike `PowerRange` in Mathematica,
824+
you specify the number of elements not the ratio.
825+
Unlike `logspace` in Python and Matlab, the `start` and `stop` arguments are
826+
always the first and last elements of the result, not powers applied to some base.
827+
828+
# Examples
829+
```
830+
julia> logrange(10, 4000, length=3)
831+
3-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
832+
10.0, 200.0, 4000.0
833+
834+
julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean
835+
true
836+
837+
julia> range(10, 40, length=3)[2] ≈ (10 + 40)/2 # arithmetic mean
838+
true
839+
840+
julia> logrange(1f0, 32f0, 11)
841+
11-element Base.LogRange{Float32, Float64}:
842+
1.0, 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0
843+
844+
julia> logrange(1, 1000, length=4) ≈ 10 .^ (0:3)
845+
true
846+
```
847+
848+
See the [`Compat.LogRange`](@ref Compat.LogRange) type for further details.
849+
850+
!!! compat "Julia 1.9"
851+
The version of this struct in Compat.jl does not use `Base.TwicePrecision{Float64}`
852+
before Julia 1.9, so it sometimes has larger floating-point errors on intermediate points.
853+
854+
!!! compat "Julia 1.11"
855+
The printing of Compat.jl's version of the struct is also different,
856+
less like `LinRange` and more like `Vector`.
857+
""" logrange
858+
859+
logrange(start::Real, stop::Real, length::Integer) = LogRange(start, stop, Int(length))
860+
logrange(start::Real, stop::Real; length::Integer) = logrange(start, stop, length)
861+
862+
@doc """
863+
LogRange{T}(start, stop, len) <: AbstractVector{T}
864+
865+
A range whose elements are spaced logarithmically between `start` and `stop`,
866+
with spacing controlled by `len`. Returned by [`logrange`](@ref).
867+
868+
Like [`LinRange`](@ref), the first and last elements will be exactly those
869+
provided, but intermediate values may have small floating-point errors.
870+
These are calculated using the logs of the endpoints, which are
871+
stored on construction, often in higher precision than `T`.
872+
873+
!!! compat "Julia 1.9"
874+
The version of this struct in Compat.jl does not use `Base.TwicePrecision{Float64}`
875+
before Julia 1.9. Therefore it has larger floating-point errors on intermediate
876+
points than shown below.
877+
878+
!!! compat "Julia 1.11"
879+
The printing of Compat.jl's version of the struct is also different,
880+
less like `LinRange` and more like `Vector`.
881+
882+
# Examples
883+
```
884+
julia> logrange(1, 4, length=5)
885+
5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
886+
1.0, 1.41421, 2.0, 2.82843, 4.0
887+
888+
julia> Base.LogRange{Float16}(1, 4, 5)
889+
5-element Base.LogRange{Float16, Float64}:
890+
1.0, 1.414, 2.0, 2.828, 4.0
891+
892+
julia> logrange(1e-310, 1e-300, 11)[1:2:end]
893+
6-element Vector{Float64}:
894+
1.0e-310
895+
9.999999999999974e-309
896+
9.999999999999981e-307
897+
9.999999999999988e-305
898+
9.999999999999994e-303
899+
1.0e-300
900+
901+
julia> prevfloat(1e-308, 5) == ans[2]
902+
true
903+
```
904+
905+
Note that integer eltype `T` is not allowed.
906+
Use for instance `round.(Int, xs)`, or explicit powers of some integer base:
907+
908+
```
909+
julia> xs = logrange(1, 512, 4)
910+
4-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
911+
1.0, 8.0, 64.0, 512.0
912+
913+
julia> 2 .^ (0:3:9) |> println
914+
[1, 8, 64, 512]
915+
```
916+
""" LogRange
917+
918+
struct LogRange{T<:Real,X} <: AbstractArray{T,1}
919+
start::T
920+
stop::T
921+
len::Int
922+
extra::Tuple{X,X}
923+
function LogRange{T}(start::T, stop::T, len::Int) where {T<:Real}
924+
if T <: Integer
925+
# LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998)
926+
throw(ArgumentError("LogRange{T} does not support integer types"))
927+
end
928+
if iszero(start) || iszero(stop)
929+
throw(DomainError((start, stop),
930+
"LogRange cannot start or stop at zero"))
931+
elseif start < 0 || stop < 0
932+
# log would throw, but _log_twice64_unchecked does not
933+
throw(DomainError((start, stop),
934+
"LogRange does not accept negative numbers"))
935+
elseif !isfinite(start) || !isfinite(stop)
936+
throw(DomainError((start, stop),
937+
"LogRange is only defined for finite start & stop"))
938+
elseif len < 0
939+
throw(ArgumentError(string( # LazyString(
940+
"LogRange(", start, ", ", stop, ", ", len, "): can't have negative length")))
941+
elseif len == 1 && start != stop
942+
throw(ArgumentError(string( # LazyString(
943+
"LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1")))
944+
end
945+
ex = _logrange_extra(start, stop, len)
946+
new{T,typeof(ex[1])}(start, stop, len, ex)
947+
end
948+
end
949+
950+
function LogRange{T}(start::Real, stop::Real, len::Integer) where {T}
951+
LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len))
952+
end
953+
function LogRange(start::Real, stop::Real, len::Integer)
954+
T = float(promote_type(typeof(start), typeof(stop)))
955+
LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len))
956+
end
957+
958+
Base.size(r::LogRange) = (r.len,)
959+
Base.length(r::LogRange) = r.len
960+
961+
Base.first(r::LogRange) = r.start
962+
Base.last(r::LogRange) = r.stop
963+
964+
function _logrange_extra(a::Real, b::Real, len::Int)
965+
loga = log(1.0 * a) # widen to at least Float64
966+
logb = log(1.0 * b)
967+
(loga/(len-1), logb/(len-1))
968+
end
969+
970+
function Base.getindex(r::LogRange{T}, i::Int) where {T}
971+
@inline
972+
@boundscheck checkbounds(r, i)
973+
i == 1 && return r.start
974+
i == r.len && return r.stop
975+
# Main path uses Math.exp_impl for TwicePrecision, but is not perfectly
976+
# accurate, hence the special cases for endpoints above.
977+
logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2]
978+
x = _exp_allowing_twice64(logx)
979+
return copysign(T(x), r.start)
980+
end
981+
982+
function Base.show(io::IO, r::LogRange{T}) where {T}
983+
print(io, "LogRange{", T, "}(")
984+
ioc = IOContext(io, :typeinfo => T)
985+
show(ioc, first(r))
986+
print(io, ", ")
987+
show(ioc, last(r))
988+
print(io, ", ")
989+
show(io, length(r))
990+
print(io, ')')
991+
end
992+
993+
# Display LogRange like LinRange -- PR widened signature of print_range to allow this
994+
# function Base.show(io::IO, ::MIME"text/plain", r::LogRange)
995+
# isempty(r) && return show(io, r)
996+
# summary(io, r)
997+
# println(io, ":")
998+
# print_range(io, r, " ", ", ", "", " \u2026 ")
999+
# end
1000+
1001+
_exp_allowing_twice64(x::Number) = exp(x)
1002+
1003+
if VERSION >= v"1.9.0-DEV.318" # Julia PR #44717 allows this high-precision path:
1004+
1005+
_exp_allowing_twice64(x::Base.TwicePrecision{Float64}) = Base.Math.exp_impl(x.hi, x.lo, Val(:ℯ))
1006+
1007+
function _log_twice64_unchecked(x::Float64)
1008+
xu = reinterpret(UInt64, x)
1009+
if xu < (UInt64(1)<<52) # x is subnormal
1010+
xu = reinterpret(UInt64, x * 0x1p52) # normalize x
1011+
xu &= ~Base.sign_mask(Float64)
1012+
xu -= UInt64(52) << 52 # mess with the exponent
1013+
end
1014+
Base.TwicePrecision(Base.Math._log_ext(xu)...)
1015+
end
1016+
1017+
function _logrange_extra(a::Float64, b::Float64, len::Int)
1018+
loga = _log_twice64_unchecked(a)
1019+
logb = _log_twice64_unchecked(b)
1020+
# The reason not to do linear interpolation on log(a)..log(b) in `getindex` is
1021+
# that division of TwicePrecision is quite slow, so do it once on construction:
1022+
(loga/(len-1), logb/(len-1))
1023+
end
1024+
end
1025+
else
1026+
# Ensure that Compat.LogRange is always this struct, not exported from Base
1027+
using Base: LogRange
1028+
end
1029+
8061030
include("deprecated.jl")
8071031

8081032
end # module Compat

test/runtests.jl

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -780,3 +780,68 @@ end
780780
Base.haslength(cycle(0:3, 2)) == false # but not sure we should test these
781781
Base.IteratorSize(cycle(0:3, 2)) == Base.SizeUnknown()
782782
end
783+
784+
# https://github.com/JuliaLang/julia/pull/39071
785+
@testset "logrange" begin
786+
# basic idea
787+
@test logrange(2, 16, 4) [2, 4, 8, 16]
788+
@test logrange(1/8, 8.0, 7) [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0]
789+
@test logrange(1000, 1, 4) [1000, 100, 10, 1]
790+
@test logrange(1, 10^9, 19)[1:2:end] 10 .^ (0:9)
791+
792+
# endpoints
793+
@test logrange(0.1f0, 100, 33)[1] === 0.1f0
794+
@test logrange(0.789, 123_456, 135_790)[[begin, end]] == [0.789, 123_456]
795+
@test logrange(nextfloat(0f0), floatmax(Float32), typemax(Int))[end] === floatmax(Float32)
796+
@test logrange(nextfloat(Float16(0)), floatmax(Float16), 66_000)[end] === floatmax(Float16)
797+
@test first(logrange(pi, 2pi, 3000)) === logrange(pi, 2pi, 3000)[1] === Float64(pi)
798+
if Int == Int64
799+
@test logrange(0.1, 1000, 2^54)[end] === 1000.0
800+
end
801+
802+
# empty, only, constant
803+
@test first(logrange(1, 2, 0)) === 1.0
804+
@test last(logrange(1, 2, 0)) === 2.0
805+
@test collect(logrange(1, 2, 0)) == Float64[]
806+
@test only(logrange(2pi, 2pi, 1)) === logrange(2pi, 2pi, 1)[1] === 2pi
807+
@test logrange(1, 1, 3) == fill(1.0, 3)
808+
809+
# subnormal Float64
810+
x = logrange(1e-320, 1e-300, 21) .* 1e300
811+
@test x logrange(1e-20, 1, 21) rtol=1e-6
812+
813+
# types
814+
@test eltype(logrange(1, 10, 3)) == Float64
815+
@test eltype(logrange(1, 10, Int32(3))) == Float64
816+
@test eltype(logrange(1, 10f0, 3)) == Float32
817+
@test eltype(logrange(1f0, 10, 3)) == Float32
818+
@test eltype(logrange(1, big(10), 3)) == BigFloat
819+
@test logrange(big"0.3", big(pi), 50)[1] == big"0.3"
820+
@test logrange(big"0.3", big(pi), 50)[end] == big(pi)
821+
822+
# more constructors
823+
@test logrange(1,2,length=3) === Compat.LogRange(1,2,3) == Compat.LogRange{Float64}(1,2,3)
824+
@test logrange(1f0, 2f0, length=3) == Compat.LogRange{Float32}(1,2,3)
825+
826+
# errors
827+
@test_throws UndefKeywordError logrange(1, 10) # no default length
828+
@test_throws ArgumentError logrange(1, 10, -1) # negative length
829+
@test_throws ArgumentError logrange(1, 10, 1) # endpoints must not differ
830+
@test_throws DomainError logrange(1, -1, 3) # needs complex numbers
831+
@test_throws DomainError logrange(-1, -2, 3) # not supported, for now
832+
@test_throws MethodError logrange(1, 2+3im, length=4) # not supported, for now
833+
@test_throws ArgumentError logrange(1, 10, 2)[true] # bad index
834+
@test_throws BoundsError logrange(1, 10, 2)[3]
835+
@test_throws ArgumentError Compat.LogRange{Int}(1,4,5) # no integer ranges
836+
@test_throws MethodError Compat.LogRange(1,4, length=5) # type does not take keyword
837+
# (not sure if these should ideally be DomainError or ArgumentError)
838+
@test_throws DomainError logrange(1, Inf, 3)
839+
@test_throws DomainError logrange(0, 2, 3)
840+
@test_throws DomainError logrange(1, NaN, 3)
841+
@test_throws DomainError logrange(NaN, 2, 3)
842+
843+
# printing
844+
@test repr(Compat.LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" # like 2-arg show
845+
@test_skip repr("text/plain", Compat.LogRange(1,2,3)) == "3-element Compat.LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0"
846+
@test_skip repr("text/plain", Compat.LogRange(1,2,0)) == "LogRange{Float64}(1.0, 2.0, 0)" # empty case
847+
end

0 commit comments

Comments
 (0)