Документация разработчика
Построение интерполянта
В результате построения создается объект интерполяции. В некоторых случаях это сравнительно простой процесс: например, при использовании только интерполяционных схем NoInterp
, Constant
или Linear
построение фактически соответствует регистрации массива значений и «параметров» (интерполяционной схемы), заданных в момент построения. Такая простота объясняется тем, что интерполированные значения можно вычислить непосредственно на основе сеточных значений, предоставленных при построении: воссоздает при .
Для Quadratic
и более высоких порядков эффективное вычисление требует предварительной фильтрации массива значений. Это фактически соответствует «обращению» вычисления, которое будет выполняться в ходе интерполяции, с целью приблизительно воссоздать исходные значения в точках на сетке. В общем случае это соответствует решению близкой к тридиагональной системы уравнений с обращением базовой интерполяционной схемы так, что для некоторых функций и (дополнительные сведения см. в разделе Quadratic
). Здесь — это предварительно отфильтрованная версия a
такая, что подстановка (для которой могут быть получены отличные от 0 и 1 значения при вызове и соответственно) приблизительно восстанавливает .
Точный вид системы уравнений, которую нужно решить, зависит от порядка интерполяции и граничных условий. Граничные условия часто вводят отклонения от идеальной тридиагональности; с этими «дополнительными условиями» эффективно справляется пакет WoodburyMatrices
. Данные вычисления реализуются независимо для каждой оси с помощью пакета AxisAlgorithms
.
В каталоге doc
есть старые файлы, в которых излагаются некоторые математические принципы. Вот полезный ресурс:
Филипп Тевеназ (Philippe Thévenaz), Тьерри Блю (Thierry Blu) и Майкл Унсер (Michael Unser). Interpolation revisited. IEEE Transactions on Medical Imaging 19.7 (2000): стр. 739—758.
Обратите внимание: одним из следствий применения этих принципов является то, что чтобы обеспечить поддержку квадратичной или кубической интерполяции для |
Использование интерполянта
Под использованием понимается вычисление выражений вида itp(x, y...)
, Interpolations.gradient(itp, x, y...)
и т. д. Само по себе использование состоит из двух шагов: вычисления весов и выполнения интерполяции.
Вычисление весов
Веса зависят от интерполяционной схемы и позиции x, y...
, но не от коэффициентов интерполируемого массива. Поэтому возможно множество ситуаций, когда может потребоваться повторно использовать ранее вычисленные веса, и пакет Interpolations.jl
спроектирован с учетом этого факта.
Ключевой концепцией на этом шаге является тип Interpolations.WeightedIndex
, но приводить здесь всю информацию из его подробного docstring не имеет смысла. Достаточно будет добавить, что WeightedIndex
— это по сути абстрактный тип с двумя конкретными подтипами:
-
WeightedAdjIndex
для индексов, по которым будет происходить обращение к смежным точкам массива коэффициентов (точкам, индексы которых увеличиваются на 1 по соответствующему измерению). Они применяются, если в ходе предварительной фильтрации происходит заполнение, которое можно использовать даже на границах, либо для таких интерполяционных схем, какLinear
, которые не требуют заполнения. -
В
WeightedArbIndex
хранятся как вес, так и индекс, связанный с каждой точкой сетки, к которой происходит обращение, что позволяет кодировать схемы доступа к сетке. Такие индексы применяются в особых обстоятельствах. Ярчайшим примером являются периодические граничные условия, в случае с которыми обращение к массиву коэффициентов может происходить в несмежных позициях.
Способ вычисления WeightedIndex
зависит от интерполяционной схемы (например, Linear
или Quadratic
), а также от того, вычисляются ли значения, градиенты (gradient
) или гессианы (hessian
). Порядок обработки производных более подробно описывается ниже.
Интерполяция
Общие массивы AbstractArray
можно индексировать с помощью индексов WeightedIndex
, и в результате получается интерполированное значение. Иными словами, конечным результатом является просто itp.coefs[wis...]
, где wis
— кортеж индексов WeightedIndex
. Чтобы такая перегрузка действовала, мы заключаем coefs
в InterpGetindex
, то есть InterpGetindex(itp.coefs)[wis...]
Производные по определенной оси можно вычислять, просто подставляя вместо компонента wis
тот, который предназначен для вычисления производных, а не значений.
В качестве демонстрации давайте посмотрим, как происходит следующее вычисление.
julia> A = reshape(1:27, 3, 3, 3)
3×3×3 reshape(::UnitRange{Int64}, 3, 3, 3) with eltype Int64:
[:, :, 1] =
1 4 7
2 5 8
3 6 9
[:, :, 2] =
10 13 16
11 14 17
12 15 18
[:, :, 3] =
19 22 25
20 23 26
21 24 27
julia> itp = interpolate(A, BSpline(Linear()));
julia> x = (1.2, 1.4, 1.7)
(1.2, 1.4, 1.7)
julia> itp(x...)
8.7
Используя средства отладки среды IDE, такой как Juno или VSCode, или применяя Debugger.jl из REPL, можно легко пошагово выполнить код с заходом в приведенный выше вызов, чтобы отслеживать его работу параллельно с описанием ниже. |
Если оставить в стороне такие детали, как проверка границ, главный вызов — это вызов метода Interpolations.weightedindexes
:
julia> wis = Interpolations.weightedindexes((Interpolations.value_weights,), Interpolations.itpinfo(itp)..., x)
(Interpolations.WeightedAdjIndex{2, Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2, Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2, Float64}(1, (0.30000000000000004, 0.7)))
julia> wis[1]
Interpolations.WeightedAdjIndex{2, Float64}(1, (0.8, 0.19999999999999996))
julia> wis[2]
Interpolations.WeightedAdjIndex{2, Float64}(1, (0.6000000000000001, 0.3999999999999999))
julia> wis[3]
Interpolations.WeightedAdjIndex{2, Float64}(1, (0.30000000000000004, 0.7))
julia> Interpolations.InterpGetindex(A)[wis...]
8.7
Можно заметить, что каждый компонент wis
соответствует определенной позиции: 1,2, 1,4 и 1,7 соответственно. Мы можем обратиться к A
по индексу wis
, и в результате будет возвращено значение itp(x...)
, которое в данном случае будет таким:
0.8 * A[1, wis[2], wis[3]] + 0.2 * A[2, wis[2], wis[3]] = 0.6 * (0.8 * A[1, 1, wis[3]] + 0.2 * A[2, 1, wis[3]]) + 0.4 * (0.8 * A[1, 2, wis[3]] + 0.2 * A[2, 2, wis[3]]) = 0.3 * (0.6 * (0.8 * A[1, 1, 1] + 0.2 * A[2, 1, 1]) + 0.4 * (0.8 * A[1, 2, 1] + 0.2 * A[2, 2, 1]) ) + 0.7 * (0.6 * (0.8 * A[1, 1, 2] + 0.2 * A[2, 1, 2]) + 0.4 * (0.8 * A[1, 2, 2] + 0.2 * A[2, 2, 2]) )
Было вычислено значение itp
при x...
, потому что метод weightedindexes
был вызван с единственной функцией, Interpolations.value_weights
(а это означает, что «для весов требовалось вычислить значение»).
Помните, что для интерполяции |
Для вычисления производных мы также передаем дополнительные функции, например Interpolations.gradient_weights
:
julia> wis = Interpolations.weightedindexes((Interpolations.value_weights, Interpolations.gradient_weights), Interpolations.itpinfo(itp)..., x) ((Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))), (Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))), (Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)))) julia> wis[1] (Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))) julia> wis[2] (Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.30000000000000004, 0.7))) julia> wis[3] (Interpolations.WeightedAdjIndex{2,Float64}(1, (0.8, 0.19999999999999996)), Interpolations.WeightedAdjIndex{2,Float64}(1, (0.6000000000000001, 0.3999999999999999)), Interpolations.WeightedAdjIndex{2,Float64}(1, (-1.0, 1.0))) julia> Interpolations.InterpGetindex(A)[wis[1]...] 1.0 julia> Interpolations.InterpGetindex(A)[wis[2]...] 3.000000000000001 julia> Interpolations.InterpGetindex(A)[wis[3]...] 9.0
В этом случае можно заметить, что wis
— 3-элементный кортеж из 3-элементных кортежей. A[wis[i]...]
можно использовать для вычисления i
-го компонента градиента.
Если внимательно посмотреть на каждый элемент в wis
, можно заметить, что в каждом «внутреннем» 3-элементном кортеже повторяются два из трех элементов компонента wis
, полученного выше при вызове weightedindexes
только с функцией value_weights
. wis[1]
заменяет первый элемент взвешенным индексом с весами (-1.0, 1.0)
, что соответствует вычислению углового коэффициента по измерению. Аналогичным образом, wis[2]
и wis[3]
заменяют соответственно второй и третий индексы значений таким же вычислением углового коэффициента.
Гессиан вычисляется очень похожим образом. Отличие в том, что иногда необходимо заменить два разных индекса или один индекс на набор весов, соответствующих второй производной.
Таким образом, производные в определенных направлениях вычисляются путем простой «замены весов» по соответствующим измерениям.
Код для выполнения такой замены несколько сложен из-за необходимости поддерживать произвольную размерность, чтобы в Julia можно было успешно выполнять вывод типов. В нем применяются особые операции с кортежами, иногда называемые «программированием кортежей в стиле Лиспа». Дополнительные советы, касающиеся такого программирования, можно найти на форуме обсуждения Julia. Реализовать такую замену можно было бы и с помощью генерируемых функций, но это существенно увеличило бы время компиляции и могло бы привести к проблемам с «возрастом мира».
Поддержка GPU
В настоящее время Interpolations.jl
поддерживает использование интерполянтов на GPU посредством трансляции.
Базовый рабочий процесс выглядит так:
julia> using Interpolations, Adapt, CUDA # Или любой другой пакет для работы с GPU
julia > itp = Interpolations.interpolate([1, 2, 3], (BSpline(Linear()))); # создаем объект интерполянта в памяти ЦП
julia> cuitp = adapt(CuArray{Float32}, itp); # адаптируем его к памяти GPU
julia > cuitp.(1:0.5:2) # вызываем объект интерполянта посредством трансляции
julia> gradient.(Ref(cuitp), 1:0.5:2)
Для этой цели в ITP <: AbstractInterpolation
должен быть определен собственный метод Adapt.adapt_structure(to, itp::ITP)
, который создает новый объект ITP
с адаптированными полями (adapt(to, itp.fieldname)
) itp
. Адаптацию полей можно пропустить, если известно, что объект уже совместим с GPU, как в случае, например, с диапазоном isbit
.
Некоторые адапторы могут изменять тип хранимых данных. Убедитесь в том, что у адаптированного объекта |
Кроме того, для всех совместимых с GPU типов AbstractInterpolation
должен быть определен собственный метод Interpolations.root_storage_type
. Эта функция позволяет изменять механизм трансляции, перегружая BroadcastStyle
по умолчанию. Дополнительные сведения см. в разделе Настройка трансляции.