Документация разработчика
Построение интерполянта
В результате построения создается объект интерполяции. В некоторых случаях это сравнительно простой процесс: например, при использовании только интерполяционных схем 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 по умолчанию. Дополнительные сведения см. в разделе Настройка трансляции.