Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# runic formatting
9006272

5 changes: 2 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ jobs:
fail-fast: false
matrix:
version:
- '1.6'
- '1.1'
- 'min'
- '1'
# - 'nightly'
os:
Expand All @@ -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 }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
*.jl.mem
deps/deps.jl
Manifest.toml
Manifest-v*.toml
12 changes: 9 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
name = "CenterIndexedArrays"
uuid = "46a7138f-0d70-54e1-8ada-fb8296f91f24"
version = "1.0.0"
authors = ["Tim Holy <tim.holy@gmail.com>"]
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"]
91 changes: 72 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```
28 changes: 19 additions & 9 deletions src/CenterIndexedArrays.jl
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
2 changes: 0 additions & 2 deletions src/deprecated.jl

This file was deleted.

10 changes: 0 additions & 10 deletions src/symrange.jl
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,6 @@ Base.axes(r::SymRange) = (r,)

@inline Base.unsafe_indices(r::SymRange) = (r,)

function iterate(r::SymRange)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the motivation for deleting these that they're redundant with the generic method for AbstractUnitRange? That's quite plausible, and deleting code is always nice.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nvm I just saw edf0fbd. Wow, a good catch!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Claude suggested this.

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
Expand Down
30 changes: 30 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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))
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Loading