Skip to content

Commit 0c39f15

Browse files
committed
Refactor FieldMatrix indexing for readability
Also add support for tensor slicing and improved docs wip finish docs minor formatting fix another minor docs formatting fix
1 parent 7635b2b commit 0c39f15

File tree

5 files changed

+271
-51
lines changed

5 files changed

+271
-51
lines changed

.buildkite/pipeline.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -874,13 +874,13 @@ steps:
874874
agents:
875875
slurm_gpus: 1
876876

877-
- label: "Unit: scalar_field_matrix (CPU)"
878-
key: cpu_scalar_field_matrix
879-
command: "julia --color=yes --check-bounds=yes --project=.buildkite test/MatrixFields/scalar_field_matrix.jl"
877+
- label: "Unit: field_matrix_indexing (CPU)"
878+
key: cpu_field_matrix_indexing
879+
command: "julia --color=yes --check-bounds=yes --project=.buildkite test/MatrixFields/field_matrix_indexing.jl"
880880

881-
- label: "Unit: scalar_field_matrix (GPU)"
882-
key: gpu_scalar_field_matrix
883-
command: "julia --color=yes --project=.buildkite test/MatrixFields/scalar_field_matrix.jl"
881+
- label: "Unit: field_matrix_indexing (GPU)"
882+
key: gpu_field_matrix_indexing
883+
command: "julia --color=yes --project=.buildkite test/MatrixFields/field_matrix_indexing.jl"
884884
env:
885885
CLIMACOMMS_DEVICE: "CUDA"
886886
agents:

docs/src/matrix_fields.md

Lines changed: 164 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ scalar_field_matrix
108108
A FieldMatrix entry can be:
109109

110110
- An `UniformScaling`, which contains a `Number`
111-
- A `DiagonalMatrixRow`, which can contain aything
112-
- A `ColumnwiseBandMatrixField`, where each row is a [`BandMatrixRow`](@ref) where the band element type is representable with the space's base number type.
111+
- A `DiagonalMatrixRow`, which can contain either a `Number` or a tensor (represented as a `Geometry.Axis2Tensor`)
112+
- A `ColumnwiseBandMatrixField`, where each value is a [`BandMatrixRow`](@ref) with entries of any type that can be represented using the field's base number type.
113113

114114
If an entry contains a composite type, the fields of that type can be extracted.
115115
This is also true for nested composite types.
@@ -164,10 +164,7 @@ An example of this is:
164164

165165
Now consider what happens indexing `A` with the key `(@name(name1.foo.bar.buz), @name(name2.biz.bop.fud))`.
166166

167-
First, a function searches the keys of `A` for a key that `(@name(foo.bar.buz), @name(biz.bop.fud))`
168-
is a child of. In this example, `(@name(foo.bar.buz), @name(biz.bop.fud))` is a child of
169-
the key `(@name(name1), @name(name2))`, and
170-
`(@name(foo.bar.buz), @name(biz.bop.fud))` is referred to as the internal key.
167+
First, `getindex` finds a key in `A` that contains the key being indexed. In this example, `(@name(name1.foo.bar.buz), @name(name2.biz.bop.fud))` is contained within `(@name(name1), @name(name2))`, so `(@name(name1), @name(name2))` is called the "parent key" and `(@name(foo.bar.buz), @name(biz.bop.fud))` is referred to as the "internal key".
171168

172169
Next, the entry that `(@name(name1), @name(name2))` is paired with is recursively indexed
173170
by the internal key.
@@ -181,9 +178,9 @@ works as follows:
181178
then extract the specified component, and recurse on it with the remaining `internal_name_pair`.
182179
3. If the element type of each band of `entry` is a `Geometry.AdjointAxisVector`, then recurse on the parent of the adjoint.
183180
4. If `internal_name_pair[1]` is not empty, and the first name in it is a field of the element type of each band of `entry`,
184-
extract that field from `entry`, and recurse on the it with the remaining names of `internal_name_pair[1]` and all of `internal_name_pair[2]`
185-
5. If `internal_name_pair[2]` is not empty, and the first name in it is a field of the element type of each row of `entry`,
186-
extract that field from `entry`, and recurse on the it with all of `internal_name_pair[1]` and the remaining names of `internal_name_pair[2]`
181+
extract that field from `entry`, and recurse into it with the remaining names of `internal_name_pair[1]` and all of `internal_name_pair[2]`
182+
5. If `internal_name_pair[2]` is not empty, and the first name in it is a field of the element type of each band of `entry`,
183+
extract that field from `entry`, and recurse into it with all of `internal_name_pair[1]` and the remaining names of `internal_name_pair[2]`
187184
6. At this point, if none of the previous cases are true, both `internal_name_pair[1]` and `internal_name_pair[2]` should be
188185
non-empty, and it is assumed that `entry` is being used to implicitly represent some tensor structure. If the first name in
189186
`internal_name_pair[1]` is equivalent to `internal_name_pair[2]`, then both the first names are dropped, and entry is recursed onto.
@@ -194,3 +191,161 @@ the following situations:
194191

195192
1. The internal key indexes to a type different than the basetype of the entry
196193
2. The internal key indexes to a zero-ed value
194+
195+
```@setup 2
196+
using ClimaCore.CommonSpaces
197+
using ClimaCore.Geometry
198+
using ClimaCore.Fields
199+
import ClimaCore: MatrixFields
200+
import ClimaCore.MatrixFields: @name
201+
FT = Float64
202+
space = ColumnSpace(FT ;
203+
z_elem = 6,
204+
z_min = 0,
205+
z_max = 10,
206+
staggering = CellCenter()
207+
)
208+
f = map(x -> rand(Geometry.Covariant12Vector{Float64}), Fields.local_geometry_field(space))
209+
g = map(x -> rand(Geometry.Covariant12Vector{Float64}), Fields.local_geometry_field(space))
210+
identity_axis2tensor = Geometry.Covariant12Vector(FT(1), FT(0)) *
211+
Geometry.Contravariant12Vector(FT(1), FT(0))' +
212+
Geometry.Covariant12Vector(FT(0), FT(1)) *
213+
Geometry.Contravariant12Vector(FT(0), FT(1))'
214+
∂f_∂g = fill(MatrixFields.TridiagonalMatrixRow(-0.5 * identity_axis2tensor, identity_axis2tensor, -0.5 * identity_axis2tensor), space)
215+
J = MatrixFields.FieldMatrix((@name(f), @name(g))=> ∂f_∂g)
216+
```
217+
218+
## Optimizations
219+
220+
Each entry of a `FieldMatrix` can be a `ColumnwiseBandMatrixField`, a `DiagonalMatrixRow`, or an
221+
`UniformScaling`.
222+
223+
A `ColumnwiseBandMatrixField` is a `Field` with a `BandMatrixRow` at each point. It is intended
224+
to represent a collection of banded matrices, where there is one band matrix for each column
225+
of the space the `Field` is on. Beyond only storing the diagonals of the band matrix, an `entry`
226+
can be optimized to use less memory. Each optimized representation can be indexed equivalently to
227+
non optimized representation, and used in addition, subtraction, Matrix-vector multiplication,
228+
Matrix-matrix multiplication, `RecursiveApply`, and `FieldMatrixSolver`.
229+
230+
For the following sections, `space` is a column space with $N_v$ levels. A column space is
231+
used for simplicity in this example, but the optimizations work with any space with columns.
232+
233+
Let $f$ and $g$ be `Fields` on `space` with elements of type with elements of type
234+
`T_f` and `T_g`. $f_i$ and $g_i$ refers to the values of $f$ and $g$ at the $ 0 < i \leq N_v$ level.
235+
236+
Let $M$ be a $N_v \times N_v$ banded matrix with lower and upper bandwidth of $b_1$ and $b_2$.
237+
$M$ represents $\frac{\partial f}{\partial g}$, so $M_{i,j} = \frac{\partial f_i}{\partial g_j}$
238+
239+
### `ScalingFieldMatrixEntry` Optimization
240+
241+
Consider the case where $b_1 = 0$ and $b_2 = 0$, i.e $M$ is a diagonal matrix, and
242+
where $M = k * I$, and $k$ is of type `T_k`. This would happen if
243+
$\frac{\partial f_i}{\partial g_j} = [i=j] * k$. Instead of storing
244+
each element on the diagonal, less memory can be used by storing a single value that represents a scaling of the identity. This reduces memory usage by a factor of $N_v$.
245+
246+
```julia
247+
entry = fill(DiagonalMatrixRow(k), space)
248+
```
249+
250+
can also be represented by
251+
252+
```julia
253+
entry = DiagonalMatrixRow(k)
254+
```
255+
256+
or, if `T_k` is a scalar, then
257+
258+
```julia
259+
entry = I * k
260+
```
261+
262+
### Implicit Tensor Structure Optimization
263+
264+
The functions that index an entry with an internal key assume the implicit tensor structure optimization is being used
265+
when all of the following are true for `entry` where `T_k` is the element type of each band, and
266+
`(internal_key_1, internal_key_2)` is the internal key indexing `entry`.
267+
268+
- the first key in the `internal_key_1` name chain is not a fieldname of `T_k`
269+
- the first key in the `internal_key_2` name chain is not a fieldname of `T_k`
270+
- `internal_key_1` and `internal_key_2` are both not empty
271+
272+
For most use cases, `T_k` is a scalar.
273+
274+
If the above conditions are met, the optimization assumes that the user intends the
275+
entry to have an implicit tensor structure, with the values of type `T_k` representing a scaling of the
276+
tensor identity. If both the first and second name in the name pair are equivalent, then they index onto the diagonal,
277+
and the value is returned. Otherwise, they index off the diagonal, and a zero value
278+
is returned.
279+
280+
This optimization is intended to be used when `T_f = T_g`, and they are both `AxisVectors`
281+
The notation $f_{n}[i]$ where $0 < n \leq N_v$ refers to the $i$ component of the element
282+
at the $n$ vertical level of $f$. In the following example, `T_f=T_g=Covariant12Vector`, and
283+
$b_1 = b_2 = 1$, and
284+
285+
```math
286+
\frac{\partial f_n[i]}{\partial g_m[j]} = \begin{cases}
287+
-0.5, & \text{if } i = j \text{ and } m = n-1 \text{ or } m = n+1 \\
288+
1, & \text{if } i = j \text{ and } m = n \\
289+
0, & \text{if } i \neq j \text{ or } m < n -1 \text{ or } m > n +1
290+
\end{cases}
291+
```
292+
293+
The non-zero values of each row of `M` are equivalent in this example, but they can also vary in value.
294+
295+
```julia
296+
∂f_∂g = fill(MatrixFields.TridiagonalMatrixRow(-0.5 * identity_axis2tensor, identity_axis2tensor, -0.5 * identity_axis2tensor), space)
297+
J = MatrixFields.FieldMatrix((@name(f), @name(g))=> ∂f_∂g)
298+
```
299+
300+
`∂f_∂g` can be indexed into to get the partial derrivatives of individual components.
301+
302+
303+
```@example 2
304+
J[(@name(f.components.data.:(1)), @name(g.components.data.:(1)))]
305+
```
306+
307+
```@example 2
308+
J[(@name(f.components.data.:(2)), @name(g.components.data.:(1)))]
309+
```
310+
311+
This can be more optimally stored with the implicit tensor structure optimization:
312+
313+
```@example 2
314+
∂f_∂g = fill(MatrixFields.TridiagonalMatrixRow(-0.5, 1.0, -0.5), space) # hide
315+
J = MatrixFields.FieldMatrix((@name(f), @name(g))=> ∂f_∂g) # hide
316+
```
317+
318+
```julia
319+
∂f_∂g = fill(MatrixFields.TridiagonalMatrixRow(-0.5, 1.0, -0.5), space)
320+
J = MatrixFields.FieldMatrix((@name(f), @name(g))=> ∂f_∂g)
321+
```
322+
323+
```@example 2
324+
J[(@name(f.components.data.:(1)), @name(g.components.data.:(1)))]
325+
```
326+
327+
```@example 2
328+
Base.Broadcast.materialize(J[(@name(f.components.data.:(2)), @name(g.components.data.:(1)))])
329+
```
330+
331+
If it is the case that
332+
333+
```math
334+
\frac{\partial f_n[i]}{\partial g_m[j]} = \begin{cases}
335+
k, & \text{if } i = j \text{ and } m = n \\
336+
0, & \text{if } i \neq j \text{ or } m \neq n
337+
\end{cases}
338+
```
339+
340+
where $k$ is a constant scalar, both the implicit tensor structure optimization and
341+
`ScalingFieldMatrixEntry` optimization can both be applied:
342+
343+
```julia
344+
∂f_∂g = fill(MatrixFields.DiagonalMatrixRow(k), space)
345+
```
346+
347+
can equivalently be represented with
348+
349+
```julia
350+
∂f_∂g = MatrixFields.DiagonalMatrixRow(k)
351+
```

0 commit comments

Comments
 (0)