[ANN] ArrayPadding.jl - periodic, symmetric, smooth or constants padding

ArrayPadding.jl Pads arrays of any dimension with various border options including constants, periodic, symmetric, mirror and smooth. Can control amount of padding applied to the left and right side of each dimension. Fully differentiable (compatible with Zygote.jl Flux.jl )

14 Likes

Is your approach more performant or more flexible than GitHub - JuliaArrays/PaddedViews.jl: Add virtual padding to the edges of an array?

PaddedViews.jl only supports padding of constants TMK

2 Likes

Your current approach creates a new array.
I wonder if you can add a feature to have a view of the data.

A bigger challenge, if possible, to have such view with no run time overhead.

Yeah it’s tricky doing virtual padding for non-constant border options. There’s always PaddedViews.jl for virtual padding constants. Also a key goal is being fully differentiable (with Zygote.jl Flux.jl) and GPU compatible - so virtual indexing was deemed too much trouble vs a clean eager implementation.

As a suggestion, we call Base.cat which potentially can be modified to virtually concatenate arrays instead of allocating memory - probably cleanest way to do virtual indexing

1 Like

Could you elaborate?

Also, I didn’t see the option to have inplace functionality.
Namely given a pre allocated buffer to use.

malloc in my pkg results from calling Base.cat of the original array and its sliced views. If cat were lazy, then my pkg wouldn’t alloc new array. I think LazyArrays.jl has lazy vcat hcat but not cat in general unless I’m mistaken. A lazy non-allocating version of cat might benefit other folks too

1 Like

@pxshen Thanks for creating the package, it indeed fulfills the gap from PaddedViews.jl. Have tried mirror padding size (1,1) as follows, and it worked as expected. Size (2,2) apparently failed, as I expected a 9x9 matrix output, but function returned same 5x5 matrix:

julia> a = reshape(1:25, (5,5))
5×5 reshape(::UnitRange{Int64}, 5, 5) with eltype Int64:
 1   6  11  16  21
 2   7  12  17  22
 3   8  13  18  23
 4   9  14  19  24
 5  10  15  20  25

julia> pad(a, :mirror, (1,1))
7×7 Matrix{Int64}:
  7  2   7  12  17  22  17
  6  1   6  11  16  21  16
  7  2   7  12  17  22  17
  8  3   8  13  18  23  18
  9  4   9  14  19  24  19
 10  5  10  15  20  25  20
  9  4   9  14  19  24  19

julia> pad(a, :mirror, (2,2))
5×5 Matrix{Int64}:
 1   6  11  16  21
 2   7  12  17  22
 3   8  13  18  23
 4   9  14  19  24
 5  10  15  20  25

Would appreciate your advise. Thanks.

Thanks for finding bug for mirror and symmetric - fixed and pushed - might take few hours to update on general registry. Also if there’s enough need I can make the package non-allocating / virtually indexed.

2 Likes

A virtual / non allocating mode would be great.
So the user can chose.

I wonder if it also can be done with no overhead for the elements which are not in the boundary.

1 Like

pad now takes lazy=true for non-allocating virtually indexed result. It uses LazyArrays.jl so only up to 2d arrays are supported in lazy mode. I think there’s minor indexing overhead

Also added :replicate border option

4 Likes

Lazy mode (using LazyArrays.jl underneath) doesn’t work with Zygote.jl autodiff. I’ll try fixing this

2 Likes

Actually I think the cleanest way to avoid both allocation and indexing overhead is to preallocate the bigger array as has been mentioned by Roy. For that I added pad! which pads inwards and mutates original array in place. docs updated. the effective border is set back by the padding amount when evaluating various border options. It’s still AD (automatic differentiation) compatible thru Zygote.bufferfrom internally

Sorry - for mutating pad! , Zygote.gradient is wrong - will get fixed this wk

1 Like

For pad! I decided to let user allocate Zygote.Buffer if AD is needed (see Github page for example). This is an extra allocation to track mutations. pad! doesn’t malloc otherwise

1 Like

A few design questions:
Any reason behind the choice to make PaddedArray not a subtype of AbstractArray? Also why is not not a parametric struct?
I.e. why do you define it to be

struct PaddedArray
    a
    l
    r
    function PaddedArray(a, l=left(a), r=right(a))
        new(a, l, r)
    end
end

instead of

struct PaddedArray{T,N,ArrayType} <: AbstractArray{T,N}
    a::ArrayType
    l::Vector{Int}
    r::Vector{Int}
    function PaddedArray(a::A, l=left(a), r=right(a)) where {T,N,A <: AbstractArray{T,N}}
        new{T,N,A}(a, l, r)
    end
end

or something along those lines?

1 Like

Thanks PaddedArray is actually a private type that I use to track the amount of padding in some experiments but it’s not used by other functions and shouldn’t have been exported. Your version is the right way - it’s just that parametric types mess up Zygote sometimes causing "need custom adjoints " errors

1 Like

ah ok, I see now that pad just returns a regular Julia array! That makes sense