Skip to content

Commit b3fb9f7

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 Make suggested docs changes
1 parent 9e273b2 commit b3fb9f7

File tree

5 files changed

+265
-62
lines changed

5 files changed

+265
-62
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: 157 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ scalar_field_matrix
107107

108108
A FieldMatrix entry can be:
109109

110-
- 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.
110+
- A `UniformScaling`, which contains a `Number`
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.
@@ -162,35 +162,172 @@ An example of this is:
162162
A = MatrixFields.FieldMatrix((@name(name1), @name(name2)) => sample_entry)
163163
```
164164

165-
Now consider what happens indexing `A` with the key `(@name(name1.foo.bar.buz), @name(name2.biz.bop.fud))`.
165+
Now consider what happens when 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.
174171

175-
The recursive indexing of an internal entry given some entry `entry` and internal_key `internal_name_pair`
172+
The recursive indexing of an internal entry given some entry `entry` and internal key `internal_name_pair`
176173
works as follows:
177174

178175
1. If the `internal_name_pair` is blank, return `entry`
179-
2. If the element type of each band of `entry` is an `Axis2Tensor`, and `internal_name_pair` is of the form
180-
`(@name(components.data.1...), @name(components.data.2...))` (potentially with different numbers),
181-
then extract the specified component, and recurse on it with the remaining `internal_name_pair`.
176+
2. If the element type of each band of `entry` is an `Axis2Tensor`, and `internal_name_pair` is of the form `(@name(components.data.1...), @name(components.data.2...))` (potentially with different numbers), then extract the specified component, and recurse on it with the remaining `internal_name_pair`.
182177
3. If the element type of each band of `entry` is a `Geometry.AdjointAxisVector`, then recurse on the parent of the adjoint.
183-
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]`
187-
6. At this point, if none of the previous cases are true, both `internal_name_pair[1]` and `internal_name_pair[2]` should be
188-
non-empty, and it is assumed that `entry` is being used to implicitly represent some tensor structure. If the first name in
189-
`internal_name_pair[1]` is equivalent to `internal_name_pair[2]`, then both the first names are dropped, and entry is recursed onto.
190-
If the first names are different, both the first names are dropped, and the zero of entry is recursed onto.
178+
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`, 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]`
179+
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`, 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]`
180+
6. At this point, if none of the previous cases are true, both `internal_name_pair[1]` and `internal_name_pair[2]` should be non-empty, and it is assumed that `entry` is being used to implicitly represent some tensor structure. If the first name in `internal_name_pair[1]` is equivalent to `internal_name_pair[2]`, then both the first names are dropped, and entry is recursed onto. If the first names are different, both the first names are dropped, and the zero of entry is recursed onto.
191181

192182
When the entry is a `ColumnWiseBandMatrixField`, indexing it will return a broadcasted object in
193183
the following situations:
194184

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

0 commit comments

Comments
 (0)