Skip to content

Commit 2579639

Browse files
committed
prototype ConservativeRegridding with FV approximation
1 parent 24d6838 commit 2579639

File tree

3 files changed

+178
-0
lines changed

3 files changed

+178
-0
lines changed

src/Remapping/Remapping.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,6 @@ using ..RecursiveApply
2222
include("remapping_utils.jl")
2323
include("interpolate_array.jl")
2424
include("distributed_remapping.jl")
25+
include("conservative_regridding.jl")
2526

2627
end
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
"""
2+
get_element_vertices(space::SpectralElementSpace2D)
3+
4+
Returns a vector of vectors, each containing the coordinates of the vertices
5+
of an element. The vertices are in clockwise order for each element, and the
6+
first coordinate pair is repeated at the end.
7+
8+
Also performs a check for zero area polygons, and throws an error if any are found.
9+
10+
This is the format expected by ConservativeRegridding.jl to construct a
11+
Regridder object.
12+
"""
13+
function get_element_vertices(space)
14+
# Get the indices of the vertices of the elements, in clockwise order for each element
15+
Nh = Meshes.nelements(space.grid.topology.mesh)
16+
Nq = Quadratures.degrees_of_freedom(Spaces.quadrature_style(space))
17+
vertex_inds = [
18+
CartesianIndex(i, j, 1, 1, e) # f and v are 1 for SpectralElementSpace2D
19+
for e in 1:Nh
20+
for (i, j) in [(1, 1), (1, Nq), (Nq, Nq), (Nq, 1), (1, 1)]
21+
] # repeat the first coordinate pair at the end
22+
23+
# Get the lat and lon at each vertex index
24+
coords = Fields.coordinate_field(space)
25+
vertex_coords = [
26+
(Fields.field_values(coords.lat)[ind], Fields.field_values(coords.long)[ind])
27+
for ind in vertex_inds
28+
]
29+
30+
# Put each polygon into a vector, with the first coordinate pair repeated at the end
31+
vertices = collect(Iterators.partition(vertex_coords, 5))
32+
33+
# Check for zero area polygons
34+
for polygon in vertices
35+
if allequal(first.(polygon)) || allequal(last.(polygon))
36+
@error "Zero area polygon found in vertices" polygon
37+
end
38+
end
39+
return vertices
40+
end
41+
42+
### These functions are used to facilitate storing a single value per element on a field
43+
### rather than one value per node.
44+
"""
45+
integrate_each_element(field)
46+
47+
Integrate the field over each element of the space.
48+
Returns a vector with length equal to the number of elements in the space,
49+
containing the integrated value over the nodes of each element.
50+
"""
51+
function integrate_each_element(field)
52+
space = axes(field)
53+
weighted_values =
54+
RecursiveApply.rmul.(
55+
Spaces.weighted_jacobian(space),
56+
Fields.todata(field),
57+
)
58+
59+
Nh = Meshes.nelements(space.grid.topology.mesh)
60+
integral_each_element = zeros(Float64, Nh)
61+
Nq = Quadratures.degrees_of_freedom(Spaces.quadrature_style(space))
62+
for e in 1:Nh # loop over each element
63+
for i in 1:Nq
64+
for j in 1:Nq
65+
integral_each_element[e] += weighted_values[CartesianIndex(i, j, 1, 1, e)]
66+
end
67+
end
68+
end
69+
return integral_each_element
70+
end
71+
72+
"""
73+
get_value_per_element(field, ones_field)
74+
75+
Get one value per element of a field by integrating over the nodes of
76+
each element and dividing by the area of the element.
77+
78+
Here `ones_field` is a field on the same space as `field` with all
79+
values set to 1.
80+
"""
81+
function get_value_per_element(field, ones_field)
82+
integral_each_element = integrate_each_element(field)
83+
area_each_element = integrate_each_element(ones_field)
84+
return integral_each_element ./ area_each_element
85+
end
86+
87+
"""
88+
set_value_per_element!(field, value_per_element)
89+
90+
Set the values of a field with the provided values in each element.
91+
Each node within an element will have the same value.
92+
93+
The input vector is expected to be of length equal to the number of elements in
94+
the space.
95+
"""
96+
function set_value_per_element!(field, value_per_element)
97+
space = axes(field)
98+
Nh = Meshes.nelements(space.grid.topology.mesh)
99+
Nq = Quadratures.degrees_of_freedom(Spaces.quadrature_style(space))
100+
101+
@assert length(value_per_element) == Nh "Length of value_per_element must be equal to the number of elements in the space"
102+
103+
# Set the value in each node of each element to the value per element
104+
for e in 1:Nh
105+
for i in 1:Nq
106+
for j in 1:Nq
107+
Fields.field_values(field)[CartesianIndex(i, j, 1, 1, e)] =
108+
value_per_element[e]
109+
end
110+
end
111+
end
112+
return field
113+
end
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using ClimaCore:
2+
CommonSpaces, Remapping, Fields, Spaces, RecursiveApply, Meshes, Quadratures
3+
using ConservativeRegridding
4+
5+
6+
7+
# TODO figure out if 0-area elements is expected with odd h_elem
8+
9+
space1 = CommonSpaces.CubedSphereSpace(;
10+
radius = 10,
11+
n_quad_points = 3,
12+
h_elem = 8,
13+
)
14+
space2 = CommonSpaces.CubedSphereSpace(;
15+
radius = 10,
16+
n_quad_points = 4,
17+
h_elem = 6,
18+
)
19+
20+
vertices1 = Remapping.get_element_vertices(space1)
21+
vertices2 = Remapping.get_element_vertices(space2)
22+
23+
# Pass in destination vertices first, source vertices second
24+
# TODO open issue in CR.jl about ordering of inputs source/dest
25+
regridder_1_to_2 = ConservativeRegridding.Regridder(vertices2, vertices1)
26+
regridder_2_to_1 = ConservativeRegridding.Regridder(vertices1, vertices2)
27+
28+
# Define a field on the first space, to use as our source field
29+
field1 = Fields.coordinate_field(space1).lat
30+
ones_field1 = Fields.ones(space1)
31+
32+
# Check that integrating over each element and summing gives the same result as integrating over the whole domain
33+
@assert isapprox(sum(Remapping.integrate_each_element(field1)), sum(field1), atol = 1e-12)
34+
# Check that integrating 1 over each element and summing gives the same result as integrating 1 over the whole domain
35+
@assert sum(Remapping.integrate_each_element(ones_field1)) sum(ones_field1)
36+
37+
# Get one value per element in the field, equal to the average of the values at nodes of the element
38+
value_per_element1 = Remapping.get_value_per_element(field1, ones_field1)
39+
40+
# Allocate a vector with length equal to the number of elements in the target space
41+
value_per_element2 = zeros(Float64, Meshes.nelements(space2.grid.topology.mesh))
42+
ConservativeRegridding.regrid!(value_per_element2, regridder_1_to_2, value_per_element1)
43+
44+
# Now that we have our regridded vector, put it onto a field on the second space
45+
field2 = Fields.zeros(space2)
46+
Remapping.set_value_per_element!(field2, value_per_element2)
47+
field1_one_value_per_element = Fields.zeros(space1)
48+
Remapping.set_value_per_element!(field1_one_value_per_element, value_per_element1)
49+
50+
# # Plot the fields
51+
# using ClimaCoreMakie
52+
# using GLMakie
53+
# fig = ClimaCoreMakie.fieldheatmap(field1)
54+
# save("field1.png", fig)
55+
# fig = ClimaCoreMakie.fieldheatmap(field1_one_value_per_element)
56+
# save("field1_one_value_per_element.png", fig)
57+
# fig = ClimaCoreMakie.fieldheatmap(field2)
58+
# save("field2.png", fig)
59+
60+
# Check the conservation error
61+
abs_error = abs(sum(field1) - sum(field2))
62+
@assert abs_error < 1e-12
63+
abs_error_one_value_per_element = abs(sum(field1_one_value_per_element) - sum(field2))
64+
@assert abs_error_one_value_per_element < 2e-12

0 commit comments

Comments
 (0)