Документация Engee

Для разработчиков

Написание кода, поддерживающего OffsetArrays, обычно не вызывает трудностей. В большинстве случаев можно использовать следующие рекомендации:

  • заменяйте многие использования size на axes;

  • заменяйте 1:length(A) на eachindex(A), или, если вам нужен целочисленный индекс, на LinearIndices(A);

  • заменяйте явные выделения типа Array{Int}(undef, size(B)) на similar(Array{Int}, axes(B)).

Более подробную информацию можно найти в документации разработчика Julia. Самые острые проблемы, как правило, связаны с осями. Более подробная информация, касающаяся OffsetArrays.jl, приведена ниже.

Основные принципы

Как работает OffsetArrays? Фундаментальный принцип очень прост: OffsetArray является просто оболочкой «родительского» массива с указанием смещения индекса:

julia> oa = OffsetArray([1 2; 3 4], 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 Matrix{Int64}:
 1  2
 3  4

julia> oa.offsets
(-1, 4)

Итак, parent(oa) — это исходный массив, из которого мы его построили, а oa.offsets — кортеж, каждый элемент которого кодирует индексное смещение, которое нужно применить по соответствующей оси. Когда вы индексируете oa[i,j], он «переводит» индексы i,j обратно в индексы родительского массива, а затем возвращает значение в родительском массиве.

Оси OffsetArrays

Внутреннее вычисление смещения осуществляется с помощью типа IdOffsetRange:

julia> ax = axes(oa, 2)
OffsetArrays.IdOffsetRange(values=5:6, indices=5:6)

По аналогии с Base.IdentityUnitRange, ax[x] == x всегда выполняется.

julia> ax[5]
5
julia> ax[1]
ERROR: BoundsError: attempt to access 2-element OffsetArrays.IdOffsetRange{Int64, Base.OneTo{Int64}} with indices 5:6 at index [1]
[...]

Согласно этому свойству они становятся собственными осями:

julia> axes(ax)
(OffsetArrays.IdOffsetRange(values=5:6, indices=5:6),)

julia> axes(ax[ax])
(OffsetArrays.IdOffsetRange(values=5:6, indices=5:6),)

Этот пример индексирования является идемпотентным. Это полезная характеристика для обеспечения «фундаментальной аксиомы» обобщенного индексирования, что a[ax][i] == a[ax[i]]:

julia> oa2 = OffsetArray([5, 10, 15, 20], 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)
OffsetArrays.IdOffsetRange(values=0:3, indices=0:3)

julia> oa2[2]
15

julia> oa2[ax2][2]
15

julia> oa2[ax2[2]]
15

IdOffsetRange применяют смещение как к значениям, так и к индексам диапазона, а в остальном сохраняют родительский диапазон.

Существуют обстоятельства, при которых построение определенного типа IdOffsetRange невозможно без изменения осей диапазона (см. описание OffsetArrays.IdOffsetRange).

В будущем этот пакет будет различать построение и преобразование:

- построение (или *принуждение*) всегда будет успешным, даже если для этого придется изменить оси результата (примеры: `RangeType(rng)`, `typeof(rng1)(rng2)`)
  • преобразование будет успешным, только если оно может сохранить и значения, и оси (примеры: convert(RangeType, rng), oftype(rng1, rng2))

    Хотя сейчас их действие эквивалентно (преобразование в настоящее время осуществляет принуждение), разработчикам рекомендуется «обезопасить» свой код на будущее, выбрав поведение, подходящее для каждого случая использования.

Заключение в оболочку других типов массивов смещений

OffsetArray может заключить в оболочку любой подтип AbstractArray, включая те, которые не используют индексирование на основе 1. Однако такие массивы должны удовлетворять фундаментальной аксиоме идемпотентного индексирования, чтобы все компоненты работали правильно. Другими словами, ось массива смещений должна иметь те же значения, что и ее собственная ось. Это свойство встроено в OffsetArray, если родительский объект использует индексирование на основе 1, но пользователь должен убедиться в его корректности в случае, если необходимо заключить в оболочку тип, использующий индексы со смещением.

Продемонстрируем это на примере создания пользовательского типа диапазона, основанного на 0, который мы инкапсулируем в OffsetArray:

julia> struct ZeroBasedRange{T,A<:AbstractRange{T}} <: AbstractRange{T}
           a :: A
           function ZeroBasedRange(a::AbstractRange{T}) where {T}
               @assert !Base.has_offset_axes(a)
               new{T, typeof(a)}(a)
           end
       end;

julia> Base.parent(A::ZeroBasedRange) = A.a;

julia> Base.first(A::ZeroBasedRange) = first(A.a);

julia> Base.length(A::ZeroBasedRange) = length(A.a);

julia> Base.last(A::ZeroBasedRange) = last(A.a);

julia> Base.size(A::ZeroBasedRange) = size(A.a);

julia> Base.axes(A::ZeroBasedRange) = map(x -> 0:x-1, size(A.a));

julia> Base.getindex(A::ZeroBasedRange, i::Int) = A.a[i + 1];

julia> Base.step(A::ZeroBasedRange) = step(A.a);

julia> function Base.show(io::IO, A::ZeroBasedRange)
           show(io, A.a)
           print(io, " with indices $(axes(A,1))")
       end;

Это определение ZeroBasedRange, похоже, имеет правильные индексы, например:

julia> z = ZeroBasedRange(1:4)
1:4 with indices 0:3

julia> z[0]
1

julia> z[3]
4

Однако здесь не используется идемпотентное индексирование, так как ось ZeroBasedRange не является собственной осью.

julia> axes(z, 1)
0:3

julia> axes(axes(z, 1), 1)
Base.OneTo(4)

Это приведет к осложнениям в некоторых функциях, например LinearIndices, которые склонны неявно предполагать идемпотентное индексирование. В этом случае LinearIndices для z не совпадет с осью.

julia> LinearIndices(z)
4-element LinearIndices{1, Tuple{UnitRange{Int64}}}:
 1
 2
 3
 4

Инкапсулирование такого типа в OffsetArray может привести к неожиданным ошибкам.

julia> zo = OffsetArray(z, 1);

julia> axes(zo, 1)
OffsetArrays.IdOffsetRange(values=1:4, indices=2:5)

julia> Array(zo)
ERROR: BoundsError: attempt to access 4-element UnitRange{Int64} at index [5]
[...]

Преобразование Array ошибочно, несмотря на то, что zo имеет индексы, основанные на 1. Функция axes(zo, 1) указывает на базовую проблему — значения и индексы оси различны. Можно проверить, что ось zo не является собственной осью:

julia> axes(zo, 1)
OffsetArrays.IdOffsetRange(values=1:4, indices=2:5)

julia> axes(axes(zo, 1), 1)
OffsetArrays.IdOffsetRange(values=2:5, indices=2:5)

В этом случае ошибку можно исправить, определив, что функция axes для ZeroBasedRange является идемпотентной, например, используя оболочку OffsetArrays.IdentityUnitRange:

julia> Base.axes(A::ZeroBasedRange) = map(x -> OffsetArrays.IdentityUnitRange(0:x-1), size(A.a))

julia> axes(zo, 1)
OffsetArrays.IdOffsetRange(values=1:4, indices=1:4)

При таком новом определении значения и индексы оси идентичны, что делает индексирование идемпотентным. Теперь преобразование в Array работает, как и ожидалось:

julia> Array(zo)
4-element Vector{Int64}:
 1
 2
 3
 4

Предупреждения

Поскольку IdOffsetRange ведет себя совершенно иначе, чем обычный тип UnitRange, есть несколько случаев, о которых вам следует знать, особенно при работе с многомерными массивами.

Одним из них является getindex:

julia> Ao = zeros(-3:3, -3:3); Ao[:] .= 1:49;

julia> Ao[-3:0, :] |> axes # первое измерение не сохраняет смещения
(OffsetArrays.IdOffsetRange(values=1:4, indices=1:4), OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3))

julia> Ao[-3:0, -3:3] |> axes # ни одно из измерений не сохраняет смещения
(Base.OneTo(4), Base.OneTo(7))

julia> Ao[axes(Ao)...] |> axes # смещения сохраняются
(OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3), OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3))

julia> Ao[:] |> axes # Это линейное индексирование
(Base.OneTo(49),)

Обратите внимание, что при передаче UnitRange смещения в соответствующем измерении не сохранятся. Сначала это может показаться странным, но, поскольку здесь соблюдается правило a[ax][i] == a[ax[i]], ошибки нет.

julia> I = -3:0; # UnitRange всегда начинается с индекса 1

julia> Ao[I, 0][1] == Ao[I[1], 0]
true

julia> ax = axes(Ao, 1) # ax начинается с индекса -3
OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3)

julia> Ao[ax, 0][1] == Ao[ax[1], 0]
true

Использование пользовательских типов осей

Хотя в качестве индексов для построения OffsetArray могут использоваться самые разные AbstractUnitRange, предоставляемые Base, иногда бывает удобно определить собственные типы. Конструктор OffsetArray принимает любой тип, который может быть преобразован в AbstractUnitRange. Этот процесс выполняется в два этапа. Предположим, что вызываемым конструктором является OffsetArray(A, indstup), где indstup — это кортеж (Tuple) индексов.

  1. На первом этапе конструктор вызывает to_indices(A, axes(A), indstup), чтобы снизить уровень indstup до кортежа (Tuple) из AbstractUnitRange. На этом шаге, помимо всего прочего, Colon преобразуются в диапазоны осей. Пользовательские типы могут расширять Base.to_indices(A, axes(A), indstup) с желаемым преобразованием indstup в Tuple{Vararg{AbstractUnitRange{Int}}}, если это осуществимо.

  2. На втором шаге результат, полученный на предыдущем шаге, снова обрабатывается для преобразования в кортеж (Tuple) из AbstractUnitRange, чтобы выполнить те моменты, которые не удалось реализовать на первом шаге. На этом этапе может быть задан дополнительный параметр настройки: тип может быть преобразован либо в один AbstractUnitRange{Int}, либо в кортеж (Tuple) из них. Тип может указывать, какой из этих двух вариантов поведения желателен, расширяя OffsetArrays.AxisConversionStyle. Примером типа, который обрабатывается на этом этапе, является CartesianIndices, который преобразуется в кортеж (Tuple) из AbstractUnitRange.

Например, вот несколько пользовательских типов, которые упрощают индексирование на основе нуля:

julia> struct ZeroBasedIndexing end

julia> Base.to_indices(A, inds, ::Tuple{ZeroBasedIndexing}) = map(x -> 0:length(x)-1, inds)

julia> a = zeros(3, 3);

julia> oa = OffsetArray(a, ZeroBasedIndexing());

julia> axes(oa)
(OffsetArrays.IdOffsetRange(values=0:2, indices=0:2), OffsetArrays.IdOffsetRange(values=0:2, indices=0:2))

В этом примере нам пришлось определить действие to_indices, так как у типа ZeroBasedIndexing не было привычной иерархии. Все становится еще проще, если мы выделяем подтип AbstractUnitRange. В этом случае нам нужно определить first и length для пользовательского диапазона, чтобы использовать его в качестве оси:

julia> struct ZeroTo <: AbstractUnitRange{Int}
       n :: Int
       ZeroTo(n) = new(n < 0 ? -1 : n)
       end

julia> Base.first(::ZeroTo) = 0

julia> Base.length(r::ZeroTo) = r.n + 1

julia> oa = OffsetArray(zeros(2,2), ZeroTo(1), ZeroTo(1));

julia> axes(oa)
(OffsetArrays.IdOffsetRange(values=0:1, indices=0:1), OffsetArrays.IdOffsetRange(values=0:1, indices=0:1))

Обратите внимание, что индексирование на основе нуля также можно получить с помощью предопределенного типа OffsetArrays.Origin.