diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..1c97275d --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,39 @@ +name: Documentation + +on: + pull_request: + push: + branches: + - 'master' + - 'release-' + tags: '*' + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1] + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + - name: Cache artifacts + uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia --project=docs/ docs/make.jl diff --git a/.gitignore b/.gitignore index b067edde..d018836e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /Manifest.toml +/docs/Manifest.toml diff --git a/.travis.yml b/.travis.yml index 935c986b..43e186e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,14 +15,3 @@ notifications: after_success: # push coverage results to Codecov - julia -e 'using Pkg, OffsetArrays; cd(joinpath(dirname(pathof(OffsetArrays)), "..")); Pkg.add("Coverage"); using Coverage; Codecov.submit(Codecov.process_folder())' - -jobs: - include: - - stage: "Documentation" - julia: 1 - os: linux - script: - - julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); - Pkg.instantiate()' - - julia --project=docs/ docs/make.jl - after_success: skip diff --git a/README.md b/README.md index aed64840..a5c8f184 100644 --- a/README.md +++ b/README.md @@ -23,30 +23,24 @@ fall within these axis ranges. Example: ```julia using OffsetArrays -A = reshape(1:15, 3, 5) -println("here is A:") -display(A) -OA = OffsetArray(A, -1:1, 0:4) # OA will have axes (-1:1, 0:4) -println("here is OA:") -display(OA) -@show OA[-1,0] OA[1,4] -``` - -which prints out - -``` -here is A: -3×5 reshape(::UnitRange{Int64}, 3, 5) with eltype Int64: - 1 4 7 10 13 - 2 5 8 11 14 - 3 6 9 12 15 -here is OA: -OffsetArray(reshape(::UnitRange{Int64}, 3, 5), -1:1, 0:4) with eltype Int64 with indices -1:1×0:4: - 1 4 7 10 13 - 2 5 8 11 14 - 3 6 9 12 15 -OA[-1, 0] = 1 -OA[1, 4] = 15 +julia> A = Float64.(reshape(1:15, 3, 5)) +3×5 Matrix{Float64}: + 1.0 4.0 7.0 10.0 13.0 + 2.0 5.0 8.0 11.0 14.0 + 3.0 6.0 9.0 12.0 15.0 + +julia> OA = OffsetArray(A, -1:1, 0:4) # OA will have axes (-1:1, 0:4) +3×5 OffsetArray(::Matrix{Float64}, -1:1, 0:4) with eltype Float64 with indices -1:1×0:4: + 1.0 4.0 7.0 10.0 13.0 + 2.0 5.0 8.0 11.0 14.0 + 3.0 6.0 9.0 12.0 15.0 + +julia> OA = OffsetArray(A, CartesianIndex(-1, 0):CartesianIndex(1, 4)) +3×5 OffsetArray(::Matrix{Float64}, -1:1, 0:4) with eltype Float64 with indices -1:1×0:4: +[...] + +julia> OA[-1,0], OA[1,4] +(1.0, 15.0) ``` [pkgeval-img]: https://juliaci.github.io/NanosoldierReports/pkgeval_badges/O/OffsetArrays.svg diff --git a/docs/Project.toml b/docs/Project.toml index 1b9ab1f8..b28c0ae8 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,5 +1,7 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" [compat] -Documenter = "0.24" +Documenter = "0.25" diff --git a/docs/make.jl b/docs/make.jl index b7ca46be..7a769a95 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,4 +1,4 @@ -using Documenter +using Documenter, JSON using OffsetArrays makedocs( @@ -9,6 +9,17 @@ makedocs( doctestfilters = [r"at \./.*", r"at /home.*", r"top-level scope.*", r"\[\d*\]\s*$"], # for backtraces ) +# a workdaround to github action that only push preview when PR has "push_preview" labels +# issue: https://github.com/JuliaDocs/Documenter.jl/issues/1225 +function should_push_preview(event_path = get(ENV, "GITHUB_EVENT_PATH", nothing)) + event_path === nothing && return false + event = JSON.parsefile(event_path) + haskey(event, "pull_request") || return false + labels = [x["name"] for x in event["pull_request"]["labels"]] + return "push_preview" in labels + end + deploydocs( - repo = "github.com:JuliaArrays/OffsetArrays.jl.git" + repo = "github.com:JuliaArrays/OffsetArrays.jl.git", + push_preview = should_push_preview() ) diff --git a/docs/src/index.md b/docs/src/index.md index 05558ad9..403748b6 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -16,66 +16,54 @@ OA = OffsetArray(A, axis1, axis2, ...) ``` where you want `OA` to have axes `(axis1, axis2, ...)` and be indexed by values that -fall within these axis ranges. Example: +fall within these axis ranges. -```julia +```@repl index using OffsetArrays + A = Float64.(reshape(1:15, 3, 5)) -println("Here is A:") -display(A) -OA = OffsetArray(A, -1:1, 0:4) # OA will have axes (-1:1, 0:4) -println("Here is OA:") -display(OA) -@show OA[-1,0] OA[1,4] + +OA = OffsetArray(A, -1:1, 0:4) # OA will have axes (-1:1, 0:4) + +OA = OffsetArray(A, CartesianIndex(-1, 0):CartesianIndex(1, 4)) + +OA[-1,0], OA[1,4] ``` -gives the output +You could also pass integers as offsets, where `0` means no offsets are applied: -```julia -here is A: -3×5 Array{Float64,2}: - 1.0 4.0 7.0 10.0 13.0 - 2.0 5.0 8.0 11.0 14.0 - 3.0 6.0 9.0 12.0 15.0 -here is OA: -3×5 OffsetArray(::Array{Float64,2}, -1:1, 0:4) with eltype Float64 with indices -1:1×0:4: - 1.0 4.0 7.0 10.0 13.0 - 2.0 5.0 8.0 11.0 14.0 - 3.0 6.0 9.0 12.0 15.0 -OA[-1, 0] = 1.0 -OA[1, 4] = 15.0 +```@repl index +OA = OffsetArray(A, -2, -1) ``` -OffsetArrays works for arbitrary dimensionality: +When you create a new `OffsetArray` on the top of another `OffsetArray`, the offsets are +accumulated: -```julia -julia> using OffsetArrays +```@repl index +OOA = OffsetArray(OA, 2, 1) +``` -julia> y = OffsetArray{Float64}(undef, -1:1, -7:7, -128:512, -5:5, -1:1, -3:3, -2:2, -1:1); +For the special cases that you want to compensate the offset back to the ordinary 1-based array, you +can use [`OffsetArrays.no_offset_view(A)`](@ref). Furthermore, you could use +`Base.require_one_based_indexing` if you want to ensure the array does not have offsets. -julia> summary(y) -"OffsetArrays.OffsetArray{Float64,8,Array{Float64,8}} with indices -1:1×-7:7×-128:512×-5:5×-1:1×-3:3×-2:2×-1:1" +```@repl index +OffsetArrays.no_offset_view(OA) -julia> y[-1,-7,-128,-5,-1,-3,-2,-1] = 14 -14 +Base.require_one_based_indexing(ans) -julia> y[-1,-7,-128,-5,-1,-3,-2,-1] += 5 -19.0 +Base.require_one_based_indexing(OA) ``` -You can use `OffsetArrays.no_offset_view(A)` if you want to return a view of the data in `A` but where indexing starts at 1. - ## Example: Relativistic Notation Suppose we have a position vector `r = [:x, :y, :z]` which is naturally one-based, ie. `r[1] == :x`, `r[2] == :y`, `r[3] == :z` and we also want to construct a relativistic position vector which includes time as the 0th component. This can be done with OffsetArrays like -```jldoctest -julia> using OffsetArrays - +```jldoctest; setup = :(using OffsetArrays) julia> r = [:x, :y, :z]; julia> x = OffsetVector([:t, r...], 0:3) -4-element OffsetArray(::Array{Symbol,1}, 0:3) with eltype Symbol with indices 0:3: +4-element OffsetArray(::Vector{Symbol}, 0:3) with eltype Symbol with indices 0:3: :t :x :y @@ -85,7 +73,7 @@ julia> x[0] :t julia> x[1:3] -3-element Array{Symbol,1}: +3-element Vector{Symbol}: :x :y :z @@ -94,17 +82,17 @@ julia> x[1:3] ## Example: Polynomials Suppose one wants to represent the Laurent polynomial -``` + +```math 6/x + 5 - 2*x + 3*x^2 + x^3 ``` -in julia. The coefficients of this polynomial are a naturally `-1` based list, since the `n`th element of the list -(counting from `-1`) `6, 5, -2, 3, 1` is the coefficient corresponding to the `n`th power of `x`. This Laurent polynomial can be evaluated at say `x = 2` as follows. -```jldoctest -julia> using OffsetArrays +The coefficients of this polynomial are a naturally `-1` based list, since the `n`th element of the list +(counting from `-1`) `6, 5, -2, 3, 1` is the coefficient corresponding to the `n`th power of `x`. This Laurent polynomial can be evaluated at say `x = 2` as follows. +```jldoctest; setup = :(using OffsetArrays) julia> coeffs = OffsetVector([6, 5, -2, 3, 1], -1:3) -5-element OffsetArray(::Array{Int64,1}, -1:3) with eltype Int64 with indices -1:3: +5-element OffsetArray(::Vector{Int64}, -1:3) with eltype Int64 with indices -1:3: 6 5 -2 diff --git a/docs/src/internals.md b/docs/src/internals.md index baebd4fa..3d6c8117 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -7,7 +7,7 @@ The majority of cases can be handled with these tips: - replace `1:length(A)` with `eachindex(A)`, or if you need an integer index with `LinearIndices(A)` - replace explicit allocations like `Array{Int}(undef, size(B))` with `similar(Array{Int}, axes(B))` -More information can be found in [Julia's developer documentation](https://docs.julialang.org/en/v1.0/devdocs/offset-arrays/). +More information can be found in [Julia's developer documentation](https://docs.julialang.org/en/v1/devdocs/offset-arrays/). The most subtle issues tend to arise around the axes, and further detail specific to OffsetArrays.jl follows below. @@ -19,12 +19,12 @@ with an index offset: ```jldoctest oa; setup=:(using OffsetArrays) julia> oa = OffsetArray([1 2; 3 4], 0:1, 5:6) -2×2 OffsetArray(::Array{Int64,2}, 0:1, 5:6) with eltype Int64 with indices 0:1×5:6: +2×2 OffsetArray(::Matrix{Int64}, 0:1, 5:6) with eltype Int64 with indices 0:1×5:6: 1 2 3 4 julia> parent(oa) -2×2 Array{Int64,2}: +2×2 Matrix{Int64}: 1 2 3 4 @@ -39,61 +39,48 @@ indexes and then returns the value in the parent. ## The axes of OffsetArrays -```jldoctest oa -julia> axes(oa) -(0:1, 5:6) -``` - -This looks straightforward, but if you dive deeper you'll notice some complexities: +The internal of offset computing is achieved by [`IdOffsetRange`](@ref OffsetArrays.IdOffsetRange) +type: ```jldoctest oa julia> ax = axes(oa, 2) -5:6 - -julia> typeof(ax) -OffsetArrays.IdOffsetRange{Int64,Base.OneTo{Int64}} +OffsetArrays.IdOffsetRange(5:6) +``` -julia> ax[1] -ERROR: BoundsError: attempt to access 2-element Base.OneTo{Int64} at index [-3] -Stacktrace: - [1] throw_boundserror(::Base.OneTo{Int64}, ::Int64) at ./abstractarray.jl:538 - [2] getindex at ./range.jl:625 [inlined] - [3] getindex(::OffsetArrays.IdOffsetRange{Int64,Base.OneTo{Int64}}, ::Int64) at /home/tim/.julia/dev/OffsetArrays/src/axes.jl:139 - [4] top-level scope at none:0 +This has a similar design to `Base.IdentityUnitRange` that `ax[x] == x` always holds. +```jldoctest oa julia> ax[5] 5 +julia> ax[1] +ERROR: BoundsError: attempt to access 2-element Base.OneTo{Int64} at index [-3] +[...] ``` -The axes are of type [`OffsetArrays.IdOffsetRange`](@ref). -`IdOffsetRange`s are essentially OffsetArrays specialized for ranges, with the additional -property that they tend to be their own axes: +This property makes sure that they tend to be their own axes: ```jldoctest oa -julia> ax -5:6 - julia> axes(ax) -(5:6,) +(OffsetArrays.IdOffsetRange(5:6),) julia> axes(ax[ax]) -(5:6,) +(OffsetArrays.IdOffsetRange(5:6),) ``` This example of indexing is [idempotent](https://en.wikipedia.org/wiki/Idempotence). This is a useful characteristic for ensuring the "fundamental axiom" of generalized indexing, -that `a[rng][i] == a[rng[i]]`: +that `a[ax][i] == a[ax[i]]`: ```jldoctest; setup=:(using OffsetArrays) julia> oa2 = OffsetArray([5, 10, 15, 20], 0:3) -4-element OffsetArray(::Array{Int64,1}, 0:3) with eltype Int64 with indices 0:3: +4-element OffsetArray(::Vector{Int64}, 0:3) with eltype Int64 with indices 0:3: 5 10 15 20 julia> ax2 = axes(oa2, 1) -0:3 +OffsetArrays.IdOffsetRange(0:3) julia> oa2[2] 15 @@ -116,3 +103,43 @@ julia> oa2[ax2[2]] - conversion will succeed only if it can preserve both the values and the axes (Examples: `convert(RangeType, rng)`, `oftype(rng1, rng2)`) While these behave equivalently now (conversion currently performs coercion), developers are encouraged to "future-proof" their code by choosing the behavior appropriate for each usage. + +## Caveats + +Because `IdOffsetRange` behaves quite differently to the normal `UnitRange` type, there are some +cases that you should be aware of, especially when you are working with multi-dimensional arrays. + +One such cases is `getindex`: + +```jldoctest getindex; setup = :(using OffsetArrays) +julia> Ao = zeros(-3:3, -3:3); Ao[:] .= 1:49; + +julia> Ao[-3:0, :] |> axes # the first dimension does not preserve offsets +(OffsetArrays.IdOffsetRange(1:4), OffsetArrays.IdOffsetRange(-3:3)) + +julia> Ao[-3:0, -3:3] |> axes # neither dimensions preserve offsets +(Base.OneTo(4), Base.OneTo(7)) + +julia> Ao[axes(Ao)...] |> axes # offsets are preserved +(OffsetArrays.IdOffsetRange(-3:3), OffsetArrays.IdOffsetRange(-3:3)) + +julia> Ao[:] |> axes # This is linear indexing +(Base.OneTo(49),) +``` + +Note that if you pass a `UnitRange`, the offsets in corresponding dimension will not be preserved. +This might look weird at first, but since it follows the `a[ax][i] == a[ax[i]]` rule, it is not a +bug. + +```jldoctest getindex +julia> I = -3:0; # UnitRange always starts at index 1 + +julia> Ao[I, 0][1] == Ao[I[1], 0] +true + +julia> ax = axes(Ao, 1) # ax starts at index -3 +OffsetArrays.IdOffsetRange(-3:3) + +julia> Ao[ax, 0][1] == Ao[ax[1], 0] +true +``` diff --git a/src/OffsetArrays.jl b/src/OffsetArrays.jl index b64b175c..9d050094 100644 --- a/src/OffsetArrays.jl +++ b/src/OffsetArrays.jl @@ -69,8 +69,12 @@ used the given `indices`, which are checked for compatible size. # Example +There are two types of `indices`: integers and ranges-like types. + +Integers are recognized as offsets, where `0` means no offsets are applied: + ```jldoctest; setup=:(using OffsetArrays) -julia> A = OffsetArray(reshape(1:6, 2, 3), 0:1, -1:1) +julia> A = OffsetArray(reshape(1:6, 2, 3), -1, -2) 2×3 OffsetArray(reshape(::UnitRange{Int64}, 2, 3), 0:1, -1:1) with eltype Int64 with indices 0:1×-1:1: 1 3 5 2 4 6 @@ -78,6 +82,34 @@ julia> A = OffsetArray(reshape(1:6, 2, 3), 0:1, -1:1) julia> A[0, 1] 5 ``` + +Examples of range-like types are: `Colon()`(aka `:`), `UnitRange`(e.g, `-1:2`), and +`CartesianIndices`. + +```jldoctest; setup=:(using OffsetArrays) +julia> OffsetArray(reshape(1:6, 2, 3), 0:1, -1:1) +2×3 OffsetArray(reshape(::UnitRange{Int64}, 2, 3), 0:1, -1:1) with eltype Int64 with indices 0:1×-1:1: + 1 3 5 + 2 4 6 + +julia> OffsetArray(reshape(1:6, 2, 3), :, -1:1) # : as a placeholder means no offset is applied at this dimension +2×3 OffsetArray(reshape(::UnitRange{Int64}, 2, 3), 1:2, -1:1) with eltype Int64 with indices 1:2×-1:1: + 1 3 5 + 2 4 6 + +julia> OffsetArray(reshape(1:6, 2, 3), CartesianIndex(0, -1):CartesianIndex(1, 1)) +2×3 OffsetArray(reshape(::UnitRange{Int64}, 2, 3), 0:1, -1:1) with eltype Int64 with indices 0:1×-1:1: + 1 3 5 + 2 4 6 +``` + +Integers and range-like types can't be used interchangebly: + +```julia +julia> OffsetArray(reshape(1:6, 2, 3), 0, -1:1) +ERROR: [...] +``` + """ function OffsetArray(A::AbstractArray{T,N}, inds::NTuple{N,AbstractUnitRange}) where {T,N} axparent = axes(A) @@ -347,7 +379,7 @@ specific to remove a level of indirection when applicable. julia> A = [1 3 5; 2 4 6]; julia> O = OffsetArray(A, 0:1, -1:1) -2×3 OffsetArray(::Array{Int64,2}, 0:1, -1:1) with eltype Int64 with indices 0:1×-1:1: +2×3 OffsetArray(::Matrix{Int64}, 0:1, -1:1) with eltype Int64 with indices 0:1×-1:1: 1 3 5 2 4 6 @@ -355,7 +387,7 @@ julia> OffsetArrays.no_offset_view(O)[1,1] = -9 -9 julia> A -2×3 Array{Int64,2}: +2×3 Matrix{Int64}: -9 3 5 2 4 6 ``` diff --git a/src/axes.jl b/src/axes.jl index 19e5dd43..105fdb85 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -11,10 +11,10 @@ i.e., it's the "identity," which is the origin of the "Id" in `IdOffsetRange`. The most common case is shifting a range that starts at 1 (either `1:n` or `Base.OneTo(n)`): ```jldoctest; setup=:(import OffsetArrays) julia> ro = OffsetArrays.IdOffsetRange(1:3, -2) --1:1 +OffsetArrays.IdOffsetRange(-1:1) julia> axes(ro, 1) --1:1 +OffsetArrays.IdOffsetRange(-1:1) julia> ro[-1] -1 @@ -26,10 +26,10 @@ ERROR: BoundsError: attempt to access 3-element UnitRange{Int64} at index [5] If the range doesn't start at 1, the values may be different from the indices: ```jldoctest; setup=:(import OffsetArrays) julia> ro = OffsetArrays.IdOffsetRange(11:13, -2) -9:11 +OffsetArrays.IdOffsetRange(9:11) julia> axes(ro, 1) # 11:13 is indexed by 1:3, and the offset is also applied to the axes --1:1 +OffsetArrays.IdOffsetRange(-1:1) julia> ro[-1] 9