From c6468af29886e74fccb27750a8e154376a0b6a7e Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 09:37:59 -0500 Subject: [PATCH 1/9] Fix block_center precedence bug and modernize API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix `sz[i]>>1+1` → `(sz[i]>>1)+1` in block_center (operator precedence bug; was computing sz>>2 instead of (sz>>1)+1) - Add Counter(max::AbstractVector{<:Integer}) constructor to accept any integer vector, not just Vector{Int} - Add Base.length(c::Counter) so Counter composes with collect, comprehensions, and IteratorSize-aware code - Annotate Base.iterate(c, state::Vector{Int}) for clarity - Reorder quadratic(shift, Q, denom::Matrix) → quadratic(denom, shift, Q) to put data first, following Julia convention; widen denom to AbstractMatrix - Infer output eltype in quadratic from float(eltype(Q)) instead of hardcoding Float64 - Add tests for block_center and the AbstractVector Counter constructor Co-Authored-By: Claude Sonnet 4.6 --- src/RegisterUtilities.jl | 15 ++++++++++----- test/runtests.jl | 12 ++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/RegisterUtilities.jl b/src/RegisterUtilities.jl index c670395..0e91253 100644 --- a/src/RegisterUtilities.jl +++ b/src/RegisterUtilities.jl @@ -10,13 +10,17 @@ 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) @@ -35,10 +39,11 @@ using RegisterCore, LinearAlgebra export quadratic, block_center, tighten 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,10 +51,10 @@ 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) function block_center(sz...) - return ntuple(i -> sz[i] >> 1 + 1, length(sz)) + return ntuple(i -> (sz[i] >> 1) + 1, length(sz)) end function tighten(A::AbstractArray) diff --git a/test/runtests.jl b/test/runtests.jl index dc401c6..35bfaf4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,4 +24,16 @@ 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 From 11900663d784dead2643f79818df14778765b1be Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 09:38:10 -0500 Subject: [PATCH 2/9] Bump version to 0.2.0 Breaking change: quadratic(denom, shift, Q) argument order reversed. Co-Authored-By: Claude Sonnet 4.6 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 53143e8..1246100 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "RegisterUtilities" uuid = "d4862ba2-f42c-5aeb-af4f-96a8884a16c4" authors = ["Tim Holy "] -version = "0.1.0" +version = "0.2.0" [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" From 311cc5d847429fa1ec5ac6790f904f78debe226a Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 09:40:32 -0500 Subject: [PATCH 3/9] Add Manifest-v*.toml to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 8b753b3389edd11dffff367071f3bc983651705e Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 10:29:12 -0500 Subject: [PATCH 4/9] Add Aqua.jl quality checks to test suite Co-Authored-By: Claude Sonnet 4.6 --- Project.toml | 9 +++++++-- test/runtests.jl | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 1246100..25954c5 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,22 @@ name = "RegisterUtilities" uuid = "d4862ba2-f42c-5aeb-af4f-96a8884a16c4" -authors = ["Tim Holy "] version = "0.2.0" +authors = ["Tim Holy "] [deps] LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434" [compat] +Aqua = "0.8" +LinearAlgebra = "1" +RegisterCore = "0.2" +Test = "1" julia = "^1.1" [extras] +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Aqua", "Test"] diff --git a/test/runtests.jl b/test/runtests.jl index 35bfaf4..b8ea513 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,11 @@ using Test +using Aqua using RegisterUtilities +@testset "Aqua" begin + Aqua.test_all(RegisterUtilities) +end + @testset "Counter test" begin for empty_gridsize in ((), (0,), (1, 0), (1, -1), (1, 0, 1)) for c in Counter(empty_gridsize) From bb8cb9a37fe3d23f5612bed38ccd05064a22c11b Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 10:37:47 -0500 Subject: [PATCH 5/9] Add ExplicitImports.jl: make all imports explicit Co-Authored-By: Claude Sonnet 4.6 --- Project.toml | 4 +++- src/RegisterUtilities.jl | 3 ++- test/runtests.jl | 5 +++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 25954c5..7b92bb6 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ RegisterCore = "67712758-55e7-5c3c-8e85-dda1d7758434" [compat] Aqua = "0.8" +ExplicitImports = "1" LinearAlgebra = "1" RegisterCore = "0.2" Test = "1" @@ -16,7 +17,8 @@ julia = "^1.1" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +ExplicitImports = "7d51a73a-1435-4ff3-83d9-f097790105c7" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "Test"] +test = ["Aqua", "ExplicitImports", "Test"] diff --git a/src/RegisterUtilities.jl b/src/RegisterUtilities.jl index 0e91253..28c5534 100644 --- a/src/RegisterUtilities.jl +++ b/src/RegisterUtilities.jl @@ -34,7 +34,8 @@ end # Below functions are from RegisterTestUtilities -using RegisterCore, LinearAlgebra +using LinearAlgebra: LinearAlgebra, dot +using RegisterCore: RegisterCore, MismatchArray export quadratic, block_center, tighten diff --git a/test/runtests.jl b/test/runtests.jl index b8ea513..c9f7eae 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,16 @@ using Test using Aqua +using ExplicitImports 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) From b155884009346dfcfba53cc9f86d711ffbb306a4 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 10:45:27 -0500 Subject: [PATCH 6/9] Add tests for quadratic and tighten, reaching 100% coverage Co-Authored-By: Claude Sonnet 4.6 --- test/runtests.jl | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index c9f7eae..f81eb23 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,8 @@ using Test using Aqua using ExplicitImports +using LinearAlgebra: I +using RegisterCore: RegisterCore using RegisterUtilities @testset "Aqua" begin @@ -47,3 +49,37 @@ end @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 From cd222a9f27a076b381fb6db6de453f9687eb4553 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 10:48:57 -0500 Subject: [PATCH 7/9] Add docstrings for all exported symbols Cover RegisterUtilities (module), Counter, quadratic, block_center, and tighten with full Julia-style docstrings including argument descriptions and examples. Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 11 +++++ src/RegisterUtilities.jl | 86 ++++++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 .claude/freshen-package-status diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status new file mode 100644 index 0000000..b2bf205 --- /dev/null +++ b/.claude/freshen-package-status @@ -0,0 +1,11 @@ +DONE: design review +DONE: API review +DONE: update .gitignore +DONE: format with runic +DONE: add Aqua.jl +DONE: remove deprecations +DONE: add ExplicitImports.jl +DONE: limit struct mutability +DONE: improve test coverage +DONE: add and improve docstrings +TODO: add or improve documentation diff --git a/src/RegisterUtilities.jl b/src/RegisterUtilities.jl index 28c5534..8a27ee8 100644 --- a/src/RegisterUtilities.jl +++ b/src/RegisterUtilities.jl @@ -1,11 +1,38 @@ +""" + 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 @@ -39,6 +66,26 @@ 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) T = float(eltype(Q)) A = zeros(T, m, n) @@ -54,10 +101,43 @@ end 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)) 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 From a22116ba4081f768854a1b15379908ec1eac6ffa Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 10:59:06 -0500 Subject: [PATCH 8/9] Add README with usage examples and update CI action versions Co-Authored-By: Claude Sonnet 4.6 --- .claude/freshen-package-status | 2 +- .github/workflows/CI.yml | 6 +-- README.md | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 README.md diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status index b2bf205..9e5e151 100644 --- a/.claude/freshen-package-status +++ b/.claude/freshen-package-status @@ -8,4 +8,4 @@ DONE: add ExplicitImports.jl DONE: limit struct mutability DONE: improve test coverage DONE: add and improve docstrings -TODO: add or improve documentation +DONE: add or improve documentation diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 6ac1e04..44b5f91 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -20,8 +20,8 @@ jobs: 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/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 +``` From 63a49ca24b07241583074980180614b9ea011488 Mon Sep 17 00:00:00 2001 From: Dae Woo Kim Date: Wed, 29 Apr 2026 12:48:04 -0500 Subject: [PATCH 9/9] add .git-blame-ignore-revs --- .claude/freshen-package-status | 11 ----------- .claude/settings.json | 23 +++++++++++++++++++++++ .git-blame-ignore-revs | 2 ++ .github/workflows/CI.yml | 2 +- Project.toml | 4 ++-- 5 files changed, 28 insertions(+), 14 deletions(-) delete mode 100644 .claude/freshen-package-status create mode 100644 .claude/settings.json create mode 100644 .git-blame-ignore-revs diff --git a/.claude/freshen-package-status b/.claude/freshen-package-status deleted file mode 100644 index 9e5e151..0000000 --- a/.claude/freshen-package-status +++ /dev/null @@ -1,11 +0,0 @@ -DONE: design review -DONE: API review -DONE: update .gitignore -DONE: format with runic -DONE: add Aqua.jl -DONE: remove deprecations -DONE: add ExplicitImports.jl -DONE: limit struct mutability -DONE: improve test coverage -DONE: add and improve docstrings -DONE: add or improve documentation 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 44b5f91..7f73548 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: version: - - '1.1' + - 'min' - '1' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index 7b92bb6..bab9a46 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "RegisterUtilities" uuid = "d4862ba2-f42c-5aeb-af4f-96a8884a16c4" -version = "0.2.0" +version = "1.0.0" authors = ["Tim Holy "] [deps] @@ -13,7 +13,7 @@ ExplicitImports = "1" LinearAlgebra = "1" RegisterCore = "0.2" Test = "1" -julia = "^1.1" +julia = "1.10" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"