Skip to content

Commit 1c48e71

Browse files
committed
examples/fci-operators
Shows how to use the PetscOperators, and compares against the yup/ydown operators. Includes a script to generate the mesh, which currently requires a branch of Zoidberg to run. The code to calculate Grad_par, Div_par and Div_par_Grad_par from forward and backward operators is now in `PetscOperators::getParallel()`. That calculates and returns a collection of `Parallel` operators. This example could be turned into a test: - `Grad_par` should be close between the two methods - Volume integrals of `Div_par` and `Div_par_Grad_par` results should be near zero (flux conserved).
1 parent fb34b2a commit 1c48e71

File tree

8 files changed

+361
-0
lines changed

8 files changed

+361
-0
lines changed

examples/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ add_subdirectory(dalf3)
1818
add_subdirectory(eigen-box)
1919
add_subdirectory(elm-pb)
2020
add_subdirectory(elm-pb-outerloop)
21+
add_subdirectory(fci-operators)
2122
add_subdirectory(fci-wave)
2223
add_subdirectory(finite-volume/diffusion)
2324
add_subdirectory(finite-volume/fluid)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
cmake_minimum_required(VERSION 3.13)
2+
3+
project(fci-operators LANGUAGES CXX)
4+
5+
if(NOT TARGET bout++::bout++)
6+
find_package(bout++ REQUIRED)
7+
endif()
8+
9+
bout_add_example(
10+
fci-operators
11+
SOURCES fci_operators_example.cxx
12+
DATA_DIRS data
13+
EXTRA_FILES rotating-ellipse-20x8x20.fci.nc makeplots.py
14+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[mesh]
2+
file = "rotating-ellipse-20x8x20.fci.nc"
3+
4+
[mesh:paralleltransform]
5+
type = fci
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include "bout/bout.hxx"
2+
#include "bout/difops.hxx"
3+
#include "bout/field.hxx"
4+
#include "bout/field3d.hxx"
5+
#include "bout/field_factory.hxx"
6+
#include "bout/globals.hxx"
7+
#include "bout/options.hxx"
8+
#include "bout/petsc_operators.hxx"
9+
10+
int main(int argc, char** argv) {
11+
BoutInitialise(argc, argv);
12+
13+
const PetscOperators ops(bout::globals::mesh);
14+
15+
// Parallel operators
16+
const auto parallel = ops.getParallel();
17+
18+
// Test field
19+
Field3D f = FieldFactory::get()->create3D("sin(y) + cos(z)");
20+
bout::globals::mesh->communicate(f);
21+
f.applyParallelBoundary("parallel_dirichlet_o2");
22+
23+
Field3D f_neumann = FieldFactory::get()->create3D("sin(y) + cos(z)");
24+
bout::globals::mesh->communicate(f_neumann);
25+
f_neumann.applyParallelBoundary("parallel_neumann_o1");
26+
27+
Options dump;
28+
dump["f"] = f;
29+
dump["dV"] = parallel.dV;
30+
31+
dump["grad_par_op"] = parallel.Grad_par(f);
32+
dump["grad_par_yud"] = Grad_par(f);
33+
34+
dump["div_par_op"] = parallel.Div_par(f);
35+
36+
auto* coords = bout::globals::mesh->getCoordinates();
37+
bout::globals::mesh->communicate(coords->Bxy);
38+
coords->Bxy.applyParallelBoundary("parallel_neumann_o1");
39+
dump["div_par_yud"] = Div_par(f);
40+
41+
dump["div_par_grad_par_op"] = parallel.Div_par_Grad_par(f_neumann);
42+
dump["div_par_grad_par_yud"] = Div_par_K_Grad_par(1.0, f_neumann);
43+
44+
bout::writeDefaultOutputFile(dump);
45+
46+
BoutFinalise();
47+
return 0;
48+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from boutdata import collect
2+
import matplotlib.pyplot as plt
3+
import numpy as np
4+
5+
path = "data"
6+
7+
grad_par_op = collect("grad_par_op", path=path)
8+
grad_par_yud = collect("grad_par_yud", path=path)
9+
10+
div_par_op = collect("div_par_op", path=path)
11+
div_par_yud = collect("div_par_yud", path=path)
12+
13+
dV = collect("dV", path=path) # Cell volume
14+
15+
# Integrate divergence
16+
op_div_int = np.sum((dV * div_par_op)[2:-2, :, :])
17+
yud_div_int = np.sum((dV * div_par_yud)[2:-2, :, :])
18+
19+
print(f"Div_par volume integral Op: {op_div_int} Yup/down: {yud_div_int}")
20+
21+
div_par_grad_par_op = collect("div_par_grad_par_op", path=path)
22+
div_par_grad_par_yud = collect("div_par_grad_par_yud", path=path)
23+
24+
# Integrate divergence
25+
op_div2_int = np.sum((dV * div_par_grad_par_op)[2:-2, :, :])
26+
yud_div2_int = np.sum((dV * div_par_grad_par_yud)[2:-2, :, :])
27+
28+
print(f"Div_par_Grad_par volume integral Op: {op_div2_int} Yup/down: {yud_div2_int}")
29+
30+
31+
def plot_comparison(a, alabel, b, blabel):
32+
fig, axs = plt.subplots(1, 3, figsize=(10, 3))
33+
vmax = max([np.amax(a), np.amax(b)])
34+
vmin = min([np.amin(a), np.amin(b)])
35+
36+
cs = axs[0].contourf(a, vmin=vmin, vmax=vmax)
37+
fig.colorbar(cs, ax=axs[0])
38+
axs[0].set_title(alabel)
39+
40+
axs[1].contourf(b, vmin=vmin, vmax=vmax)
41+
axs[1].set_title(blabel)
42+
43+
cs = axs[2].contourf(a - b)
44+
axs[2].set_title("Difference")
45+
fig.colorbar(cs, ax=axs[2])
46+
47+
return fig
48+
49+
50+
fig = plot_comparison(
51+
grad_par_op[2:-2, 2, :], "Operator", grad_par_yud[2:-2, 2, :], "Yup/down"
52+
)
53+
fig.suptitle("Parallel Gradient")
54+
fig.tight_layout()
55+
fig.savefig("gradient.pdf")
56+
fig.savefig("gradient.png")
57+
58+
fig = plot_comparison(
59+
div_par_op[2:-2, 2, :],
60+
f"Operator [integral {op_div_int:.2e}]",
61+
div_par_yud[2:-2, 2, :],
62+
f"Yup/down [integral {yud_div_int:.2e}]",
63+
)
64+
fig.suptitle("Parallel Divergence")
65+
fig.tight_layout()
66+
fig.savefig("divergence.pdf")
67+
fig.savefig("divergence.png")
68+
69+
fig = plot_comparison(
70+
div_par_grad_par_op[2:-2, 2, :],
71+
f"Operator [integral {op_div2_int:.2e}]",
72+
div_par_grad_par_yud[2:-2, 2, :],
73+
f"Yup/down [integral {yud_div2_int:.2e}]",
74+
)
75+
fig.suptitle("Parallel diffusion")
76+
fig.tight_layout()
77+
fig.savefig("diffusion.pdf")
78+
fig.savefig("diffusion.png")
79+
80+
81+
plt.show()
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python
2+
3+
# Create grids for a rotating ellipse stellarator, based on curvilinear grids
4+
5+
import numpy as np
6+
7+
import zoidberg
8+
9+
#############################################################################
10+
# Define the magnetic field
11+
12+
# Length in y after which the coils return to their starting (R,Z) locations
13+
yperiod = 2.0 * np.pi / 5
14+
15+
xcentre = 2.0
16+
17+
magnetic_field = zoidberg.field.RotatingEllipse(
18+
xcentre=xcentre, # Major radius of axis [m]
19+
radius=0.8, # Minor radius of the coils [m]
20+
I_coil=0.4, # Coil current
21+
yperiod=yperiod,
22+
Btor=1.0, # Toroidal magnetic field
23+
)
24+
25+
#############################################################################
26+
# Create the inner flux surface, starting at a point at phi=0
27+
# To do this we need to define the y locations of the poloidal points
28+
# where we will construct grids
29+
30+
start_r = xcentre + 0.4
31+
start_z = 0.0
32+
33+
nslices = 8 # Number of poloidal slices
34+
ycoords = np.linspace(0, yperiod, nslices)
35+
npoints = 20 # Points per poloidal slice
36+
37+
rzcoord, ycoords = zoidberg.fieldtracer.trace_poincare(
38+
magnetic_field, start_r, start_z, yperiod, y_slices=ycoords, revs=npoints, nover=1
39+
)
40+
41+
inner_lines = []
42+
for i in range(nslices):
43+
r = rzcoord[:, i, 0, 0]
44+
z = rzcoord[:, i, 0, 1]
45+
line = zoidberg.rzline.line_from_points(r, z)
46+
# Re-map the points so they're approximately uniform in distance along the surface
47+
# Note that this results in some motion of the line
48+
line = line.equallySpaced()
49+
inner_lines.append(line)
50+
51+
# Now have a list of y coordinates (ycoords) and inner lines (inner_lines)
52+
53+
#############################################################################
54+
# Generate a fixed circle for the outer boundary
55+
56+
outer_line = zoidberg.rzline.circle(R0=xcentre, r=0.6)
57+
58+
#############################################################################
59+
# Now have inner and outer boundaries for each poloidal grid
60+
# Generate a grid on each poloidal slice using the elliptic grid generator
61+
62+
nx = 20
63+
nz = 20
64+
65+
pol_grids = [
66+
zoidberg.poloidal_grid.grid_elliptic(inner_line, outer_line, nx, nz)
67+
for inner_line in inner_lines
68+
]
69+
70+
#############################################################################
71+
# Create a grid, then calculate forward and backward maps
72+
73+
grid = zoidberg.grid.Grid(pol_grids, ycoords, yperiod, yperiodic=True)
74+
75+
maps = zoidberg.make_maps(grid, magnetic_field)
76+
77+
#############################################################################
78+
# Interpolation weights
79+
80+
weight_vars = zoidberg.weights.calculate_parallel_map_operators(maps)
81+
maps.update(weight_vars)
82+
83+
#############################################################################
84+
# Write grid file
85+
86+
filename = f"rotating-ellipse-{nx}x{nslices}x{nz}.fci.nc"
87+
88+
print(f"Writing to grid file '{filename}'")
89+
zoidberg.write_maps(
90+
grid, magnetic_field, maps, gridfile=filename, new_names=False, metric2d=False
91+
)

include/bout/petsc_operators.hxx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,21 @@ public:
419419
return PetscOperator::diagonal(mapping, f);
420420
}
421421

422+
/// A standard collection of operators
423+
/// parallel to the magnetic field
424+
struct Parallel {
425+
PetscOperator Grad_par; ///< Gradient
426+
PetscOperator Div_par; ///< Divergence
427+
PetscOperator Div_par_Grad_par; ///< Diffusion
428+
Field3D dV; ///< Cell volume
429+
};
430+
431+
/// Calculate a set of parallel operators
432+
///
433+
/// This assumes that "forward" and "backward" operators
434+
/// are stored in the mesh.
435+
Parallel getParallel() const;
436+
422437
private:
423438
/// Read a `Field3D` from the mesh.
424439
///

src/mesh/petsc_operators.cxx

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,110 @@ PetscOperator PetscOperators::get(const std::string& name) const {
299299
this->meshGetArray<BoutReal>(this->mesh, name + "_weights"));
300300
}
301301

302+
PetscOperators::Parallel PetscOperators::getParallel() const {
303+
// Read maps from the mesh
304+
auto forward = this->get("forward");
305+
auto backward = this->get("backward");
306+
307+
// ---- Construct Grad_par ----
308+
//
309+
// Create a parallel gradient operator by combining the parallel
310+
// length dl = dy * sqrt(g_22) with forward & backward operators
311+
auto* coords = this->mesh->getCoordinates();
312+
Field3D dl = coords->dy * sqrt(coords->g_22);
313+
dl.splitParallelSlices();
314+
dl.yup() = 0.0;
315+
dl.ydown() = 0.0;
316+
dl.applyParallelBoundary("parallel_neumann_o1");
317+
318+
auto inv_2dl = 0.5 / dl;
319+
inv_2dl.splitParallelSlices();
320+
inv_2dl.yup() = 0.0;
321+
inv_2dl.ydown() = 0.0;
322+
inv_2dl.applyParallelBoundary("parallel_neumann_o1");
323+
324+
inv_2dl.yup() *= 0.5;
325+
inv_2dl.ydown() *= 0.5;
326+
327+
auto inv_2dl_op = this->diagonal(inv_2dl);
328+
329+
auto Grad_par = inv_2dl_op * (forward - backward);
330+
331+
// ---- Construct Div_par ----
332+
//
333+
// Use the Support Operator Method (SOM) to calculate
334+
// Div_par from Grad_par and cell volumes.
335+
336+
// Cell volume
337+
auto dV = coords->J * coords->dx * coords->dy * coords->dz;
338+
dV.splitParallelSlices();
339+
dV.yup() = 0.0;
340+
dV.ydown() = 0.0;
341+
dV.applyParallelBoundary("parallel_neumann_o1");
342+
auto dV_op = this->diagonal(dV);
343+
344+
Field3D neg_inv_dV = -1. / dV;
345+
neg_inv_dV.splitParallelSlices();
346+
neg_inv_dV.yup() = 0.0;
347+
neg_inv_dV.ydown() = 0.0;
348+
neg_inv_dV.applyParallelBoundary("parallel_neumann_o1");
349+
auto neg_inv_dV_op = this->diagonal(neg_inv_dV);
350+
351+
auto Div_par = neg_inv_dV_op * Grad_par.transpose() * dV_op;
352+
353+
// ---- Construct Div_par_Grad_par ----
354+
//
355+
// Requires gradients between planes, at +1/2 and -1/2, and interpolation
356+
// operators to calculate quantities between cells.
357+
358+
// Identity operator
359+
Field3D one{1.0};
360+
one.splitParallelSlices();
361+
one.yup() = 1.0;
362+
one.ydown() = 1.0;
363+
const auto identity = this->diagonal(one);
364+
365+
// Interpolate at + 1/2
366+
const auto interp_plus_op = (identity + forward) * 0.5;
367+
368+
// dl averaged at +1/2
369+
const Field3D dl_plus = interp_plus_op(dl);
370+
Field3D inv_dl_plus = 1. / dl_plus;
371+
inv_dl_plus.splitParallelSlices();
372+
inv_dl_plus.yup() = 0.0;
373+
inv_dl_plus.ydown() = 0.0;
374+
inv_dl_plus.applyParallelBoundary("parallel_neumann_o1");
375+
const auto inv_dl_plus_op = this->diagonal(inv_dl_plus);
376+
377+
// Gradient at + 1/2
378+
const auto Grad_plus = inv_dl_plus_op * (forward - identity);
379+
380+
// Divergence at -1/2
381+
const auto Div_minus = neg_inv_dV_op * Grad_plus.transpose() * dV_op;
382+
383+
// Interpolate at - 1/2
384+
auto interp_minus_op = (identity + backward) * 0.5;
385+
386+
// dl averaged at -1/2
387+
const Field3D dl_minus = interp_minus_op(dl);
388+
Field3D inv_dl_minus = 1. / dl_minus;
389+
inv_dl_minus.splitParallelSlices();
390+
inv_dl_minus.yup() = 0.0;
391+
inv_dl_minus.ydown() = 0.0;
392+
inv_dl_minus.applyParallelBoundary("parallel_neumann_o1");
393+
const auto inv_dl_minus_op = this->diagonal(inv_dl_minus);
394+
395+
// Gradient at - 1/2
396+
auto Grad_minus = inv_dl_minus_op * (identity - backward);
397+
398+
// Divergence at +1/2
399+
const auto Div_plus = neg_inv_dV_op * Grad_minus.transpose() * dV_op;
400+
401+
// Div(Grad_par()) operator
402+
auto Div_par_Grad_par = ((Div_minus * Grad_plus) + (Div_plus * Grad_minus)) * 0.5;
403+
404+
return Parallel{std::move(Grad_par), std::move(Div_par), std::move(Div_par_Grad_par),
405+
std::move(dV)};
406+
}
407+
302408
#endif // BOUT_HAS_PETSC

0 commit comments

Comments
 (0)