Skip to content

Commit 8912805

Browse files
mcabbottKristofferC
authored andcommitted
Add a lazy logrange function and LogRange type (#39071)
(cherry picked from commit 3ed2b49)
1 parent 0520b80 commit 8912805

File tree

7 files changed

+297
-4
lines changed

7 files changed

+297
-4
lines changed

NEWS.md

+1
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ New library functions
8383

8484
* `in!(x, s::AbstractSet)` will return whether `x` is in `s`, and insert `x` in `s` if not.
8585
* The new `Libc.mkfifo` function wraps the `mkfifo` C function on Unix platforms ([#34587]).
86+
* `logrange(start, stop; length)` makes a range of constant ratio, instead of constant step ([#39071])
8687
* `copyuntil(out, io, delim)` and `copyline(out, io)` copy data into an `out::IO` stream ([#48273]).
8788
* `eachrsplit(string, pattern)` iterates split substrings right to left.
8889
* `Sys.username()` can be used to return the current user's username ([#51897]).

base/exports.jl

+2
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,7 @@ export
412412
isperm,
413413
issorted,
414414
last,
415+
logrange,
415416
mapslices,
416417
max,
417418
maximum!,
@@ -1095,6 +1096,7 @@ public
10951096
Generator,
10961097
ImmutableDict,
10971098
OneTo,
1099+
LogRange,
10981100
AnnotatedString,
10991101
AnnotatedChar,
11001102
UUID,

base/range.jl

+186-4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Valid invocations of range are:
7070
* Call `range` with one of `stop` or `length`. `start` and `step` will be assumed to be one.
7171
7272
See Extended Help for additional details on the returned type.
73+
See also [`logrange`](@ref) for logarithmically spaced points.
7374
7475
# Examples
7576
```jldoctest
@@ -252,10 +253,13 @@ end
252253
## 1-dimensional ranges ##
253254

254255
"""
255-
AbstractRange{T}
256+
AbstractRange{T} <: AbstractVector{T}
256257
257-
Supertype for ranges with elements of type `T`.
258-
[`UnitRange`](@ref) and other types are subtypes of this.
258+
Supertype for linear ranges with elements of type `T`.
259+
[`UnitRange`](@ref), [`LinRange`](@ref) and other types are subtypes of this.
260+
261+
All subtypes must define [`step`](@ref).
262+
Thus [`LogRange`](@ref Base.LogRange) is not a subtype of `AbstractRange`.
259263
"""
260264
abstract type AbstractRange{T} <: AbstractArray{T,1} end
261265

@@ -550,6 +554,8 @@ julia> collect(LinRange(-0.1, 0.3, 5))
550554
0.19999999999999998
551555
0.3
552556
```
557+
558+
See also [`Logrange`](@ref Base.LogRange) for logarithmically spaced points.
553559
"""
554560
struct LinRange{T,L<:Integer} <: AbstractRange{T}
555561
start::T
@@ -620,7 +626,7 @@ parameters `pre` and `post` characters for each printed row,
620626
`sep` separator string between printed elements,
621627
`hdots` string for the horizontal ellipsis.
622628
"""
623-
function print_range(io::IO, r::AbstractRange,
629+
function print_range(io::IO, r::AbstractArray,
624630
pre::AbstractString = " ",
625631
sep::AbstractString = ", ",
626632
post::AbstractString = "",
@@ -1488,3 +1494,179 @@ julia> mod(3, 0:2) # mod(3, 3)
14881494
"""
14891495
mod(i::Integer, r::OneTo) = mod1(i, last(r))
14901496
mod(i::Integer, r::AbstractUnitRange{<:Integer}) = mod(i-first(r), length(r)) + first(r)
1497+
1498+
1499+
"""
1500+
logrange(start, stop, length)
1501+
logrange(start, stop; length)
1502+
1503+
Construct a specialized array whose elements are spaced logarithmically
1504+
between the given endpoints. That is, the ratio of successive elements is
1505+
a constant, calculated from the length.
1506+
1507+
This is similar to `geomspace` in Python. Unlike `PowerRange` in Mathematica,
1508+
you specify the number of elements not the ratio.
1509+
Unlike `logspace` in Python and Matlab, the `start` and `stop` arguments are
1510+
always the first and last elements of the result, not powers applied to some base.
1511+
1512+
# Examples
1513+
```jldoctest
1514+
julia> logrange(10, 4000, length=3)
1515+
3-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
1516+
10.0, 200.0, 4000.0
1517+
1518+
julia> ans[2] ≈ sqrt(10 * 4000) # middle element is the geometric mean
1519+
true
1520+
1521+
julia> range(10, 40, length=3)[2] ≈ (10 + 40)/2 # arithmetic mean
1522+
true
1523+
1524+
julia> logrange(1f0, 32f0, 11)
1525+
11-element Base.LogRange{Float32, Float64}:
1526+
1.0, 1.41421, 2.0, 2.82843, 4.0, 5.65685, 8.0, 11.3137, 16.0, 22.6274, 32.0
1527+
1528+
julia> logrange(1, 1000, length=4) ≈ 10 .^ (0:3)
1529+
true
1530+
```
1531+
1532+
See the [`LogRange`](@ref Base.LogRange) type for further details.
1533+
1534+
See also [`range`](@ref) for linearly spaced points.
1535+
1536+
!!! compat "Julia 1.11"
1537+
This function requires at least Julia 1.11.
1538+
"""
1539+
logrange(start::Real, stop::Real, length::Integer) = LogRange(start, stop, Int(length))
1540+
logrange(start::Real, stop::Real; length::Integer) = logrange(start, stop, length)
1541+
1542+
1543+
"""
1544+
LogRange{T}(start, stop, len) <: AbstractVector{T}
1545+
1546+
A range whose elements are spaced logarithmically between `start` and `stop`,
1547+
with spacing controlled by `len`. Returned by [`logrange`](@ref).
1548+
1549+
Like [`LinRange`](@ref), the first and last elements will be exactly those
1550+
provided, but intermediate values may have small floating-point errors.
1551+
These are calculated using the logs of the endpoints, which are
1552+
stored on construction, often in higher precision than `T`.
1553+
1554+
# Examples
1555+
```jldoctest
1556+
julia> logrange(1, 4, length=5)
1557+
5-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
1558+
1.0, 1.41421, 2.0, 2.82843, 4.0
1559+
1560+
julia> Base.LogRange{Float16}(1, 4, 5)
1561+
5-element Base.LogRange{Float16, Float64}:
1562+
1.0, 1.414, 2.0, 2.828, 4.0
1563+
1564+
julia> logrange(1e-310, 1e-300, 11)[1:2:end]
1565+
6-element Vector{Float64}:
1566+
1.0e-310
1567+
9.999999999999974e-309
1568+
9.999999999999981e-307
1569+
9.999999999999988e-305
1570+
9.999999999999994e-303
1571+
1.0e-300
1572+
1573+
julia> prevfloat(1e-308, 5) == ans[2]
1574+
true
1575+
```
1576+
1577+
Note that integer eltype `T` is not allowed.
1578+
Use for instance `round.(Int, xs)`, or explicit powers of some integer base:
1579+
1580+
```jldoctest
1581+
julia> xs = logrange(1, 512, 4)
1582+
4-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:
1583+
1.0, 8.0, 64.0, 512.0
1584+
1585+
julia> 2 .^ (0:3:9) |> println
1586+
[1, 8, 64, 512]
1587+
```
1588+
1589+
!!! compat "Julia 1.11"
1590+
This type requires at least Julia 1.11.
1591+
"""
1592+
struct LogRange{T<:Real,X} <: AbstractArray{T,1}
1593+
start::T
1594+
stop::T
1595+
len::Int
1596+
extra::Tuple{X,X}
1597+
function LogRange{T}(start::T, stop::T, len::Int) where {T<:Real}
1598+
if T <: Integer
1599+
# LogRange{Int}(1, 512, 4) produces InexactError: Int64(7.999999999999998)
1600+
throw(ArgumentError("LogRange{T} does not support integer types"))
1601+
end
1602+
if iszero(start) || iszero(stop)
1603+
throw(DomainError((start, stop),
1604+
"LogRange cannot start or stop at zero"))
1605+
elseif start < 0 || stop < 0
1606+
# log would throw, but _log_twice64_unchecked does not
1607+
throw(DomainError((start, stop),
1608+
"LogRange does not accept negative numbers"))
1609+
elseif !isfinite(start) || !isfinite(stop)
1610+
throw(DomainError((start, stop),
1611+
"LogRange is only defined for finite start & stop"))
1612+
elseif len < 0
1613+
throw(ArgumentError(LazyString(
1614+
"LogRange(", start, ", ", stop, ", ", len, "): can't have negative length")))
1615+
elseif len == 1 && start != stop
1616+
throw(ArgumentError(LazyString(
1617+
"LogRange(", start, ", ", stop, ", ", len, "): endpoints differ, while length is 1")))
1618+
end
1619+
ex = _logrange_extra(start, stop, len)
1620+
new{T,typeof(ex[1])}(start, stop, len, ex)
1621+
end
1622+
end
1623+
1624+
function LogRange{T}(start::Real, stop::Real, len::Integer) where {T}
1625+
LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len))
1626+
end
1627+
function LogRange(start::Real, stop::Real, len::Integer)
1628+
T = float(promote_type(typeof(start), typeof(stop)))
1629+
LogRange{T}(convert(T, start), convert(T, stop), convert(Int, len))
1630+
end
1631+
1632+
size(r::LogRange) = (r.len,)
1633+
length(r::LogRange) = r.len
1634+
1635+
first(r::LogRange) = r.start
1636+
last(r::LogRange) = r.stop
1637+
1638+
function _logrange_extra(a::Real, b::Real, len::Int)
1639+
loga = log(1.0 * a) # widen to at least Float64
1640+
logb = log(1.0 * b)
1641+
(loga/(len-1), logb/(len-1))
1642+
end
1643+
function _logrange_extra(a::Float64, b::Float64, len::Int)
1644+
loga = _log_twice64_unchecked(a)
1645+
logb = _log_twice64_unchecked(b)
1646+
# The reason not to do linear interpolation on log(a)..log(b) in `getindex` is
1647+
# that division of TwicePrecision is quite slow, so do it once on construction:
1648+
(loga/(len-1), logb/(len-1))
1649+
end
1650+
1651+
function getindex(r::LogRange{T}, i::Int) where {T}
1652+
@inline
1653+
@boundscheck checkbounds(r, i)
1654+
i == 1 && return r.start
1655+
i == r.len && return r.stop
1656+
# Main path uses Math.exp_impl for TwicePrecision, but is not perfectly
1657+
# accurate, hence the special cases for endpoints above.
1658+
logx = (r.len-i) * r.extra[1] + (i-1) * r.extra[2]
1659+
x = _exp_allowing_twice64(logx)
1660+
return T(x)
1661+
end
1662+
1663+
function show(io::IO, r::LogRange{T}) where {T}
1664+
print(io, "LogRange{", T, "}(")
1665+
ioc = IOContext(io, :typeinfo => T)
1666+
show(ioc, first(r))
1667+
print(io, ", ")
1668+
show(ioc, last(r))
1669+
print(io, ", ")
1670+
show(io, length(r))
1671+
print(io, ')')
1672+
end

base/show.jl

+7
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ function show(io::IO, ::MIME"text/plain", r::LinRange)
2323
print_range(io, r)
2424
end
2525

26+
function show(io::IO, ::MIME"text/plain", r::LogRange) # display LogRange like LinRange
27+
isempty(r) && return show(io, r)
28+
summary(io, r)
29+
println(io, ":")
30+
print_range(io, r, " ", ", ", "", " \u2026 ")
31+
end
32+
2633
function _isself(ft::DataType)
2734
ftname = ft.name
2835
isdefined(ftname, :mt) || return false

base/twiceprecision.jl

+16
Original file line numberDiff line numberDiff line change
@@ -785,3 +785,19 @@ _tp_prod(t::TwicePrecision) = t
785785
x.hi < y.hi || ((x.hi == y.hi) & (x.lo < y.lo))
786786

787787
isbetween(a, x, b) = a <= x <= b || b <= x <= a
788+
789+
# These functions exist for use in LogRange:
790+
791+
_exp_allowing_twice64(x::Number) = exp(x)
792+
_exp_allowing_twice64(x::TwicePrecision{Float64}) = Math.exp_impl(x.hi, x.lo, Val(:ℯ))
793+
794+
# No error on negative x, and for NaN/Inf this returns junk:
795+
function _log_twice64_unchecked(x::Float64)
796+
xu = reinterpret(UInt64, x)
797+
if xu < (UInt64(1)<<52) # x is subnormal
798+
xu = reinterpret(UInt64, x * 0x1p52) # normalize x
799+
xu &= ~sign_mask(Float64)
800+
xu -= UInt64(52) << 52 # mess with the exponent
801+
end
802+
TwicePrecision(Math._log_ext(xu)...)
803+
end

doc/src/base/math.md

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ Base.:(:)
3939
Base.range
4040
Base.OneTo
4141
Base.StepRangeLen
42+
Base.logrange
43+
Base.LogRange
4244
Base.:(==)
4345
Base.:(!=)
4446
Base.:(!==)

test/ranges.jl

+83
Original file line numberDiff line numberDiff line change
@@ -2603,3 +2603,86 @@ end
26032603
errmsg = ("deliberately unsupported for CartesianIndex", "StepRangeLen")
26042604
@test_throws errmsg range(CartesianIndex(1), step=CartesianIndex(1), length=3)
26052605
end
2606+
2607+
@testset "logrange" begin
2608+
# basic idea
2609+
@test logrange(2, 16, 4) [2, 4, 8, 16]
2610+
@test logrange(1/8, 8.0, 7) [0.125, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0]
2611+
@test logrange(1000, 1, 4) [1000, 100, 10, 1]
2612+
@test logrange(1, 10^9, 19)[1:2:end] 10 .^ (0:9)
2613+
2614+
# endpoints
2615+
@test logrange(0.1f0, 100, 33)[1] === 0.1f0
2616+
@test logrange(0.789, 123_456, 135_790)[[begin, end]] == [0.789, 123_456]
2617+
@test logrange(nextfloat(0f0), floatmax(Float32), typemax(Int))[end] === floatmax(Float32)
2618+
@test logrange(nextfloat(Float16(0)), floatmax(Float16), 66_000)[end] === floatmax(Float16)
2619+
@test first(logrange(pi, 2pi, 3000)) === logrange(pi, 2pi, 3000)[1] === Float64(pi)
2620+
if Int == Int64
2621+
@test logrange(0.1, 1000, 2^54)[end] === 1000.0
2622+
end
2623+
2624+
# empty, only, constant
2625+
@test first(logrange(1, 2, 0)) === 1.0
2626+
@test last(logrange(1, 2, 0)) === 2.0
2627+
@test collect(logrange(1, 2, 0)) == Float64[]
2628+
@test only(logrange(2pi, 2pi, 1)) === logrange(2pi, 2pi, 1)[1] === 2pi
2629+
@test logrange(1, 1, 3) == fill(1.0, 3)
2630+
2631+
# subnormal Float64
2632+
x = logrange(1e-320, 1e-300, 21) .* 1e300
2633+
@test x logrange(1e-20, 1, 21) rtol=1e-6
2634+
2635+
# types
2636+
@test eltype(logrange(1, 10, 3)) == Float64
2637+
@test eltype(logrange(1, 10, Int32(3))) == Float64
2638+
@test eltype(logrange(1, 10f0, 3)) == Float32
2639+
@test eltype(logrange(1f0, 10, 3)) == Float32
2640+
@test eltype(logrange(1, big(10), 3)) == BigFloat
2641+
@test logrange(big"0.3", big(pi), 50)[1] == big"0.3"
2642+
@test logrange(big"0.3", big(pi), 50)[end] == big(pi)
2643+
2644+
# more constructors
2645+
@test logrange(1,2,length=3) === Base.LogRange(1,2,3) == Base.LogRange{Float64}(1,2,3)
2646+
@test logrange(1f0, 2f0, length=3) == Base.LogRange{Float32}(1,2,3)
2647+
2648+
# errors
2649+
@test_throws UndefKeywordError logrange(1, 10) # no default length
2650+
@test_throws ArgumentError logrange(1, 10, -1) # negative length
2651+
@test_throws ArgumentError logrange(1, 10, 1) # endpoints must not differ
2652+
@test_throws DomainError logrange(1, -1, 3) # needs complex numbers
2653+
@test_throws DomainError logrange(-1, -2, 3) # not supported, for now
2654+
@test_throws MethodError logrange(1, 2+3im, length=4) # not supported, for now
2655+
@test_throws ArgumentError logrange(1, 10, 2)[true] # bad index
2656+
@test_throws BoundsError logrange(1, 10, 2)[3]
2657+
@test_throws ArgumentError Base.LogRange{Int}(1,4,5) # no integer ranges
2658+
@test_throws MethodError Base.LogRange(1,4, length=5) # type does not take keyword
2659+
# (not sure if these should ideally be DomainError or ArgumentError)
2660+
@test_throws DomainError logrange(1, Inf, 3)
2661+
@test_throws DomainError logrange(0, 2, 3)
2662+
@test_throws DomainError logrange(1, NaN, 3)
2663+
@test_throws DomainError logrange(NaN, 2, 3)
2664+
2665+
# printing
2666+
@test repr(Base.LogRange(1,2,3)) == "LogRange{Float64}(1.0, 2.0, 3)" # like 2-arg show
2667+
@test repr("text/plain", Base.LogRange(1,2,3)) == "3-element Base.LogRange{Float64, Base.TwicePrecision{Float64}}:\n 1.0, 1.41421, 2.0"
2668+
@test repr("text/plain", Base.LogRange(1,2,0)) == "LogRange{Float64}(1.0, 2.0, 0)" # empty case
2669+
end
2670+
2671+
@testset "_log_twice64_unchecked" begin
2672+
# it roughly works
2673+
@test big(Base._log_twice64_unchecked(exp(1))) 1.0
2674+
@test big(Base._log_twice64_unchecked(exp(123))) 123.0
2675+
2676+
# it gets high accuracy
2677+
@test abs(big(log(4.0)) - log(big(4.0))) < 1e-16
2678+
@test abs(big(Base._log_twice64_unchecked(4.0)) - log(big(4.0))) < 1e-30
2679+
2680+
# it handles subnormals
2681+
@test abs(big(Base._log_twice64_unchecked(1e-310)) - log(big(1e-310))) < 1e-20
2682+
2683+
# it accepts negative, NaN, etc without complaint:
2684+
@test Base._log_twice64_unchecked(-0.0).lo isa Float64
2685+
@test Base._log_twice64_unchecked(-1.23).lo isa Float64
2686+
@test Base._log_twice64_unchecked(NaN).lo isa Float64
2687+
@test Base._log_twice64_unchecked(Inf).lo isa Float64
2688+
end

0 commit comments

Comments
 (0)