diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..d2944f6 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# runic formatting +9006272 + diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 0a55892..d7157e9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,8 +13,7 @@ jobs: fail-fast: false matrix: version: - - '1.6' - - '1.1' + - 'min' - '1' # - 'nightly' os: @@ -23,7 +22,7 @@ jobs: - x64 steps: - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} diff --git a/.gitignore b/.gitignore index 49541d3..a174b0a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.jl.mem deps/deps.jl Manifest.toml +Manifest-v*.toml diff --git a/Project.toml b/Project.toml index 55c42c7..510a0c7 100644 --- a/Project.toml +++ b/Project.toml @@ -1,20 +1,26 @@ name = "CenterIndexedArrays" uuid = "46a7138f-0d70-54e1-8ada-fb8296f91f24" +version = "1.0.0" authors = ["Tim Holy "] -version = "0.2.4" [deps] Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" [compat] +Aqua = "0.8" +ExplicitImports = "1" Interpolations = "0.11, 0.12, 0.13, 0.14, 0.15, 0.16" OffsetArrays = "0.10, 0.11, 1" -julia = "1" +Random = "1" +Test = "1" +julia = "1.10" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Random", "Test"] +test = ["Aqua", "ExplicitImports", "Random", "Test"] diff --git a/README.md b/README.md index 0726c92..51bc9ba 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,98 @@ # CenterIndexedArrays -[![Build Status](https://travis-ci.org/HolyLab/CenterIndexedArrays.jl.svg?branch=master)](https://travis-ci.org/HolyLab/CenterIndexedArrays.jl) +[![CI](https://github.com/HolyLab/CenterIndexedArrays.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/HolyLab/CenterIndexedArrays.jl/actions/workflows/CI.yml) +[![codecov](https://codecov.io/gh/HolyLab/CenterIndexedArrays.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/HolyLab/CenterIndexedArrays.jl) +[![version](https://juliahub.com/docs/General/CenterIndexedArrays/stable/version.svg)](https://juliahub.com/ui/Packages/General/CenterIndexedArrays) +[![Aqua QA](https://juliatesting.github.io/Aqua.jl/dev/assets/badge.svg)](https://github.com/JuliaTesting/Aqua.jl) -A CenterIndexedArray is an array indexed symmetrically around its midpoint. -Here's a quick demo: +A `CenterIndexedArray` is an array indexed symmetrically around its midpoint: the center +element is always at index `(0, 0, …)`, and along each dimension of size `2n+1` the valid +indices run from `-n` to `n`. All dimension sizes must be odd. + +A common use case is image registration, where the mismatch between two images is stored as +a function of their relative displacement — the center of that array is naturally the +zero-displacement case. + +## Installation ```julia +using Pkg +Pkg.add("CenterIndexedArrays") +``` + +## Basic usage + +```jldoctest +julia> using CenterIndexedArrays + julia> A = CenterIndexedArray(reshape(1:15, 3, 5)) 3×5 CenterIndexedArray(reshape(::UnitRange{Int64}, 3, 5)) with eltype Int64 with indices SymRange(1)×SymRange(2): 1 4 7 10 13 2 5 8 11 14 3 6 9 12 15 -julia> A[0, 0] # the center point +julia> A[0, 0] # center element 8 -julia> A[0, -1] +julia> A[0, -1] # one step left of center 5 + +julia> A[-1, 2] # one step above, two steps right +13 ``` -An example application is in image registration, to encode the mismatch between two images as you displace them relative to one another. +You can also allocate an uninitialized array: -The axes, `SymRange`, are symmetric ranges. They too are indexed symmetrically around 0: +```jldoctest +julia> using CenterIndexedArrays -```julia -julia> r = CenterIndexedArrays.SymRange(3) +julia> B = CenterIndexedArray{Float64}(undef, 3, 5); + +julia> axes(B) +(SymRange(1), SymRange(2)) + +julia> size(B) +(3, 5) +``` + +## SymRange axes + +The axes of a `CenterIndexedArray` are `SymRange` values — unit ranges symmetric around +zero. `SymRange(n)` covers `-n:n` and has length `2n+1`. + +```jldoctest +julia> using CenterIndexedArrays: SymRange + +julia> r = SymRange(3) SymRange(3) julia> length(r) 7 -julia> r[7] -ERROR: BoundsError: attempt to access 7-element SymRange with indices SymRange(3) at index [7] -Stacktrace: - [1] throw_boundserror(::SymRange, ::Int64) at ./abstractarray.jl:538 - [2] getindex(::SymRange, ::Int64) at /home/tim/.julia/dev/CenterIndexedArrays/src/symrange.jl:28 - [3] top-level scope at none:0 +julia> first(r), last(r) +(-3, 3) + +julia> r[-3], r[3] +(-3, 3) +``` + +## Interpolation + +Wrapping an `Interpolations.jl` interpolation object enables fractional (sub-integer) +indexing, which is useful when computing cross-correlations at non-integer offsets. + +```jldoctest +julia> using CenterIndexedArrays, Interpolations + +julia> dat = collect(reshape(1.0:25.0, 5, 5)); + +julia> itp = interpolate(dat, BSpline(Linear())); + +julia> A = CenterIndexedArray(itp); -julia> r[-3] --3 +julia> A[0, 0] # center, equivalent to dat[3, 3] +13.0 -julia> r[3] -3 +julia> A[0.5, 0.0] # fractional index; linearly interpolated +13.5 ``` diff --git a/src/CenterIndexedArrays.jl b/src/CenterIndexedArrays.jl index f58d354..07e4205 100644 --- a/src/CenterIndexedArrays.jl +++ b/src/CenterIndexedArrays.jl @@ -1,18 +1,30 @@ +""" + CenterIndexedArrays + +Provides `CenterIndexedArray`, an array type whose center element is at +index `(0, 0, …)`. Valid indices along each dimension of size `2n+1` +run from `-n` to `n`. +""" module CenterIndexedArrays -using Interpolations, OffsetArrays -using OffsetArrays: IdentityUnitRange +using Interpolations: Interpolations, AbstractInterpolation +using OffsetArrays: OffsetArrays, OffsetArray export CenterIndexedArray include("symrange.jl") """ -A `CenterIndexedArray` is one for which the array center has indexes -`0,0,...`. Along each coordinate, allowed indexes range from `-n:n`. + CenterIndexedArray(A::AbstractArray) + CenterIndexedArray{T}(undef, dims...) + CenterIndexedArray{T,N}(undef, dims...) -CenterIndexedArray(A) "converts" `A` into a CenterIndexedArray. All -the sizes of `A` must be odd. +An array wrapper that re-indexes around zero: the center element is at +index `(0, 0, …)`, and along each dimension of size `2n+1` the valid +indices run from `-n` to `n`. All dimension sizes must be odd. + +The first form wraps `A` without copying. The `undef` forms allocate a +new `Array{T}` with the given dimensions (each of which must be odd). """ struct CenterIndexedArray{T, N, A <: AbstractArray} <: AbstractArray{T, N} data::A @@ -53,7 +65,7 @@ end # This is incomplete: ideally we wouldn't need SymAx in the first slot # as long as there was at least one SymAx. -function Base.similar(A::CenterIndexedArray, ::Type{T}, inds::Tuple{SymAx, Vararg{Union{Int, <:IdentityUnitRange, SymAx}}}) where {T} +function Base.similar(A::CenterIndexedArray, ::Type{T}, inds::Tuple{SymAx, Vararg{Union{Int, <:Base.IdentityUnitRange, SymAx}}}) where {T} torange(n) = isa(n, Int) ? Base.OneTo(n) : n return OffsetArray{T}(undef, map(torange, inds)) end @@ -105,6 +117,4 @@ function Base.showarg(io::IO, A::CenterIndexedArray, toplevel) return toplevel && print(io, " with eltype ", eltype(A)) end -include("deprecated.jl") - end # module diff --git a/src/deprecated.jl b/src/deprecated.jl deleted file mode 100644 index d8df79c..0000000 --- a/src/deprecated.jl +++ /dev/null @@ -1,2 +0,0 @@ -@deprecate CenterIndexedArray(::Type{T}, dims) where {T} CenterIndexedArray{T}(undef, dims...) -@deprecate CenterIndexedArray(::Type{T}, dims...) where {T} CenterIndexedArray{T}(undef, dims...) diff --git a/src/symrange.jl b/src/symrange.jl index 2475288..0380908 100644 --- a/src/symrange.jl +++ b/src/symrange.jl @@ -17,16 +17,6 @@ Base.axes(r::SymRange) = (r,) @inline Base.unsafe_indices(r::SymRange) = (r,) -function iterate(r::SymRange) - r.n == 0 && return nothing - return first(r), first(r) -end - -function iterate(r::SymRange, s) - s == last(r) && return nothing - return copy(s + 1), s + 1 -end - @inline function Base.getindex(v::SymRange, i::Int) @boundscheck abs(i) <= v.n || Base.throw_boundserror(v, i) return i diff --git a/test/runtests.jl b/test/runtests.jl index 1599f8c..7fdee92 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,8 @@ using Interpolations, OffsetArrays using Test, Random using OffsetArrays: IdentityUnitRange +using Aqua +using ExplicitImports if !isdefined(@__MODULE__, :ambs) const ambs = detect_ambiguities(Base, Interpolations, OffsetArrays) @@ -10,6 +12,14 @@ end using CenterIndexedArrays using CenterIndexedArrays: SymRange +@testset "Aqua" begin + Aqua.test_all(CenterIndexedArrays) +end + +@testset "ExplicitImports" begin + test_explicit_imports(CenterIndexedArrays; all_qualified_accesses_are_public=false) +end + @testset "Ambiguities" begin ambscia = detect_ambiguities(Base, Interpolations, OffsetArrays, CenterIndexedArrays) VERSION >= v"1.1" && @test isempty(setdiff(ambscia, ambs)) @@ -46,6 +56,12 @@ end io = IOBuffer() print(io, r) @test String(take!(io)) == "SymRange(3)" + + # Non-Int Integer constructor + @test SymRange(Int32(3)) === SymRange(3) + + # SymRange(0) contains a single element, 0 + @test collect(SymRange(0)) == [0] end @testset "Uninitialized" begin @@ -174,3 +190,17 @@ end @test A + A == CenterIndexedArray(dat + dat) @test isa(A .+ 1, CenterIndexedArray) end + +@testset "Interpolations" begin + dat = rand(5, 7) + itp = interpolate(dat, BSpline(Linear())) + A = CenterIndexedArray(itp) + # Integer indexing + @test A[0, 0] ≈ dat[3, 4] + @test A[-2, -3] ≈ dat[1, 1] + @test @inferred(A[1, 2]) ≈ dat[4, 6] + # Fractional (non-integer) indexing + @test A[0.0, 0.0] ≈ dat[3, 4] + @test A[0.5, 0.5] ≈ itp(3.5, 4.5) + @test_throws BoundsError A[3, 0] +end