diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..0c6a354 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,23 @@ +{ + "permissions": { + "allow": [ + "Bash(julia --project=. -e 'using Pkg; Pkg.test\\(\\)')" + ] + } +} +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import sys,json; d=json.load(sys.stdin); print(d['tool_input'].get('file_path',''))\" | { read -r f; [[ \"$f\" == *.jl ]] && runic -i \"$f\"; } 2>/dev/null || true", + "statusMessage": "Formatting Julia with Runic..." + } + ] + } + ] + } +} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..c32bbdf --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# runic formatting +fbb2336 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6ac1e04..7f73548 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,15 +13,15 @@ jobs: fail-fast: false matrix: version: - - '1.1' + - 'min' - '1' os: - ubuntu-latest arch: - x64 steps: - - uses: actions/checkout@v2 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} @@ -40,6 +40,6 @@ jobs: - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v4 with: file: lcov.info 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 53143e8..bab9a46 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,24 @@ name = "RegisterUtilities" uuid = "d4862ba2-f42c-5aeb-af4f-96a8884a16c4" +version = "1.0.0" authors = ["Tim Holy "] -version = "0.1.0" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434" [compat] -julia = "^1.1" +Aqua = "0.8" +ExplicitImports = "1" +LinearAlgebra = "1" +RegisterCore = "0.2" +Test = "1" +julia = "1.10" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Aqua", "ExplicitImports", "Test"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..09d1209 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# RegisterUtilities.jl + +[![CI](https://github.com/HolyLab/RegisterUtilities.jl/actions/workflows/CI.yml/badge.svg)](https://github.com/HolyLab/RegisterUtilities.jl/actions/workflows/CI.yml) +[![codecov](https://codecov.io/gh/HolyLab/RegisterUtilities.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/HolyLab/RegisterUtilities.jl) +[![version](https://juliahub.com/docs/General/RegisterUtilities/stable/version.svg)](https://juliahub.com/ui/Packages/General/RegisterUtilities) + +Utility types and functions for image-registration workflows in the +[HolyLab](https://github.com/HolyLab) ecosystem. + +## Installation + +This package depends on +[RegisterCore.jl](https://github.com/HolyLab/RegisterCore.jl), which lives in +the [HolyLab registry](https://github.com/HolyLab/HolyLabRegistry). Add that +registry once before installing: + +```julia +using Pkg +pkg"registry add General https://github.com/HolyLab/HolyLabRegistry.git" +Pkg.add("RegisterUtilities") +``` + +## Usage + +### `Counter` — column-major multi-dimensional index iterator + +`Counter` yields every `Vector{Int}` index from `[1,1,…,1]` to `max` in +column-major (first-index-fastest) order. Unlike `CartesianIndices`, the +yielded values are plain vectors, which is convenient for arithmetic. + +```julia +using RegisterUtilities + +c = Counter((2, 3)) +collect(c) +# [[1,1],[2,1],[1,2],[2,2],[1,3],[2,3]] + +length(Counter((4, 5, 6))) # 120 +``` + +### `block_center` — center coordinate of a block + +Returns the 1-based center of a block with the given dimensions, using the +same convention as `MismatchArray` blocks: center of dimension `i` is +`(sz[i] >> 1) + 1`. + +```julia +block_center(5) # (3,) +block_center(4, 6) # (3, 4) +block_center(5, 5, 5) # (3, 3, 3) +``` + +### `quadratic` — quadratic-form array + +Fills a 2-D array where each element `(i,j)` equals `uᵀ Q u`, with +`u = [i,j] - center` and `center = block_center(m,n) .+ shift`. The second +method wraps the result in a `MismatchArray` with a given denominator. + +```julia +using RegisterUtilities, LinearAlgebra + +Q = [1.0 0.0; 0.0 1.0] # identity — distance² from center +A = quadratic(5, 5, [0, 0], Q) +# 5×5 matrix of squared distances from the block center (3, 3) + +using RegisterCore +denom = ones(5, 5) +ma = quadratic(denom, [0, 0], Q) # returns a MismatchArray +``` + +### `tighten` — narrow element type + +Converts a heterogeneous or overly-wide array (e.g. `Array{Real}`) to the +narrowest concrete type that holds all its values. + +```julia +A = Real[1, 2.0, 3f0] +B = tighten(A) +eltype(B) # Float64 +``` diff --git a/src/RegisterUtilities.jl b/src/RegisterUtilities.jl index c670395..8a27ee8 100644 --- a/src/RegisterUtilities.jl +++ b/src/RegisterUtilities.jl @@ -1,22 +1,53 @@ +""" + RegisterUtilities + +Utility types and functions for image-registration workflows. + +Exports: +- [`Counter`](@ref): column-major multi-dimensional index iterator. +- [`quadratic`](@ref): construct a quadratic-form array or `MismatchArray`. +- [`block_center`](@ref): compute the center coordinate of a block. +- [`tighten`](@ref): narrow an array to its tightest concrete element type. +""" module RegisterUtilities export Counter -#### Counter #### -# -# Stolen from Grid.jl. Useful when you want to do more math on the iterator. +""" + Counter(max::AbstractVector{<:Integer}) + Counter(sz::Tuple) + +An iterator that yields every `Vector{Int}` index tuple from `[1, 1, …, 1]` +to `max` (or `collect(sz)`), traversing in column-major (first-index-fastest) +order. + +Unlike `CartesianIndices`, `Counter` yields plain `Vector{Int}` values, making +it convenient when the index needs to be used in arithmetic expressions. + +`length(c)` returns the total number of elements (`prod(max)`), or `0` if any +dimension is ≤ 0. +# Examples +```julia +c = Counter((2, 3)) +collect(c) # [[1,1],[2,1],[1,2],[2,2],[1,3],[2,3]] +``` +""" struct Counter max::Vector{Int} end Counter(sz::Tuple) = Counter(Int[sz...]) +Counter(max::AbstractVector{<:Integer}) = Counter(convert(Vector{Int}, max)) + +Base.length(c::Counter) = isempty(c.max) || any(<=(0), c.max) ? 0 : prod(c.max) + function Base.iterate(c::Counter) N = length(c.max) (N == 0 || any(c.max .<= 0)) && return nothing state = ones(Int, N) return copy(state), state end -function Base.iterate(c::Counter, state) +function Base.iterate(c::Counter, state::Vector{Int}) state[1] += 1 i = 1 while state[i] > c.max[i] && i < length(state) @@ -30,15 +61,37 @@ end # Below functions are from RegisterTestUtilities -using RegisterCore, LinearAlgebra +using LinearAlgebra: LinearAlgebra, dot +using RegisterCore: RegisterCore, MismatchArray export quadratic, block_center, tighten +""" + quadratic(m, n, shift, Q) + quadratic(denom::AbstractMatrix, shift, Q) + +Construct a 2-D array of quadratic-form values centered at a shifted block +center. + +For each pixel `(i, j)` the value is `uᵀ Q u`, where `u = [i, j] - center` +and `center = block_center(m, n) .+ shift`. + +The second method wraps the result in a `MismatchArray` using `denom` as the +denominator array; `m` and `n` are taken from `size(denom)`. + +# Arguments +- `m`, `n`: output array dimensions (rows, columns). +- `denom`: an existing `AbstractMatrix` whose size sets the output dimensions + and whose values become the denominator of the returned `MismatchArray`. +- `shift`: 2-element offset added to the block center. +- `Q`: 2×2 matrix defining the quadratic form. +""" function quadratic(m, n, shift, Q) - A = zeros(m, n) + T = float(eltype(Q)) + A = zeros(T, m, n) c = block_center(m, n) cntr = [shift[1] + c[1], shift[2] + c[2]] - u = zeros(2) + u = zeros(T, 2) for j in 1:n, i in 1:m u[1], u[2] = i - cntr[1], j - cntr[2] A[i, j] = dot(u, Q * u) @@ -46,12 +99,45 @@ function quadratic(m, n, shift, Q) return A end -quadratic(shift, Q, denom::Matrix) = MismatchArray(quadratic(size(denom)..., shift, Q), denom) +quadratic(denom::AbstractMatrix, shift, Q) = MismatchArray(quadratic(size(denom)..., shift, Q), denom) + +""" + block_center(sz...) + +Return the 1-based center coordinate of a block with dimensions `sz` as an +`NTuple`. +The center of dimension `i` is computed as `(sz[i] >> 1) + 1`, matching the +convention used by `MismatchArray` blocks. + +# Examples +```julia +block_center(5) # (3,) +block_center(4, 6) # (3, 4) +block_center(5, 5, 5) # (3, 3, 3) +``` +""" function block_center(sz...) - return ntuple(i -> sz[i] >> 1 + 1, length(sz)) + return ntuple(i -> (sz[i] >> 1) + 1, length(sz)) end +""" + tighten(A::AbstractArray) + +Return a copy of `A` with the narrowest element type that can hold all of its +values. + +Iterates over every element of `A` and accumulates a common type via +`promote_type`, then copies `A` into a new array of that type. Useful for +converting heterogeneous or overly-wide arrays (e.g. `Array{Real}`) into a +concrete, efficient representation. + +# Example +```julia +A = Real[1, 2.0, 3f0] # element type Real +B = tighten(A) # element type Float64 +``` +""" function tighten(A::AbstractArray) T = typeof(first(A)) for a in A diff --git a/test/runtests.jl b/test/runtests.jl index dc401c6..f81eb23 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,18 @@ using Test +using Aqua +using ExplicitImports +using LinearAlgebra: I +using RegisterCore: RegisterCore using RegisterUtilities +@testset "Aqua" begin + Aqua.test_all(RegisterUtilities) +end + +@testset "ExplicitImports" begin + test_explicit_imports(RegisterUtilities) +end + @testset "Counter test" begin for empty_gridsize in ((), (0,), (1, 0), (1, -1), (1, 0, 1)) for c in Counter(empty_gridsize) @@ -24,4 +36,50 @@ using RegisterUtilities end @test cnt == index_vec end + # AbstractVector constructor + cnt32 = [c for c in Counter(Int32[2, 3])] + @test cnt32 == [[1,1],[2,1],[1,2],[2,2],[1,3],[2,3]] +end + +@testset "block_center" begin + @test block_center(1) == (1,) + @test block_center(2) == (2,) + @test block_center(4) == (3,) + @test block_center(5) == (3,) + @test block_center(4, 6) == (3, 4) + @test block_center(8) == (5,) +end + +@testset "quadratic" begin + Q = Matrix(1.0I, 2, 2) + m, n = 5, 7 + A = quadratic(m, n, (0, 0), Q) + @test size(A) == (m, n) + c = block_center(m, n) + @test A[c...] == 0.0 + @test all(>=(0), A) + @inferred quadratic(m, n, (0, 0), Q) + + # shifting the center moves the zero + A2 = quadratic(m, n, (1, 0), Q) + @test A2[c[1] + 1, c[2]] == 0.0 + + # matrix-denom variant returns a MismatchArray of the right size + denom = ones(m, n) + result = quadratic(denom, (0, 0), Q) + @test result isa RegisterCore.MismatchArray + @test size(result) == (m, n) +end + +@testset "tighten" begin + # heterogeneous Any-array gets promoted + A = Any[1, 2.0, 3f0] + result = tighten(A) + @test eltype(result) == Float64 + @test result ≈ [1.0, 2.0, 3.0] + + # already-concrete array is unchanged in value and type + B = [1, 2, 3] + @test tighten(B) == B + @test eltype(tighten(B)) == Int end